dpkg (1.18.25) stretch; urgency=medium
[dpkg] / scripts / Dpkg / Source / Package / V3 / Git.pm
CommitLineData
1479465f
GJ
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
20package Dpkg::Source::Package::V3::Git;
21
22use strict;
23use warnings;
24
25our $VERSION = '0.02';
26
27use Cwd qw(abs_path getcwd);
28use File::Basename;
29use File::Temp qw(tempdir);
30
31use Dpkg::Gettext;
32use Dpkg::ErrorHandling;
33use Dpkg::Exit qw(push_exit_handler pop_exit_handler);
34use Dpkg::Source::Functions qw(erasedir);
35
36use parent qw(Dpkg::Source::Package);
37
38our $CURRENT_MINOR_VERSION = '0';
39
40# Remove variables from the environment that might cause git to do
41# something unexpected.
42delete $ENV{GIT_DIR};
43delete $ENV{GIT_INDEX_FILE};
44delete $ENV{GIT_OBJECT_DIRECTORY};
45delete $ENV{GIT_ALTERNATE_OBJECT_DIRECTORIES};
46delete $ENV{GIT_WORK_TREE};
47
48sub 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
58sub _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
74my @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
86sub describe_cmdline_options {
87 my $self = shift;
88
89 my @cmdline = ( $self->SUPER::describe_cmdline_options(), @module_cmdline );
90
91 return @cmdline;
92}
93
94sub 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
107sub 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
114sub 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
208sub 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
2601;