git.pm 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. #!/usr/bin/perl
  2. #
  3. # git support for dpkg-source
  4. #
  5. # Copyright © 2007 Joey Hess <joeyh@debian.org>.
  6. # Copyright © 2008 Frank Lichtenheld <djpig@debian.org>
  7. #
  8. # This program is free software; you can redistribute it and/or modify
  9. # it under the terms of the GNU General Public License as published by
  10. # the Free Software Foundation; either version 2 of the License, or
  11. # (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU General Public License
  19. # along with this program; if not, write to the Free Software
  20. # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  21. package Dpkg::Source::Package::V3::git;
  22. use strict;
  23. use warnings;
  24. use base 'Dpkg::Source::Package';
  25. use Cwd;
  26. use File::Basename;
  27. use File::Find;
  28. use File::Temp qw(tempdir);
  29. use Dpkg;
  30. use Dpkg::Gettext;
  31. use Dpkg::Compression;
  32. use Dpkg::ErrorHandling qw(error warning subprocerr syserr info);
  33. use Dpkg::Version qw(check_version);
  34. use Dpkg::Source::Archive;
  35. use Dpkg::Exit;
  36. use Dpkg::Source::Functions qw(erasedir);
  37. our $CURRENT_MINOR_VERSION = "0";
  38. # Remove variables from the environment that might cause git to do
  39. # something unexpected.
  40. delete $ENV{GIT_DIR};
  41. delete $ENV{GIT_INDEX_FILE};
  42. delete $ENV{GIT_OBJECT_DIRECTORY};
  43. delete $ENV{GIT_ALTERNATE_OBJECT_DIRECTORIES};
  44. delete $ENV{GIT_WORK_TREE};
  45. sub import {
  46. foreach my $dir (split(/:/, $ENV{PATH})) {
  47. if (-x "$dir/git") {
  48. return 1;
  49. }
  50. }
  51. error(_g("This source package can only be manipulated using git, which is not in the PATH."));
  52. }
  53. sub sanity_check {
  54. my $srcdir = shift;
  55. if (! -d "$srcdir/.git") {
  56. error(_g("source directory is not the top directory of a git repository (%s/.git not present), but Format git was specified"),
  57. $srcdir);
  58. }
  59. if (-s "$srcdir/.gitmodules") {
  60. error(_g("git repository %s uses submodules. This is not yet supported."),
  61. $srcdir);
  62. }
  63. # Symlinks from .git to outside could cause unpack failures, or
  64. # point to files they shouldn't, so check for and don't allow.
  65. if (-l "$srcdir/.git") {
  66. error(_g("%s is a symlink"), "$srcdir/.git");
  67. }
  68. my $abs_srcdir = Cwd::abs_path($srcdir);
  69. find(sub {
  70. if (-l $_) {
  71. if (Cwd::abs_path(readlink($_)) !~ /^\Q$abs_srcdir\E(\/|$)/) {
  72. error(_g("%s is a symlink to outside %s"),
  73. $File::Find::name, $srcdir);
  74. }
  75. }
  76. }, "$srcdir/.git");
  77. return 1;
  78. }
  79. # Returns a hash of arrays of git config values.
  80. sub read_git_config {
  81. my $file = shift;
  82. my %ret;
  83. open(GIT_CONFIG, '-|', "git", "config", "--file", $file, "--null", "-l") ||
  84. subprocerr("git config");
  85. local $/ = "\0";
  86. while (<GIT_CONFIG>) {
  87. chomp;
  88. my ($key, $value) = split(/\n/, $_, 2);
  89. push @{$ret{$key}}, $value;
  90. }
  91. close(GIT_CONFIG) || syserr(_g("git config exited nonzero"));
  92. return \%ret;
  93. }
  94. sub can_build {
  95. my ($self, $dir) = @_;
  96. return (-d "$dir/.git", _g("doesn't contain a git repository"));
  97. }
  98. sub do_build {
  99. my ($self, $dir) = @_;
  100. my @argv = @{$self->{'options'}{'ARGV'}};
  101. #TODO: warn here?
  102. # my @tar_ignore = map { "--exclude=$_" } @{$self->{'options'}{'tar_ignore'}};
  103. my $diff_ignore_regexp = $self->{'options'}{'diff_ignore_regexp'};
  104. $dir =~ s{/+$}{}; # Strip trailing /
  105. my ($dirname, $updir) = fileparse($dir);
  106. if (scalar(@argv)) {
  107. usageerr(_g("-b takes only one parameter with format `%s'"),
  108. $self->{'fields'}{'Format'});
  109. }
  110. my $sourcepackage = $self->{'fields'}{'Source'};
  111. my $basenamerev = $self->get_basename(1);
  112. my $basename = $self->get_basename();
  113. my $basedirname = $basename;
  114. $basedirname =~ s/_/-/;
  115. sanity_check($dir);
  116. my $old_cwd = getcwd();
  117. chdir($dir) ||
  118. syserr(_g("unable to chdir to `%s'"), $dir);
  119. # Check for uncommitted files.
  120. # To support dpkg-source -i, get a list of files
  121. # equivalent to the ones git status finds, and remove any
  122. # ignored files from it.
  123. my @ignores = "--exclude-per-directory=.gitignore";
  124. my $core_excludesfile = `git config --get core.excludesfile`;
  125. chomp $core_excludesfile;
  126. if (length $core_excludesfile && -e $core_excludesfile) {
  127. push @ignores, "--exclude-from='$core_excludesfile'";
  128. }
  129. if (-e ".git/info/exclude") {
  130. push @ignores, "--exclude-from=.git/info/exclude";
  131. }
  132. open(GIT_LS_FILES, '-|', "git", "ls-files", "--modified", "--deleted",
  133. "-z", "--others", @ignores) ||
  134. subprocerr("git ls-files");
  135. my @files;
  136. { local $/ = "\0";
  137. while (<GIT_LS_FILES>) {
  138. chomp;
  139. if (! length $diff_ignore_regexp ||
  140. ! m/$diff_ignore_regexp/o) {
  141. push @files, $_;
  142. }
  143. }
  144. }
  145. close(GIT_LS_FILES) || syserr(_g("git ls-files exited nonzero"));
  146. if (@files) {
  147. error(_g("uncommitted, not-ignored changes in working directory: %s"),
  148. join(" ", @files));
  149. }
  150. # git clone isn't used to copy the repo because the it might be an
  151. # unclonable shallow copy.
  152. chdir($old_cwd) ||
  153. syserr(_g("unable to chdir to `%s'"), $old_cwd);
  154. my $tmp = tempdir("$dirname.git.XXXXXX", DIR => $updir);
  155. push @Dpkg::Exit::handlers, sub { erasedir($tmp) };
  156. my $tardir = "$tmp/$dirname";
  157. mkdir($tardir) ||
  158. syserr(_g("cannot create directory %s"), $tardir);
  159. system("cp", "-a", "$dir/.git", $tardir);
  160. $? && subprocerr("cp -a $dir/.git $tardir");
  161. chdir($tardir) ||
  162. syserr(_g("unable to chdir to `%s'"), $tardir);
  163. # TODO support for creating a shallow clone for those cases where
  164. # uploading the whole repo history is not desired
  165. # Clean up the new repo to save space.
  166. # First, delete the whole reflog, which is not needed in a
  167. # distributed source package.
  168. system("rm", "-rf", ".git/logs");
  169. $? && subprocerr("rm -rf .git/logs");
  170. system("git", "gc", "--prune");
  171. $? && subprocerr("git gc --prune");
  172. # .git/gitweb is created and used by git instaweb and should not be
  173. # transferwed by source package.
  174. system("rm", "-rf", ".git/gitweb");
  175. $? && subprocerr("rm -rf .git/gitweb");
  176. # As an optimisation, remove the index. It will be recreated by git
  177. # reset during unpack. It's probably small, but you never know, this
  178. # might save a lot of space. (Also, the index file may not be
  179. # portable.)
  180. unlink(".git/index"); # error intentionally ignored
  181. chdir($old_cwd) ||
  182. syserr(_g("unable to chdir to `%s'"), $old_cwd);
  183. # Create the tar file
  184. my $debianfile = "$basenamerev.git.tar." . $self->{'options'}{'comp_ext'};
  185. info(_g("building %s in %s"),
  186. $sourcepackage, $debianfile);
  187. my $tar = Dpkg::Source::Archive->new(filename => $debianfile,
  188. compression => $self->{'options'}{'compression'},
  189. compression_level => $self->{'options'}{'comp_level'});
  190. $tar->create('chdir' => $tmp);
  191. $tar->add_directory($dirname);
  192. $tar->finish();
  193. erasedir($tmp);
  194. pop @Dpkg::Exit::handlers;
  195. $self->add_file($debianfile);
  196. }
  197. # Called after a tarball is unpacked, to check out the working copy.
  198. sub do_extract {
  199. my ($self, $newdirectory) = @_;
  200. my $fields = $self->{'fields'};
  201. my $dscdir = $self->{'basedir'};
  202. check_version($fields->{'Version'});
  203. my $basename = $self->get_basename();
  204. my $basenamerev = $self->get_basename(1);
  205. my @files = $self->get_files();
  206. if (@files > 1) {
  207. error(_g("format v3.0 uses only one source file"));
  208. }
  209. my $tarfile = $files[0];
  210. if ($tarfile !~ /^\Q$basenamerev\E\.git\.tar\.$comp_regex$/) {
  211. error(_g("expected %s, got %s"),
  212. "$basenamerev.git.tar.$comp_regex", $tarfile);
  213. }
  214. erasedir($newdirectory);
  215. # Extract main tarball
  216. info(_g("unpacking %s"), $tarfile);
  217. my $tar = Dpkg::Source::Archive->new(filename => "$dscdir$tarfile");
  218. $tar->extract($newdirectory);
  219. sanity_check($newdirectory);
  220. my $old_cwd = getcwd();
  221. chdir($newdirectory) ||
  222. syserr(_g("unable to chdir to `%s'"), $newdirectory);
  223. # Disable git hooks, as unpacking a source package should not
  224. # involve running code.
  225. foreach my $hook (glob("./.git/hooks/*")) {
  226. if (-x $hook) {
  227. warning(_g("executable bit set on %s; clearing"), $hook);
  228. chmod(0666 &~ umask(), $hook) ||
  229. syserr(_g("unable to change permission of `%s'"), $hook);
  230. }
  231. }
  232. # This is a paranoia measure, since the index is not normally
  233. # provided by possibly-untrusted third parties, remove it if
  234. # present (git will recreate it as needed).
  235. if (-e ".git/index" || -l ".git/index") {
  236. unlink(".git/index") ||
  237. syserr(_g("unable to remove `%s'"), ".git/index");
  238. }
  239. # Comment out potentially probamatic or annoying stuff in
  240. # .git/config.
  241. my $safe_fields = qr/^(
  242. core\.autocrlf |
  243. branch\..* |
  244. remote\..* |
  245. core\.repositoryformatversion |
  246. core\.filemode |
  247. core\.logallrefupdates |
  248. core\.bare
  249. )$/x;
  250. my %config = %{read_git_config(".git/config")};
  251. foreach my $field (keys %config) {
  252. if ($field =~ /$safe_fields/) {
  253. delete $config{$field};
  254. }
  255. else {
  256. system("git", "config", "--file", ".git/config",
  257. "--unset-all", $field);
  258. $? && subprocerr("git config --file .git/config --unset-all $field");
  259. }
  260. }
  261. if (%config) {
  262. warning(_g("modifying .git/config to comment out some settings"));
  263. open(GIT_CONFIG, ">>", ".git/config") ||
  264. syserr(_g("unable to append to %s"), ".git/config");
  265. print GIT_CONFIG "\n# "._g("The following setting(s) were disabled by dpkg-source").":\n";
  266. foreach my $field (sort keys %config) {
  267. foreach my $value (@{$config{$field}}) {
  268. if (defined($value)) {
  269. print GIT_CONFIG "# $field=$value\n";
  270. } else {
  271. print GIT_CONFIG "# $field\n";
  272. }
  273. }
  274. }
  275. close GIT_CONFIG;
  276. }
  277. # .git/gitweb is created and used by git instaweb and should not be
  278. # transferwed by source package.
  279. system("rm", "-rf", ".git/gitweb");
  280. $? && subprocerr("rm -rf .git/gitweb");
  281. # git checkout is used to repopulate the WC with files
  282. # and recreate the index.
  283. system("git", "checkout", "-f");
  284. $? && subprocerr("git checkout -f");
  285. chdir($old_cwd) ||
  286. syserr(_g("unable to chdir to `%s'"), $old_cwd);
  287. }
  288. 1;