Commit | Line | Data |
---|---|---|
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 | ||
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; |