git.pm 10 KB

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