]> git.ipfire.org Git - thirdparty/git.git/blame - git-archimport.perl
fix same sized delta logic
[thirdparty/git.git] / git-archimport.perl
CommitLineData
d3968363
ML
1#!/usr/bin/perl -w
2#
3# This tool is copyright (c) 2005, Martin Langhoff.
4# It is released under the Gnu Public License, version 2.
5#
a6080a0a
JH
6# The basic idea is to walk the output of tla abrowse,
7# fetch the changesets and apply them.
d3968363 8#
241b5967 9
d3968363
ML
10=head1 Invocation
11
a6080a0a
JH
12 git-archimport [ -h ] [ -v ] [ -o ] [ -a ] [ -f ] [ -T ]
13 [ -D depth] [ -t tempdir ] <archive>/<branch> [ <archive>/<branch> ]
d3968363 14
241b5967
ML
15Imports a project from one or more Arch repositories. It will follow branches
16and repositories within the namespaces defined by the <archive/branch>
82e5a82f 17parameters supplied. If it cannot find the remote branch a merge comes from
a6080a0a 18it will just import it as a regular commit. If it can find it, it will mark it
241b5967 19as a merge whenever possible.
d3968363 20
241b5967 21See man (1) git-archimport for more details.
d3968363 22
241b5967 23=head1 TODO
d3968363 24
241b5967 25 - create tag objects instead of ref tags
d3968363 26 - audit shell-escaping of filenames
241b5967 27 - hide our private tags somewhere smarter
a6080a0a 28 - find a way to make "cat *patches | patch" safe even when patchfiles are missing newlines
3e525e67
EW
29 - sort and apply patches by graphing ancestry relations instead of just
30 relying in dates supplied in the changeset itself.
31 tla ancestry-graph -m could be helpful here...
d3968363
ML
32
33=head1 Devel tricks
34
a6080a0a 35Add print in front of the shell commands invoked via backticks.
d3968363 36
22ff00fc
EW
37=head1 Devel Notes
38
39There are several places where Arch and git terminology are intermixed
40and potentially confused.
41
42The notion of a "branch" in git is approximately equivalent to
43a "archive/category--branch--version" in Arch. Also, it should be noted
44that the "--branch" portion of "archive/category--branch--version" is really
45optional in Arch although not many people (nor tools!) seem to know this.
46This means that "archive/category--version" is also a valid "branch"
47in git terms.
48
49We always refer to Arch names by their fully qualified variant (which
50means the "archive" name is prefixed.
51
52For people unfamiliar with Arch, an "archive" is the term for "repository",
53and can contain multiple, unrelated branches.
54
d3968363
ML
55=cut
56
57use strict;
58use warnings;
59use Getopt::Std;
42f44b08 60use File::Temp qw(tempdir);
f88961a8 61use File::Path qw(mkpath rmtree);
d3968363 62use File::Basename qw(basename dirname);
d3968363
ML
63use Data::Dumper qw/ Dumper /;
64use IPC::Open2;
65
66$SIG{'PIPE'}="IGNORE";
67$ENV{'TZ'}="UTC";
68
1d4710d0
ML
69my $git_dir = $ENV{"GIT_DIR"} || ".git";
70$ENV{"GIT_DIR"} = $git_dir;
a7fb51d3 71my $ptag_dir = "$git_dir/archimport/tags";
1d4710d0 72
3e525e67 73our($opt_h,$opt_f,$opt_v,$opt_T,$opt_t,$opt_D,$opt_a,$opt_o);
d3968363
ML
74
75sub usage() {
76 print STDERR <<END;
77Usage: ${\basename $0} # fetch/update GIT from Arch
42f4570c 78 [ -h ] [ -v ] [ -o ] [ -a ] [ -f ] [ -T ] [ -D depth ] [ -t tempdir ]
d3968363
ML
79 repository/arch-branch [ repository/arch-branch] ...
80END
81 exit(1);
82}
83
3e525e67 84getopts("fThvat:D:") or usage();
d3968363
ML
85usage if $opt_h;
86
87@ARGV >= 1 or usage();
42f44b08
EW
88# $arch_branches:
89# values associated with keys:
90# =1 - Arch version / git 'branch' detected via abrowse on a limit
82e5a82f 91# >1 - Arch version / git 'branch' of an auxiliary branch we've merged
d9cb5399
PB
92my %arch_branches = map { my $branch = $_; $branch =~ s/:[^:]*$//; $branch => 1 } @ARGV;
93
94# $branch_name_map:
95# maps arch branches to git branch names
96my %branch_name_map = map { m/^(.*):([^:]*)$/; $1 => $2 } grep { m/:/ } @ARGV;
d3968363 97
5744f277
EW
98$ENV{'TMPDIR'} = $opt_t if $opt_t; # $ENV{TMPDIR} will affect tempdir() calls:
99my $tmp = tempdir('git-archimport-XXXXXX', TMPDIR => 1, CLEANUP => 1);
127bf00f 100$opt_v && print "+ Using $tmp as temporary directory\n";
d3968363 101
11dbe9e8
GP
102unless (-d $git_dir) { # initial import needs empty directory
103 opendir DIR, '.' or die "Unable to open current directory: $!\n";
104 while (my $entry = readdir DIR) {
105 $entry =~ /^\.\.?$/ or
106 die "Initial import needs an empty current working directory.\n"
107 }
108 closedir DIR
109}
110
d9cb5399 111my $default_archive; # default Arch archive
42f44b08
EW
112my %reachable = (); # Arch repositories we can access
113my %unreachable = (); # Arch repositories we can't access :<
d3968363 114my @psets = (); # the collection
b779d5f0 115my %psets = (); # the collection, by name
3e525e67
EW
116my %stats = ( # Track which strategy we used to import:
117 get_tag => 0, replay => 0, get_new => 0, get_delta => 0,
118 simple_changeset => 0, import_or_tag => 0
119);
b779d5f0
ML
120
121my %rptags = (); # my reverse private tags
122 # to map a SHA1 to a commitid
2777ef76 123my $TLA = $ENV{'ARCH_CLIENT'} || 'tla';
d3968363 124
42f44b08
EW
125sub do_abrowse {
126 my $stage = shift;
127 while (my ($limit, $level) = each %arch_branches) {
128 next unless $level == $stage;
a6080a0a
JH
129
130 open ABROWSE, "$TLA abrowse -fkD --merges $limit |"
42f44b08 131 or die "Problems with tla abrowse: $!";
a6080a0a 132
42f44b08
EW
133 my %ps = (); # the current one
134 my $lastseen = '';
a6080a0a 135
42f44b08
EW
136 while (<ABROWSE>) {
137 chomp;
a6080a0a 138
42f44b08
EW
139 # first record padded w 8 spaces
140 if (s/^\s{8}\b//) {
141 my ($id, $type) = split(m/\s+/, $_, 2);
142
143 my %last_ps;
144 # store the record we just captured
145 if (%ps && !exists $psets{ $ps{id} }) {
146 %last_ps = %ps; # break references
147 push (@psets, \%last_ps);
148 $psets{ $last_ps{id} } = \%last_ps;
149 }
a6080a0a 150
42f44b08
EW
151 my $branch = extract_versionname($id);
152 %ps = ( id => $id, branch => $branch );
153 if (%last_ps && ($last_ps{branch} eq $branch)) {
154 $ps{parent_id} = $last_ps{id};
155 }
a6080a0a 156
42f44b08
EW
157 $arch_branches{$branch} = 1;
158 $lastseen = 'id';
159
160 # deal with types (should work with baz or tla):
161 if ($type =~ m/\(.*changeset\)/) {
162 $ps{type} = 's';
163 } elsif ($type =~ /\(.*import\)/) {
164 $ps{type} = 'i';
6df896b5 165 } elsif ($type =~ m/\(tag.*?(\S+\@\S+).*?\)/) {
42f44b08
EW
166 $ps{type} = 't';
167 # read which revision we've tagged when we parse the log
6df896b5 168 $ps{tag} = $1;
a6080a0a 169 } else {
42f44b08
EW
170 warn "Unknown type $type";
171 }
172
173 $arch_branches{$branch} = 1;
174 $lastseen = 'id';
a6080a0a
JH
175 } elsif (s/^\s{10}//) {
176 # 10 leading spaces or more
42f44b08 177 # indicate commit metadata
a6080a0a 178
42f44b08
EW
179 # date
180 if ($lastseen eq 'id' && m/^(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d)/){
181 $ps{date} = $1;
182 $lastseen = 'date';
183 } elsif ($_ eq 'merges in:') {
184 $ps{merges} = [];
185 $lastseen = 'merges';
186 } elsif ($lastseen eq 'merges' && s/^\s{2}//) {
187 my $id = $_;
188 push (@{$ps{merges}}, $id);
a6080a0a 189
42f44b08
EW
190 # aggressive branch finding:
191 if ($opt_D) {
192 my $branch = extract_versionname($id);
193 my $repo = extract_reponame($branch);
a6080a0a 194
42f44b08
EW
195 if (archive_reachable($repo) &&
196 !defined $arch_branches{$branch}) {
197 $arch_branches{$branch} = $stage + 1;
198 }
199 }
200 } else {
201 warn "more metadata after merges!?: $_\n" unless /^\s*$/;
d3968363 202 }
d3968363 203 }
d3968363 204 }
d3968363 205
42f44b08
EW
206 if (%ps && !exists $psets{ $ps{id} }) {
207 my %temp = %ps; # break references
208 if (@psets && $psets[$#psets]{branch} eq $ps{branch}) {
209 $temp{parent_id} = $psets[$#psets]{id};
210 }
a6080a0a 211 push (@psets, \%temp);
42f44b08 212 $psets{ $temp{id} } = \%temp;
a6080a0a
JH
213 }
214
42f44b08
EW
215 close ABROWSE or die "$TLA abrowse failed on $limit\n";
216 }
d3968363
ML
217} # end foreach $root
218
42f44b08
EW
219do_abrowse(1);
220my $depth = 2;
221$opt_D ||= 0;
222while ($depth <= $opt_D) {
223 do_abrowse($depth);
224 $depth++;
225}
226
d3968363 227## Order patches by time
42f44b08
EW
228# FIXME see if we can find a more optimal way to do this by graphing
229# the ancestry data and walking it, that way we won't have to rely on
230# client-supplied dates
d3968363
ML
231@psets = sort {$a->{date}.$b->{id} cmp $b->{date}.$b->{id}} @psets;
232
233#print Dumper \@psets;
234
235##
236## TODO cleanup irrelevant patches
237## and put an initial import
238## or a full tag
3292ae47 239my $import = 0;
1d4710d0 240unless (-d $git_dir) { # initial import
d3968363
ML
241 if ($psets[0]{type} eq 'i' || $psets[0]{type} eq 't') {
242 print "Starting import from $psets[0]{id}\n";
5c94f87e 243 `git-init`;
3292ae47
ML
244 die $! if $?;
245 $import = 1;
d3968363
ML
246 } else {
247 die "Need to start from an import or a tag -- cannot use $psets[0]{id}";
248 }
b779d5f0
ML
249} else { # progressing an import
250 # load the rptags
42f44b08 251 opendir(DIR, $ptag_dir)
b779d5f0
ML
252 || die "can't opendir: $!";
253 while (my $file = readdir(DIR)) {
a7fb51d3
EW
254 # skip non-interesting-files
255 next unless -f "$ptag_dir/$file";
a6080a0a 256
a7fb51d3
EW
257 # convert first '--' to '/' from old git-archimport to use
258 # as an archivename/c--b--v private tag
259 if ($file !~ m!,!) {
260 my $oldfile = $file;
261 $file =~ s!--!,!;
262 print STDERR "converting old tag $oldfile to $file\n";
263 rename("$ptag_dir/$oldfile", "$ptag_dir/$file") or die $!;
264 }
b779d5f0
ML
265 my $sha = ptag($file);
266 chomp $sha;
b779d5f0
ML
267 $rptags{$sha} = $file;
268 }
269 closedir DIR;
d3968363
ML
270}
271
3292ae47 272# process patchsets
22ff00fc
EW
273# extract the Arch repository name (Arch "archive" in Arch-speak)
274sub extract_reponame {
275 my $fq_cvbr = shift; # archivename/[[[[category]branch]version]revision]
276 return (split(/\//, $fq_cvbr))[0];
277}
a6080a0a 278
22ff00fc
EW
279sub extract_versionname {
280 my $name = shift;
281 $name =~ s/--(?:patch|version(?:fix)?|base)-\d+$//;
282 return $name;
283}
d3968363 284
22ff00fc 285# convert a fully-qualified revision or version to a unique dirname:
a6080a0a 286# normalperson@yhbt.net-05/mpd--uclinux--1--patch-2
22ff00fc
EW
287# becomes: normalperson@yhbt.net-05,mpd--uclinux--1
288#
289# the git notion of a branch is closer to
290# archive/category--branch--version than archive/category--branch, so we
291# use this to convert to git branch names.
292# Also, keep archive names but replace '/' with ',' since it won't require
293# subdirectories, and is safer than swapping '--' which could confuse
294# reverse-mapping when dealing with bastard branches that
295# are just archive/category--version (no --branch)
296sub tree_dirname {
297 my $revision = shift;
298 my $name = extract_versionname($revision);
299 $name =~ s#/#,#;
300 return $name;
301}
302
fee3365f
ML
303# old versions of git-archimport just use the <category--branch> part:
304sub old_style_branchname {
305 my $id = shift;
306 my $ret = safe_pipe_capture($TLA,'parse-package-name','-p',$id);
307 chomp $ret;
308 return $ret;
309}
310
d9cb5399
PB
311*git_default_branchname = $opt_o ? *old_style_branchname : *tree_dirname;
312
313# retrieve default archive, since $branch_name_map keys might not include it
314sub get_default_archive {
315 if (!defined $default_archive) {
316 $default_archive = safe_pipe_capture($TLA,'my-default-archive');
317 chomp $default_archive;
318 }
319 return $default_archive;
320}
321
322sub git_branchname {
323 my $revision = shift;
324 my $name = extract_versionname($revision);
325
326 if (exists $branch_name_map{$name}) {
327 return $branch_name_map{$name};
328
329 } elsif ($name =~ m#^([^/]*)/(.*)$#
330 && $1 eq get_default_archive()
331 && exists $branch_name_map{$2}) {
332 # the names given in the command-line lacked the archive.
333 return $branch_name_map{$2};
334
335 } else {
336 return git_default_branchname($revision);
337 }
338}
22ff00fc 339
3e525e67
EW
340sub process_patchset_accurate {
341 my $ps = shift;
a6080a0a 342
3e525e67
EW
343 # switch to that branch if we're not already in that branch:
344 if (-e "$git_dir/refs/heads/$ps->{branch}") {
345 system('git-checkout','-f',$ps->{branch}) == 0 or die "$! $?\n";
d3968363 346
3e525e67
EW
347 # remove any old stuff that got leftover:
348 my $rm = safe_pipe_capture('git-ls-files','--others','-z');
349 rmtree(split(/\0/,$rm)) if $rm;
3292ae47 350 }
a6080a0a 351
3e525e67
EW
352 # Apply the import/changeset/merge into the working tree
353 my $dir = sync_to_ps($ps);
354 # read the new log entry:
355 my @commitlog = safe_pipe_capture($TLA,'cat-log','-d',$dir,$ps->{id});
356 die "Error in cat-log: $!" if $?;
357 chomp @commitlog;
358
359 # grab variables we want from the log, new fields get added to $ps:
360 # (author, date, email, summary, message body ...)
361 parselog($ps, \@commitlog);
362
363 if ($ps->{id} =~ /--base-0$/ && $ps->{id} ne $psets[0]{id}) {
a6080a0a 364 # this should work when importing continuations
3e525e67 365 if ($ps->{tag} && (my $branchpoint = eval { ptag($ps->{tag}) })) {
a6080a0a 366
3e525e67 367 # find where we are supposed to branch from
d9cb5399
PB
368 if (! -e "$git_dir/refs/heads/$ps->{branch}") {
369 system('git-branch',$ps->{branch},$branchpoint) == 0 or die "$! $?\n";
370
371 # We trust Arch with the fact that this is just a tag,
372 # and it does not affect the state of the tree, so
373 # we just tag and move on. If the user really wants us
374 # to consolidate more branches into one, don't tag because
375 # the tag name would be already taken.
376 tag($ps->{id}, $branchpoint);
377 ptag($ps->{id}, $branchpoint);
378 print " * Tagged $ps->{id} at $branchpoint\n";
379 }
380 system('git-checkout','-f',$ps->{branch}) == 0 or die "$! $?\n";
381
3e525e67
EW
382 # remove any old stuff that got leftover:
383 my $rm = safe_pipe_capture('git-ls-files','--others','-z');
384 rmtree(split(/\0/,$rm)) if $rm;
3e525e67
EW
385 return 0;
386 } else {
387 warn "Tagging from unknown id unsupported\n" if $ps->{tag};
388 }
389 # allow multiple bases/imports here since Arch supports cherry-picks
390 # from unrelated trees
a6080a0a
JH
391 }
392
3e525e67 393 # update the index with all the changes we got
3ff903bf
EW
394 system('git-diff-files --name-only -z | '.
395 'git-update-index --remove -z --stdin') == 0 or die "$! $?\n";
3e525e67
EW
396 system('git-ls-files --others -z | '.
397 'git-update-index --add -z --stdin') == 0 or die "$! $?\n";
3e525e67
EW
398 return 1;
399}
37f15d50 400
3e525e67
EW
401# the native changeset processing strategy. This is very fast, but
402# does not handle permissions or any renames involving directories
403sub process_patchset_fast {
404 my $ps = shift;
a6080a0a 405 #
d3968363
ML
406 # create the branch if needed
407 #
3292ae47
ML
408 if ($ps->{type} eq 'i' && !$import) {
409 die "Should not have more than one 'Initial import' per GIT import: $ps->{id}";
d3968363
ML
410 }
411
3292ae47 412 unless ($import) { # skip for import
1d4710d0 413 if ( -e "$git_dir/refs/heads/$ps->{branch}") {
d3968363 414 # we know about this branch
f88961a8 415 system('git-checkout',$ps->{branch});
d3968363
ML
416 } else {
417 # new branch! we need to verify a few things
418 die "Branch on a non-tag!" unless $ps->{type} eq 't';
419 my $branchpoint = ptag($ps->{tag});
a6080a0a 420 die "Tagging from unknown id unsupported: $ps->{tag}"
d3968363 421 unless $branchpoint;
a6080a0a 422
d3968363 423 # find where we are supposed to branch from
d9cb5399
PB
424 if (! -e "$git_dir/refs/heads/$ps->{branch}") {
425 system('git-branch',$ps->{branch},$branchpoint) == 0 or die "$! $?\n";
426
427 # We trust Arch with the fact that this is just a tag,
428 # and it does not affect the state of the tree, so
429 # we just tag and move on. If the user really wants us
430 # to consolidate more branches into one, don't tag because
431 # the tag name would be already taken.
432 tag($ps->{id}, $branchpoint);
433 ptag($ps->{id}, $branchpoint);
434 print " * Tagged $ps->{id} at $branchpoint\n";
435 }
436 system('git-checkout',$ps->{branch}) == 0 or die "$! $?\n";
3e525e67 437 return 0;
a6080a0a 438 }
d3968363 439 die $! if $?;
a6080a0a 440 }
d3968363 441
d3968363
ML
442 #
443 # Apply the import/changeset/merge into the working tree
a6080a0a 444 #
d3968363 445 if ($ps->{type} eq 'i' || $ps->{type} eq 't') {
d3968363 446 apply_import($ps) or die $!;
3e525e67 447 $stats{import_or_tag}++;
3292ae47 448 $import=0;
d3968363
ML
449 } elsif ($ps->{type} eq 's') {
450 apply_cset($ps);
3e525e67 451 $stats{simple_changeset}++;
d3968363
ML
452 }
453
454 #
455 # prepare update git's index, based on what arch knows
456 # about the pset, resolve parents, etc
457 #
a6080a0a
JH
458
459 my @commitlog = safe_pipe_capture($TLA,'cat-archive-log',$ps->{id});
d3968363 460 die "Error in cat-archive-log: $!" if $?;
a6080a0a 461
6df896b5 462 parselog($ps,\@commitlog);
d3968363
ML
463
464 # imports don't give us good info
465 # on added files. Shame on them
6df896b5 466 if ($ps->{type} eq 'i' || $ps->{type} eq 't') {
6df896b5
EW
467 system('git-ls-files --deleted -z | '.
468 'git-update-index --remove -z --stdin') == 0 or die "$! $?\n";
3ff903bf
EW
469 system('git-ls-files --others -z | '.
470 'git-update-index --add -z --stdin') == 0 or die "$! $?\n";
d3968363
ML
471 }
472
6df896b5 473 # TODO: handle removed_directories and renamed_directories:
3ff903bf 474
6df896b5
EW
475 if (my $del = $ps->{removed_files}) {
476 unlink @$del;
d3968363
ML
477 while (@$del) {
478 my @slice = splice(@$del, 0, 100);
6df896b5
EW
479 system('git-update-index','--remove','--',@slice) == 0 or
480 die "Error in git-update-index --remove: $! $?\n";
d3968363
ML
481 }
482 }
6df896b5
EW
483
484 if (my $ren = $ps->{renamed_files}) { # renamed
d3968363
ML
485 if (@$ren % 2) {
486 die "Odd number of entries in rename!?";
487 }
a6080a0a 488
d3968363 489 while (@$ren) {
6df896b5 490 my $from = shift @$ren;
a6080a0a 491 my $to = shift @$ren;
d3968363
ML
492
493 unless (-d dirname($to)) {
494 mkpath(dirname($to)); # will die on err
495 }
3e525e67 496 # print "moving $from $to";
6df896b5
EW
497 rename($from, $to) or die "Error renaming '$from' '$to': $!\n";
498 system('git-update-index','--remove','--',$from) == 0 or
499 die "Error in git-update-index --remove: $! $?\n";
500 system('git-update-index','--add','--',$to) == 0 or
501 die "Error in git-update-index --add: $! $?\n";
d3968363 502 }
d3968363 503 }
6df896b5 504
3ff903bf
EW
505 if (my $add = $ps->{new_files}) {
506 while (@$add) {
507 my @slice = splice(@$add, 0, 100);
508 system('git-update-index','--add','--',@slice) == 0 or
509 die "Error in git-update-index --add: $! $?\n";
510 }
511 }
512
6df896b5 513 if (my $mod = $ps->{modified_files}) {
d3968363
ML
514 while (@$mod) {
515 my @slice = splice(@$mod, 0, 100);
6df896b5
EW
516 system('git-update-index','--',@slice) == 0 or
517 die "Error in git-update-index: $! $?\n";
d3968363
ML
518 }
519 }
3e525e67
EW
520 return 1; # we successfully applied the changeset
521}
522
523if ($opt_f) {
524 print "Will import patchsets using the fast strategy\n",
525 "Renamed directories and permission changes will be missed\n";
526 *process_patchset = *process_patchset_fast;
527} else {
528 print "Using the default (accurate) import strategy.\n",
529 "Things may be a bit slow\n";
530 *process_patchset = *process_patchset_accurate;
531}
a6080a0a 532
3e525e67
EW
533foreach my $ps (@psets) {
534 # process patchsets
535 $ps->{branch} = git_branchname($ps->{id});
536
537 #
a6080a0a
JH
538 # ensure we have a clean state
539 #
3e525e67
EW
540 if (my $dirty = `git-diff-files`) {
541 die "Unclean tree when about to process $ps->{id} " .
542 " - did we fail to commit cleanly before?\n$dirty";
543 }
544 die $! if $?;
a6080a0a 545
3e525e67
EW
546 #
547 # skip commits already in repo
548 #
549 if (ptag($ps->{id})) {
550 $opt_v && print " * Skipping already imported: $ps->{id}\n";
10945e00 551 next;
3e525e67
EW
552 }
553
554 print " * Starting to work on $ps->{id}\n";
555
556 process_patchset($ps) or next;
557
215a7ad1 558 # warn "errors when running git-update-index! $!";
3e525e67 559 my $tree = `git-write-tree`;
d3968363
ML
560 die "cannot write tree $!" if $?;
561 chomp $tree;
a6080a0a 562
d3968363
ML
563 #
564 # Who's your daddy?
565 #
566 my @par;
1d4710d0 567 if ( -e "$git_dir/refs/heads/$ps->{branch}") {
f88961a8 568 if (open HEAD, "<","$git_dir/refs/heads/$ps->{branch}") {
d3968363
ML
569 my $p = <HEAD>;
570 close HEAD;
571 chomp $p;
572 push @par, '-p', $p;
a6080a0a 573 } else {
d3968363
ML
574 if ($ps->{type} eq 's') {
575 warn "Could not find the right head for the branch $ps->{branch}";
576 }
577 }
578 }
a6080a0a 579
b779d5f0
ML
580 if ($ps->{merges}) {
581 push @par, find_parents($ps);
582 }
d3968363 583
a6080a0a 584 #
d3968363
ML
585 # Commit, tag and clean state
586 #
587 $ENV{TZ} = 'GMT';
588 $ENV{GIT_AUTHOR_NAME} = $ps->{author};
589 $ENV{GIT_AUTHOR_EMAIL} = $ps->{email};
590 $ENV{GIT_AUTHOR_DATE} = $ps->{date};
591 $ENV{GIT_COMMITTER_NAME} = $ps->{author};
592 $ENV{GIT_COMMITTER_EMAIL} = $ps->{email};
593 $ENV{GIT_COMMITTER_DATE} = $ps->{date};
594
a6080a0a 595 my $pid = open2(*READER, *WRITER,'git-commit-tree',$tree,@par)
d3968363 596 or die $!;
a94f457e 597 print WRITER $ps->{summary},"\n\n";
6df896b5 598 print WRITER $ps->{message},"\n";
a6080a0a 599
6df896b5
EW
600 # make it easy to backtrack and figure out which Arch revision this was:
601 print WRITER 'git-archimport-id: ',$ps->{id},"\n";
a6080a0a 602
d3968363
ML
603 close WRITER;
604 my $commitid = <READER>; # read
605 chomp $commitid;
606 close READER;
607 waitpid $pid,0; # close;
608
609 if (length $commitid != 40) {
610 die "Something went wrong with the commit! $! $commitid";
611 }
612 #
613 # Update the branch
a6080a0a 614 #
f88961a8 615 open HEAD, ">","$git_dir/refs/heads/$ps->{branch}";
d3968363
ML
616 print HEAD $commitid;
617 close HEAD;
8366a10a 618 system('git-update-ref', 'HEAD', "$ps->{branch}");
d3968363
ML
619
620 # tag accordingly
621 ptag($ps->{id}, $commitid); # private tag
622 if ($opt_T || $ps->{type} eq 't' || $ps->{type} eq 'i') {
623 tag($ps->{id}, $commitid);
624 }
625 print " * Committed $ps->{id}\n";
626 print " + tree $tree\n";
627 print " + commit $commitid\n";
b779d5f0 628 $opt_v && print " + commit date is $ps->{date} \n";
f88961a8 629 $opt_v && print " + parents: ",join(' ',@par),"\n";
3e525e67
EW
630}
631
632if ($opt_v) {
633 foreach (sort keys %stats) {
634 print" $_: $stats{$_}\n";
635 }
636}
637exit 0;
638
639# used by the accurate strategy:
640sub sync_to_ps {
641 my $ps = shift;
642 my $tree_dir = $tmp.'/'.tree_dirname($ps->{id});
a6080a0a 643
3e525e67
EW
644 $opt_v && print "sync_to_ps($ps->{id}) method: ";
645
646 if (-d $tree_dir) {
647 if ($ps->{type} eq 't') {
648 $opt_v && print "get (tag)\n";
649 # looks like a tag-only or (worse,) a mixed tags/changeset branch,
650 # can't rely on replay to work correctly on these
651 rmtree($tree_dir);
652 safe_pipe_capture($TLA,'get','--no-pristine',$ps->{id},$tree_dir);
653 $stats{get_tag}++;
654 } else {
655 my $tree_id = arch_tree_id($tree_dir);
656 if ($ps->{parent_id} && ($ps->{parent_id} eq $tree_id)) {
657 # the common case (hopefully)
658 $opt_v && print "replay\n";
659 safe_pipe_capture($TLA,'replay','-d',$tree_dir,$ps->{id});
660 $stats{replay}++;
661 } else {
662 # getting one tree is usually faster than getting two trees
663 # and applying the delta ...
664 rmtree($tree_dir);
665 $opt_v && print "apply-delta\n";
666 safe_pipe_capture($TLA,'get','--no-pristine',
667 $ps->{id},$tree_dir);
668 $stats{get_delta}++;
669 }
670 }
671 } else {
672 # new branch work
673 $opt_v && print "get (new tree)\n";
674 safe_pipe_capture($TLA,'get','--no-pristine',$ps->{id},$tree_dir);
675 $stats{get_new}++;
676 }
a6080a0a 677
3e525e67
EW
678 # added -I flag to rsync since we're going to fast! AIEEEEE!!!!
679 system('rsync','-aI','--delete','--exclude',$git_dir,
680# '--exclude','.arch-inventory',
681 '--exclude','.arch-ids','--exclude','{arch}',
682 '--exclude','+*','--exclude',',*',
683 "$tree_dir/",'./') == 0 or die "Cannot rsync $tree_dir: $! $?";
684 return $tree_dir;
d3968363
ML
685}
686
d3968363
ML
687sub apply_import {
688 my $ps = shift;
22ff00fc 689 my $bname = git_branchname($ps->{id});
d3968363 690
f88961a8 691 mkpath($tmp);
d3968363 692
f88961a8 693 safe_pipe_capture($TLA,'get','-s','--no-pristine',$ps->{id},"$tmp/import");
a6080a0a 694 die "Cannot get import: $!" if $?;
f88961a8
EW
695 system('rsync','-aI','--delete', '--exclude',$git_dir,
696 '--exclude','.arch-ids','--exclude','{arch}',
697 "$tmp/import/", './');
d3968363 698 die "Cannot rsync import:$!" if $?;
a6080a0a 699
f88961a8 700 rmtree("$tmp/import");
d3968363 701 die "Cannot remove tempdir: $!" if $?;
a6080a0a 702
d3968363
ML
703
704 return 1;
705}
706
707sub apply_cset {
708 my $ps = shift;
709
f88961a8 710 mkpath($tmp);
d3968363
ML
711
712 # get the changeset
f88961a8 713 safe_pipe_capture($TLA,'get-changeset',$ps->{id},"$tmp/changeset");
d3968363 714 die "Cannot get changeset: $!" if $?;
a6080a0a 715
d3968363
ML
716 # apply patches
717 if (`find $tmp/changeset/patches -type f -name '*.patch'`) {
718 # this can be sped up considerably by doing
719 # (find | xargs cat) | patch
82e5a82f 720 # but that can get mucked up by patches
a6080a0a 721 # with missing trailing newlines or the standard
d3968363
ML
722 # 'missing newline' flag in the patch - possibly
723 # produced with an old/buggy diff.
724 # slow and safe, we invoke patch once per patchfile
725 `find $tmp/changeset/patches -type f -name '*.patch' -print0 | grep -zv '{arch}' | xargs -iFILE -0 --no-run-if-empty patch -p1 --forward -iFILE`;
726 die "Problem applying patches! $!" if $?;
727 }
728
729 # apply changed binary files
730 if (my @modified = `find $tmp/changeset/patches -type f -name '*.modified'`) {
731 foreach my $mod (@modified) {
732 chomp $mod;
733 my $orig = $mod;
734 $orig =~ s/\.modified$//; # lazy
735 $orig =~ s!^\Q$tmp\E/changeset/patches/!!;
736 #print "rsync -p '$mod' '$orig'";
f88961a8 737 system('rsync','-p',$mod,"./$orig");
d3968363
ML
738 die "Problem applying binary changes! $!" if $?;
739 }
740 }
741
742 # bring in new files
f88961a8 743 system('rsync','-aI','--exclude',$git_dir,
a6080a0a 744 '--exclude','.arch-ids',
f88961a8
EW
745 '--exclude', '{arch}',
746 "$tmp/changeset/new-files-archive/",'./');
d3968363
ML
747
748 # deleted files are hinted from the commitlog processing
749
f88961a8 750 rmtree("$tmp/changeset");
d3968363
ML
751}
752
753
754# =for reference
6df896b5
EW
755# notes: *-files/-directories keys cannot have spaces, they're always
756# pika-escaped. Everything after the first newline
757# A log entry looks like:
d3968363
ML
758# Revision: moodle-org--moodle--1.3.3--patch-15
759# Archive: arch-eduforge@catalyst.net.nz--2004
760# Creator: Penny Leach <penny@catalyst.net.nz>
761# Date: Wed May 25 14:15:34 NZST 2005
762# Standard-date: 2005-05-25 02:15:34 GMT
763# New-files: lang/de/.arch-ids/block_glossary_random.php.id
764# lang/de/.arch-ids/block_html.php.id
765# New-directories: lang/de/help/questionnaire
766# lang/de/help/questionnaire/.arch-ids
767# Renamed-files: .arch-ids/db_sears.sql.id db/.arch-ids/db_sears.sql.id
768# db_sears.sql db/db_sears.sql
769# Removed-files: lang/be/docs/.arch-ids/release.html.id
770# lang/be/docs/.arch-ids/releaseold.html.id
771# Modified-files: admin/cron.php admin/delete.php
772# admin/editor.html backup/lib.php backup/restore.php
773# New-patches: arch-eduforge@catalyst.net.nz--2004/moodle-org--moodle--1.3.3--patch-15
774# Summary: Updating to latest from MOODLE_14_STABLE (1.4.5+)
6df896b5 775# summary can be multiline with a leading space just like the above fields
d3968363
ML
776# Keywords:
777#
778# Updating yadda tadda tadda madda
779sub parselog {
6df896b5
EW
780 my ($ps, $log) = @_;
781 my $key = undef;
782
783 # headers we want that contain filenames:
784 my %want_headers = (
785 new_files => 1,
786 modified_files => 1,
787 renamed_files => 1,
788 renamed_directories => 1,
789 removed_files => 1,
790 removed_directories => 1,
791 );
a6080a0a 792
6df896b5
EW
793 chomp (@$log);
794 while ($_ = shift @$log) {
795 if (/^Continuation-of:\s*(.*)/) {
796 $ps->{tag} = $1;
797 $key = undef;
798 } elsif (/^Summary:\s*(.*)$/ ) {
a94f457e
PB
799 # summary can be multiline as long as it has a leading space.
800 # we squeeze it onto a single line, though.
6df896b5
EW
801 $ps->{summary} = [ $1 ];
802 $key = 'summary';
803 } elsif (/^Creator: (.*)\s*<([^\>]+)>/) {
804 $ps->{author} = $1;
805 $ps->{email} = $2;
806 $key = undef;
807 # any *-files or *-directories can be read here:
808 } elsif (/^([A-Z][a-z\-]+):\s*(.*)$/) {
809 my $val = $2;
810 $key = lc $1;
811 $key =~ tr/-/_/; # too lazy to quote :P
812 if ($want_headers{$key}) {
813 push @{$ps->{$key}}, split(/\s+/, $val);
814 } else {
815 $key = undef;
816 }
817 } elsif (/^$/) {
818 last; # remainder of @$log that didn't get shifted off is message
819 } elsif ($key) {
820 if (/^\s+(.*)$/) {
821 if ($key eq 'summary') {
822 push @{$ps->{$key}}, $1;
823 } else { # files/directories:
824 push @{$ps->{$key}}, split(/\s+/, $1);
825 }
826 } else {
827 $key = undef;
828 }
829 }
d3968363 830 }
a6080a0a 831
a94f457e
PB
832 # drop leading empty lines from the log message
833 while (@$log && $log->[0] eq '') {
834 shift @$log;
835 }
836 if (exists $ps->{summary} && @{$ps->{summary}}) {
837 $ps->{summary} = join(' ', @{$ps->{summary}});
838 }
839 elsif (@$log == 0) {
840 $ps->{summary} = 'empty commit message';
841 } else {
842 $ps->{summary} = $log->[0] . '...';
843 }
6df896b5 844 $ps->{message} = join("\n",@$log);
a6080a0a 845
6df896b5
EW
846 # skip Arch control files, unescape pika-escaped files
847 foreach my $k (keys %want_headers) {
848 next unless (defined $ps->{$k});
6e33101a 849 my @tmp = ();
6df896b5
EW
850 foreach my $t (@{$ps->{$k}}) {
851 next unless length ($t);
852 next if $t =~ m!\{arch\}/!;
853 next if $t =~ m!\.arch-ids/!;
854 # should we skip this?
855 next if $t =~ m!\.arch-inventory$!;
f84f9d38
ML
856 # tla cat-archive-log will give us filenames with spaces as file\(sp)name - why?
857 # we can assume that any filename with \ indicates some pika escaping that we want to get rid of.
6df896b5 858 if ($t =~ /\\/ ){
f88961a8 859 $t = (safe_pipe_capture($TLA,'escape','--unescaped',$t))[0];
f84f9d38 860 }
6df896b5 861 push @tmp, $t;
d3968363 862 }
6e33101a 863 $ps->{$k} = \@tmp;
d3968363 864 }
d3968363
ML
865}
866
867# write/read a tag
868sub tag {
869 my ($tag, $commit) = @_;
a6080a0a 870
fee3365f
ML
871 if ($opt_o) {
872 $tag =~ s|/|--|g;
873 } else {
d9cb5399
PB
874 my $patchname = $tag;
875 $patchname =~ s/.*--//;
876 $tag = git_branchname ($tag) . '--' . $patchname;
fee3365f 877 }
a6080a0a 878
d3968363 879 if ($commit) {
a7fb51d3 880 open(C,">","$git_dir/refs/tags/$tag")
d3968363
ML
881 or die "Cannot create tag $tag: $!\n";
882 print C "$commit\n"
883 or die "Cannot write tag $tag: $!\n";
884 close(C)
885 or die "Cannot write tag $tag: $!\n";
a7fb51d3 886 print " * Created tag '$tag' on '$commit'\n" if $opt_v;
d3968363 887 } else { # read
a7fb51d3 888 open(C,"<","$git_dir/refs/tags/$tag")
d3968363
ML
889 or die "Cannot read tag $tag: $!\n";
890 $commit = <C>;
891 chomp $commit;
892 die "Error reading tag $tag: $!\n" unless length $commit == 40;
893 close(C)
894 or die "Cannot read tag $tag: $!\n";
895 return $commit;
896 }
897}
898
899# write/read a private tag
900# reads fail softly if the tag isn't there
901sub ptag {
902 my ($tag, $commit) = @_;
a7fb51d3
EW
903
904 # don't use subdirs for tags yet, it could screw up other porcelains
a6080a0a
JH
905 $tag =~ s|/|,|g;
906
a7fb51d3
EW
907 my $tag_file = "$ptag_dir/$tag";
908 my $tag_branch_dir = dirname($tag_file);
909 mkpath($tag_branch_dir) unless (-d $tag_branch_dir);
d3968363
ML
910
911 if ($commit) { # write
a7fb51d3 912 open(C,">",$tag_file)
d3968363
ML
913 or die "Cannot create tag $tag: $!\n";
914 print C "$commit\n"
915 or die "Cannot write tag $tag: $!\n";
916 close(C)
917 or die "Cannot write tag $tag: $!\n";
a6080a0a 918 $rptags{$commit} = $tag
b779d5f0 919 unless $tag =~ m/--base-0$/;
d3968363
ML
920 } else { # read
921 # if the tag isn't there, return 0
a7fb51d3 922 unless ( -s $tag_file) {
d3968363
ML
923 return 0;
924 }
a7fb51d3 925 open(C,"<",$tag_file)
d3968363
ML
926 or die "Cannot read tag $tag: $!\n";
927 $commit = <C>;
928 chomp $commit;
929 die "Error reading tag $tag: $!\n" unless length $commit == 40;
930 close(C)
931 or die "Cannot read tag $tag: $!\n";
b779d5f0
ML
932 unless (defined $rptags{$commit}) {
933 $rptags{$commit} = $tag;
934 }
d3968363
ML
935 return $commit;
936 }
937}
b779d5f0
ML
938
939sub find_parents {
940 #
941 # Identify what branches are merging into me
942 # and whether we are fully merged
943 # git-merge-base <headsha> <headsha> should tell
a6080a0a 944 # me what the base of the merge should be
b779d5f0
ML
945 #
946 my $ps = shift;
947
948 my %branches; # holds an arrayref per branch
949 # the arrayref contains a list of
950 # merged patches between the base
951 # of the merge and the current head
952
953 my @parents; # parents found for this commit
954
955 # simple loop to split the merges
956 # per branch
957 foreach my $merge (@{$ps->{merges}}) {
22ff00fc 958 my $branch = git_branchname($merge);
b779d5f0
ML
959 unless (defined $branches{$branch} ){
960 $branches{$branch} = [];
961 }
962 push @{$branches{$branch}}, $merge;
963 }
964
965 #
a6080a0a 966 # foreach branch find a merge base and walk it to the
b779d5f0
ML
967 # head where we are, collecting the merged patchsets that
968 # Arch has recorded. Keep that in @have
969 # Compare that with the commits on the other branch
970 # between merge-base and the tip of the branch (@need)
971 # and see if we have a series of consecutive patches
972 # starting from the merge base. The tip of the series
a6080a0a 973 # of consecutive patches merged is our new parent for
b779d5f0
ML
974 # that branch.
975 #
976 foreach my $branch (keys %branches) {
37f15d50
ML
977
978 # check that we actually know about the branch
979 next unless -e "$git_dir/refs/heads/$branch";
980
b779d5f0 981 my $mergebase = `git-merge-base $branch $ps->{branch}`;
a6080a0a
JH
982 if ($?) {
983 # Don't die here, Arch supports one-way cherry-picking
984 # between branches with no common base (or any relationship
985 # at all beforehand)
986 warn "Cannot find merge base for $branch and $ps->{branch}";
987 next;
988 }
b779d5f0
ML
989 chomp $mergebase;
990
991 # now walk up to the mergepoint collecting what patches we have
992 my $branchtip = git_rev_parse($ps->{branch});
765ac8ec 993 my @ancestors = `git-rev-list --topo-order $branchtip ^$mergebase`;
b779d5f0
ML
994 my %have; # collected merges this branch has
995 foreach my $merge (@{$ps->{merges}}) {
996 $have{$merge} = 1;
997 }
998 my %ancestorshave;
999 foreach my $par (@ancestors) {
1000 $par = commitid2pset($par);
1001 if (defined $par->{merges}) {
1002 foreach my $merge (@{$par->{merges}}) {
1003 $ancestorshave{$merge}=1;
1004 }
1005 }
1006 }
1007 # print "++++ Merges in $ps->{id} are....\n";
1008 # my @have = sort keys %have; print Dumper(\@have);
1009
1010 # merge what we have with what ancestors have
1011 %have = (%have, %ancestorshave);
1012
a6080a0a 1013 # see what the remote branch has - these are the merges we
b779d5f0
ML
1014 # will want to have in a consecutive series from the mergebase
1015 my $otherbranchtip = git_rev_parse($branch);
765ac8ec 1016 my @needraw = `git-rev-list --topo-order $otherbranchtip ^$mergebase`;
b779d5f0
ML
1017 my @need;
1018 foreach my $needps (@needraw) { # get the psets
1019 $needps = commitid2pset($needps);
1020 # git-rev-list will also
a6080a0a 1021 # list commits merged in via earlier
b779d5f0
ML
1022 # merges. we are only interested in commits
1023 # from the branch we're looking at
1024 if ($branch eq $needps->{branch}) {
1025 push @need, $needps->{id};
1026 }
1027 }
1028
1029 # print "++++ Merges from $branch we want are....\n";
1030 # print Dumper(\@need);
1031
1032 my $newparent;
1033 while (my $needed_commit = pop @need) {
1034 if ($have{$needed_commit}) {
1035 $newparent = $needed_commit;
1036 } else {
1037 last; # break out of the while
1038 }
1039 }
1040 if ($newparent) {
1041 push @parents, $newparent;
1042 }
1043
1044
1045 } # end foreach branch
1046
1047 # prune redundant parents
1048 my %parents;
1049 foreach my $p (@parents) {
1050 $parents{$p} = 1;
1051 }
1052 foreach my $p (@parents) {
1053 next unless exists $psets{$p}{merges};
1054 next unless ref $psets{$p}{merges};
1055 my @merges = @{$psets{$p}{merges}};
1056 foreach my $merge (@merges) {
a6080a0a 1057 if ($parents{$merge}) {
b779d5f0
ML
1058 delete $parents{$merge};
1059 }
1060 }
1061 }
42f44b08 1062
f88961a8
EW
1063 @parents = ();
1064 foreach (keys %parents) {
1065 push @parents, '-p', ptag($_);
1066 }
b779d5f0
ML
1067 return @parents;
1068}
1069
1070sub git_rev_parse {
1071 my $name = shift;
1072 my $val = `git-rev-parse $name`;
1073 die "Error: git-rev-parse $name" if $?;
1074 chomp $val;
1075 return $val;
1076}
1077
1078# resolve a SHA1 to a known patchset
1079sub commitid2pset {
1080 my $commitid = shift;
1081 chomp $commitid;
a6080a0a 1082 my $name = $rptags{$commitid}
b779d5f0 1083 || die "Cannot find reverse tag mapping for $commitid";
a7fb51d3 1084 $name =~ s|,|/|;
a6080a0a 1085 my $ps = $psets{$name}
b779d5f0
ML
1086 || (print Dumper(sort keys %psets)) && die "Cannot find patchset for $name";
1087 return $ps;
1088}
2777ef76 1089
42f44b08 1090
82e5a82f 1091# an alternative to `command` that allows input to be passed as an array
2777ef76
EW
1092# to work around shell problems with weird characters in arguments
1093sub safe_pipe_capture {
1094 my @output;
1095 if (my $pid = open my $child, '-|') {
1096 @output = (<$child>);
1097 close $child or die join(' ',@_).": $! $?";
1098 } else {
3e525e67 1099 exec(@_) or die "$! $?"; # exec() can fail the executable can't be found
2777ef76
EW
1100 }
1101 return wantarray ? @output : join('',@output);
1102}
1103
42f44b08
EW
1104# `tla logs -rf -d <dir> | head -n1` or `baz tree-id <dir>`
1105sub arch_tree_id {
1106 my $dir = shift;
1107 chomp( my $ret = (safe_pipe_capture($TLA,'logs','-rf','-d',$dir))[0] );
1108 return $ret;
1109}
1110
1111sub archive_reachable {
1112 my $archive = shift;
1113 return 1 if $reachable{$archive};
1114 return 0 if $unreachable{$archive};
a6080a0a 1115
42f44b08
EW
1116 if (system "$TLA whereis-archive $archive >/dev/null") {
1117 if ($opt_a && (system($TLA,'register-archive',
1118 "http://mirrors.sourcecontrol.net/$archive") == 0)) {
1119 $reachable{$archive} = 1;
1120 return 1;
1121 }
1122 print STDERR "Archive is unreachable: $archive\n";
1123 $unreachable{$archive} = 1;
1124 return 0;
1125 } else {
1126 $reachable{$archive} = 1;
1127 return 1;
1128 }
1129}