chiark / gitweb /
dpkg (1.18.25) stretch; urgency=medium
[dpkg] / scripts / Dpkg / Source / Package / V3 / Git.pm
1 #
2 # git support for dpkg-source
3 #
4 # Copyright © 2007,2010 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 <https://www.gnu.org/licenses/>.
19
20 package Dpkg::Source::Package::V3::Git;
21
22 use strict;
23 use warnings;
24
25 our $VERSION = '0.02';
26
27 use Cwd qw(abs_path getcwd);
28 use File::Basename;
29 use File::Temp qw(tempdir);
30
31 use Dpkg::Gettext;
32 use Dpkg::ErrorHandling;
33 use Dpkg::Exit qw(push_exit_handler pop_exit_handler);
34 use Dpkg::Source::Functions qw(erasedir);
35
36 use parent qw(Dpkg::Source::Package);
37
38 our $CURRENT_MINOR_VERSION = '0';
39
40 # Remove variables from the environment that might cause git to do
41 # something unexpected.
42 delete $ENV{GIT_DIR};
43 delete $ENV{GIT_INDEX_FILE};
44 delete $ENV{GIT_OBJECT_DIRECTORY};
45 delete $ENV{GIT_ALTERNATE_OBJECT_DIRECTORIES};
46 delete $ENV{GIT_WORK_TREE};
47
48 sub import {
49     foreach my $dir (split(/:/, $ENV{PATH})) {
50         if (-x "$dir/git") {
51             return 1;
52         }
53     }
54     error(g_('cannot unpack git-format source package because ' .
55              'git is not in the PATH'));
56 }
57
58 sub _sanity_check {
59     my $srcdir = shift;
60
61     if (! -d "$srcdir/.git") {
62         error(g_('source directory is not the top directory of a git ' .
63                  'repository (%s/.git not present), but Format git was ' .
64                  'specified'), $srcdir);
65     }
66     if (-s "$srcdir/.gitmodules") {
67         error(g_('git repository %s uses submodules; this is not yet supported'),
68               $srcdir);
69     }
70
71     return 1;
72 }
73
74 my @module_cmdline = (
75     {
76         name => '--git-ref=<ref>',
77         help => N_('specify a git <ref> to include in the git bundle'),
78         when => 'build',
79     }, {
80         name => '--git-depth=<number>',
81         help => N_('create a shallow clone with <number> depth'),
82         when => 'build',
83     }
84 );
85
86 sub describe_cmdline_options {
87     my $self = shift;
88
89     my @cmdline = ( $self->SUPER::describe_cmdline_options(), @module_cmdline );
90
91     return @cmdline;
92 }
93
94 sub parse_cmdline_option {
95     my ($self, $opt) = @_;
96     return 1 if $self->SUPER::parse_cmdline_option($opt);
97     if ($opt =~ /^--git-ref=(.*)$/) {
98         push @{$self->{options}{git_ref}}, $1;
99         return 1;
100     } elsif ($opt =~ /^--git-depth=(\d+)$/) {
101         $self->{options}{git_depth} = $1;
102         return 1;
103     }
104     return 0;
105 }
106
107 sub can_build {
108     my ($self, $dir) = @_;
109
110     return (0, g_("doesn't contain a git repository")) unless -d "$dir/.git";
111     return 1;
112 }
113
114 sub do_build {
115     my ($self, $dir) = @_;
116     my $diff_ignore_regex = $self->{options}{diff_ignore_regex};
117
118     $dir =~ s{/+$}{}; # Strip trailing /
119     my ($dirname, $updir) = fileparse($dir);
120     my $basenamerev = $self->get_basename(1);
121
122     _sanity_check($dir);
123
124     my $old_cwd = getcwd();
125     chdir $dir or syserr(g_("unable to chdir to '%s'"), $dir);
126
127     # Check for uncommitted files.
128     # To support dpkg-source -i, get a list of files
129     # equivalent to the ones git status finds, and remove any
130     # ignored files from it.
131     my @ignores = '--exclude-per-directory=.gitignore';
132     my $core_excludesfile = qx(git config --get core.excludesfile);
133     chomp $core_excludesfile;
134     if (length $core_excludesfile && -e $core_excludesfile) {
135         push @ignores, "--exclude-from=$core_excludesfile";
136     }
137     if (-e '.git/info/exclude') {
138         push @ignores, '--exclude-from=.git/info/exclude';
139     }
140     open(my $git_ls_files_fh, '-|', 'git', 'ls-files', '--modified', '--deleted',
141          '-z', '--others', @ignores) or subprocerr('git ls-files');
142     my @files;
143     {
144       local $_;
145       local $/ = "\0";
146       while (<$git_ls_files_fh>) {
147           chomp;
148           if (! length $diff_ignore_regex ||
149               ! m/$diff_ignore_regex/o) {
150               push @files, $_;
151           }
152       }
153     }
154     close($git_ls_files_fh) or syserr(g_('git ls-files exited nonzero'));
155     if (@files) {
156         error(g_('uncommitted, not-ignored changes in working directory: %s'),
157               join(' ', @files));
158     }
159
160     # If a depth was specified, need to create a shallow clone and
161     # bundle that.
162     my $tmp;
163     my $shallowfile;
164     if ($self->{options}{git_depth}) {
165         chdir $old_cwd or syserr(g_("unable to chdir to '%s'"), $old_cwd);
166         $tmp = tempdir("$dirname.git.XXXXXX", DIR => $updir);
167         push_exit_handler(sub { erasedir($tmp) });
168         my $clone_dir = "$tmp/repo.git";
169         # file:// is needed to avoid local cloning, which does not
170         # create a shallow clone.
171         info(g_('creating shallow clone with depth %s'),
172                 $self->{options}{git_depth});
173         system('git', 'clone', '--depth=' . $self->{options}{git_depth},
174                '--quiet', '--bare', 'file://' . abs_path($dir), $clone_dir);
175         subprocerr('git clone') if $?;
176         chdir($clone_dir)
177             or syserr(g_("unable to chdir to '%s'"), $clone_dir);
178         $shallowfile = "$basenamerev.gitshallow";
179         system('cp', '-f', 'shallow', "$old_cwd/$shallowfile");
180         subprocerr('cp shallow') if $?;
181     }
182
183     # Create the git bundle.
184     my $bundlefile = "$basenamerev.git";
185     my @bundle_arg=$self->{options}{git_ref} ?
186         (@{$self->{options}{git_ref}}) : '--all';
187     info(g_('bundling: %s'), join(' ', @bundle_arg));
188     system('git', 'bundle', 'create', "$old_cwd/$bundlefile",
189            @bundle_arg,
190            'HEAD', # ensure HEAD is included no matter what
191            '--', # avoids ambiguity error when referring to eg, a debian branch
192     );
193     subprocerr('git bundle') if $?;
194
195     chdir $old_cwd or syserr(g_("unable to chdir to '%s'"), $old_cwd);
196
197     if (defined $tmp) {
198         erasedir($tmp);
199         pop_exit_handler();
200     }
201
202     $self->add_file($bundlefile);
203     if (defined $shallowfile) {
204         $self->add_file($shallowfile);
205     }
206 }
207
208 sub do_extract {
209     my ($self, $newdirectory) = @_;
210     my $fields = $self->{fields};
211
212     my $dscdir = $self->{basedir};
213     my $basenamerev = $self->get_basename(1);
214
215     my @files = $self->get_files();
216     my ($bundle, $shallow);
217     foreach my $file (@files) {
218         if ($file =~ /^\Q$basenamerev\E\.git$/) {
219             if (! defined $bundle) {
220                 $bundle = $file;
221             } else {
222                 error(g_('format v3.0 (git) uses only one .git file'));
223             }
224         } elsif ($file =~ /^\Q$basenamerev\E\.gitshallow$/) {
225             if (! defined $shallow) {
226                 $shallow = $file;
227             } else {
228                 error(g_('format v3.0 (git) uses only one .gitshallow file'));
229             }
230         } else {
231             error(g_('format v3.0 (git) unknown file: %s', $file));
232         }
233     }
234     if (! defined $bundle) {
235         error(g_('format v3.0 (git) expected %s'), "$basenamerev.git");
236     }
237
238     if ($self->{options}{no_overwrite_dir} and -e $newdirectory) {
239         error(g_('unpack target exists: %s'), $newdirectory);
240     } else {
241         erasedir($newdirectory);
242     }
243
244     # Extract git bundle.
245     info(g_('cloning %s'), $bundle);
246     system('git', 'clone', '--quiet', $dscdir . $bundle, $newdirectory);
247     subprocerr('git bundle') if $?;
248
249     if (defined $shallow) {
250         # Move shallow info file into place, so git does not
251         # try to follow parents of shallow refs.
252         info(g_('setting up shallow clone'));
253         system('cp', '-f',  $dscdir . $shallow, "$newdirectory/.git/shallow");
254         subprocerr('cp') if $?;
255     }
256
257     _sanity_check($newdirectory);
258 }
259
260 1;