X-Git-Url: https://git.octo.it/?a=blobdiff_plain;f=git-archimport.perl;h=c3bed08086d69fe3cc79b64117193a9173faa91f;hb=3dd94e3b2e2f5b00512273f96ab5628c742c112c;hp=e9e6f1b7d29a1391326f6df91cbfa69931f3c5a9;hpb=215a7ad1ef790467a4cd3f0dcffbd6e5f04c38f7;p=git.git diff --git a/git-archimport.perl b/git-archimport.perl index e9e6f1b7..c3bed080 100755 --- a/git-archimport.perl +++ b/git-archimport.perl @@ -6,35 +6,55 @@ # The basic idea is to walk the output of tla abrowse, # fetch the changesets and apply them. # + =head1 Invocation - git-archimport -i / [/] - [ / ] + git-archimport [ -h ] [ -v ] [ -T ] [ -t tempdir ] / [ / ] - The script expects you to provide the key roots where it can start the - import from an 'initial import' or 'tag' type of Arch commit. It will - then follow all the branching and tagging within the provided roots. +Imports a project from one or more Arch repositories. It will follow branches +and repositories within the namespaces defined by the +parameters suppplied. If it cannot find the remote branch a merge comes from +it will just import it as a regular commit. If it can find it, it will mark it +as a merge whenever possible. - It will die if it sees branches that have different roots. +See man (1) git-archimport for more details. -=head2 TODO +=head1 TODO - - keep track of merged patches, and mark a git merge when it happens - - smarter rules to parse the archive history "up" and "down" - - be able to continue an import where we left off + - create tag objects instead of ref tags - audit shell-escaping of filenames + - hide our private tags somewhere smarter + - find a way to make "cat *patches | patch" safe even when patchfiles are missing newlines =head1 Devel tricks Add print in front of the shell commands invoked via backticks. +=head1 Devel Notes + +There are several places where Arch and git terminology are intermixed +and potentially confused. + +The notion of a "branch" in git is approximately equivalent to +a "archive/category--branch--version" in Arch. Also, it should be noted +that the "--branch" portion of "archive/category--branch--version" is really +optional in Arch although not many people (nor tools!) seem to know this. +This means that "archive/category--version" is also a valid "branch" +in git terms. + +We always refer to Arch names by their fully qualified variant (which +means the "archive" name is prefixed. + +For people unfamiliar with Arch, an "archive" is the term for "repository", +and can contain multiple, unrelated branches. + =cut use strict; use warnings; use Getopt::Std; use File::Spec; -use File::Temp qw(tempfile); +use File::Temp qw(tempfile tempdir); use File::Path qw(mkpath); use File::Basename qw(basename dirname); use String::ShellQuote; @@ -48,34 +68,37 @@ use IPC::Open2; $SIG{'PIPE'}="IGNORE"; $ENV{'TZ'}="UTC"; -our($opt_h,$opt_v, $opt_T, - $opt_C,$opt_t); +my $git_dir = $ENV{"GIT_DIR"} || ".git"; +$ENV{"GIT_DIR"} = $git_dir; +my $ptag_dir = "$git_dir/archimport/tags"; + +our($opt_h,$opt_v, $opt_T,$opt_t,$opt_o); sub usage() { print STDERR <= 1 or usage(); my @arch_roots = @ARGV; -my $tmp = $opt_t; -$tmp ||= '/tmp'; -$tmp .= '/git-archimport/'; - -my $git_tree = $opt_C; -$git_tree ||= "."; - +my ($tmpdir, $tmpdirname) = tempdir('git-archimport-XXXXXX', TMPDIR => 1, CLEANUP => 1); +my $tmp = $opt_t || 1; +$tmp = tempdir('git-archimport-XXXXXX', TMPDIR => 1, CLEANUP => 1); +$opt_v && print "+ Using $tmp as temporary directory\n"; my @psets = (); # the collection +my %psets = (); # the collection, by name + +my %rptags = (); # my reverse private tags + # to map a SHA1 to a commitid foreach my $root (@arch_roots) { my ($arepo, $abranch) = split(m!/!, $root); @@ -96,6 +119,7 @@ foreach my $root (@arch_roots) { if (%ps) { my %temp = %ps; # break references push (@psets, \%temp); + $psets{$temp{id}} = \%temp; %ps = (); } @@ -158,7 +182,8 @@ foreach my $root (@arch_roots) { if (%ps) { my %temp = %ps; # break references - push (@psets, \%temp); + push (@psets, \%temp); + $psets{ $temp{id} } = \%temp; %ps = (); } close ABROWSE; @@ -174,7 +199,7 @@ foreach my $root (@arch_roots) { ## and put an initial import ## or a full tag my $import = 0; -unless (-d '.git') { # initial import +unless (-d $git_dir) { # initial import if ($psets[0]{type} eq 'i' || $psets[0]{type} eq 't') { print "Starting import from $psets[0]{id}\n"; `git-init-db`; @@ -183,12 +208,73 @@ unless (-d '.git') { # initial import } else { die "Need to start from an import or a tag -- cannot use $psets[0]{id}"; } +} else { # progressing an import + # load the rptags + opendir(DIR, "$git_dir/archimport/tags") + || die "can't opendir: $!"; + while (my $file = readdir(DIR)) { + # skip non-interesting-files + next unless -f "$ptag_dir/$file"; + + # convert first '--' to '/' from old git-archimport to use + # as an archivename/c--b--v private tag + if ($file !~ m!,!) { + my $oldfile = $file; + $file =~ s!--!,!; + print STDERR "converting old tag $oldfile to $file\n"; + rename("$ptag_dir/$oldfile", "$ptag_dir/$file") or die $!; + } + my $sha = ptag($file); + chomp $sha; + $rptags{$sha} = $file; + } + closedir DIR; } # process patchsets -foreach my $ps (@psets) { +# extract the Arch repository name (Arch "archive" in Arch-speak) +sub extract_reponame { + my $fq_cvbr = shift; # archivename/[[[[category]branch]version]revision] + return (split(/\//, $fq_cvbr))[0]; +} + +sub extract_versionname { + my $name = shift; + $name =~ s/--(?:patch|version(?:fix)?|base)-\d+$//; + return $name; +} + +# convert a fully-qualified revision or version to a unique dirname: +# normalperson@yhbt.net-05/mpd--uclinux--1--patch-2 +# becomes: normalperson@yhbt.net-05,mpd--uclinux--1 +# +# the git notion of a branch is closer to +# archive/category--branch--version than archive/category--branch, so we +# use this to convert to git branch names. +# Also, keep archive names but replace '/' with ',' since it won't require +# subdirectories, and is safer than swapping '--' which could confuse +# reverse-mapping when dealing with bastard branches that +# are just archive/category--version (no --branch) +sub tree_dirname { + my $revision = shift; + my $name = extract_versionname($revision); + $name =~ s#/#,#; + return $name; +} + +# old versions of git-archimport just use the part: +sub old_style_branchname { + my $id = shift; + my $ret = safe_pipe_capture($TLA,'parse-package-name','-p',$id); + chomp $ret; + return $ret; +} + +*git_branchname = $opt_o ? *old_style_branchname : *tree_dirname; - $ps->{branch} = branchname($ps->{id}); +# process patchsets +foreach my $ps (@psets) { + $ps->{branch} = git_branchname($ps->{id}); # # ensure we have a clean state @@ -203,10 +289,12 @@ foreach my $ps (@psets) { # skip commits already in repo # if (ptag($ps->{id})) { - $opt_v && print "Skipping already imported: $ps->{id}\n"; + $opt_v && print " * Skipping already imported: $ps->{id}\n"; next; } + print " * Starting to work on $ps->{id}\n"; + # # create the branch if needed # @@ -215,7 +303,7 @@ foreach my $ps (@psets) { } unless ($import) { # skip for import - if ( -e ".git/refs/heads/$ps->{branch}") { + if ( -e "$git_dir/refs/heads/$ps->{branch}") { # we know about this branch `git checkout $ps->{branch}`; } else { @@ -268,7 +356,7 @@ foreach my $ps (@psets) { # imports don't give us good info # on added files. Shame on them if ($ps->{type} eq 'i' || $ps->{type} eq 't') { - `find . -type f -print0 | grep -zv '^./.git' | xargs -0 -l100 git-update-index --add`; + `find . -type f -print0 | grep -zv '^./$git_dir' | xargs -0 -l100 git-update-index --add`; `git-ls-files --deleted -z | xargs --no-run-if-empty -0 -l100 git-update-index --remove`; } @@ -332,8 +420,8 @@ foreach my $ps (@psets) { # Who's your daddy? # my @par; - if ( -e ".git/refs/heads/$ps->{branch}") { - if (open HEAD, "<.git/refs/heads/$ps->{branch}") { + if ( -e "$git_dir/refs/heads/$ps->{branch}") { + if (open HEAD, "<$git_dir/refs/heads/$ps->{branch}") { my $p = ; close HEAD; chomp $p; @@ -345,6 +433,9 @@ foreach my $ps (@psets) { } } + if ($ps->{merges}) { + push @par, find_parents($ps); + } my $par = join (' ', @par); # @@ -377,11 +468,10 @@ foreach my $ps (@psets) { # # Update the branch # - open HEAD, ">.git/refs/heads/$ps->{branch}"; + open HEAD, ">$git_dir/refs/heads/$ps->{branch}"; print HEAD $commitid; close HEAD; - unlink ('.git/HEAD'); - symlink("refs/heads/$ps->{branch}",".git/HEAD"); + system('git-update-ref', 'HEAD', "$ps->{branch}"); # tag accordingly ptag($ps->{id}, $commitid); # private tag @@ -391,25 +481,19 @@ foreach my $ps (@psets) { print " * Committed $ps->{id}\n"; print " + tree $tree\n"; print " + commit $commitid\n"; - # print " + commit date is $ps->{date} \n"; -} - -sub branchname { - my $id = shift; - $id =~ s#^.+?/##; - my @parts = split(m/--/, $id); - return join('--', @parts[0..1]); + $opt_v && print " + commit date is $ps->{date} \n"; + $opt_v && print " + parents: $par \n"; } sub apply_import { my $ps = shift; - my $bname = branchname($ps->{id}); + my $bname = git_branchname($ps->{id}); `mkdir -p $tmp`; `tla get -s --no-pristine -A $ps->{repo} $ps->{id} $tmp/import`; die "Cannot get import: $!" if $?; - `rsync -v --archive --delete --exclude '.git' --exclude '.arch-ids' --exclude '{arch}' $tmp/import/* ./`; + `rsync -v --archive --delete --exclude '$git_dir' --exclude '.arch-ids' --exclude '{arch}' $tmp/import/* ./`; die "Cannot rsync import:$!" if $?; `rm -fr $tmp/import`; @@ -455,7 +539,7 @@ sub apply_cset { } # bring in new files - `rsync --archive --exclude '.git' --exclude '.arch-ids' --exclude '{arch}' $tmp/changeset/new-files-archive/* ./`; + `rsync --archive --exclude '$git_dir' --exclude '.arch-ids' --exclude '{arch}' $tmp/changeset/new-files-archive/* ./`; # deleted files are hinted from the commitlog processing @@ -534,6 +618,11 @@ sub parselog { next if $t =~ m!\{arch\}/!; next if $t =~ m!\.arch-ids/!; next if $t =~ m!\.arch-inventory$!; + # tla cat-archive-log will give us filenames with spaces as file\(sp)name - why? + # we can assume that any filename with \ indicates some pika escaping that we want to get rid of. + if ($t =~ /\\/ ){ + $t = `tla escape --unescaped '$t'`; + } push (@tmp, shell_quote($t)); } @$ref = @tmp; @@ -546,19 +635,24 @@ sub parselog { # write/read a tag sub tag { my ($tag, $commit) = @_; - $tag =~ s|/|--|g; - $tag = shell_quote($tag); + + if ($opt_o) { + $tag =~ s|/|--|g; + } else { + # don't use subdirs for tags yet, it could screw up other porcelains + $tag =~ s|/|,|g; + } if ($commit) { - open(C,">.git/refs/tags/$tag") + open(C,">","$git_dir/refs/tags/$tag") or die "Cannot create tag $tag: $!\n"; print C "$commit\n" or die "Cannot write tag $tag: $!\n"; close(C) or die "Cannot write tag $tag: $!\n"; - print "Created tag '$tag' on '$commit'\n" if $opt_v; + print " * Created tag '$tag' on '$commit'\n" if $opt_v; } else { # read - open(C,"<.git/refs/tags/$tag") + open(C,"<","$git_dir/refs/tags/$tag") or die "Cannot read tag $tag: $!\n"; $commit = ; chomp $commit; @@ -573,32 +667,186 @@ sub tag { # reads fail softly if the tag isn't there sub ptag { my ($tag, $commit) = @_; - $tag =~ s|/|--|g; - $tag = shell_quote($tag); + + # don't use subdirs for tags yet, it could screw up other porcelains + $tag =~ s|/|,|g; - unless (-d '.git/archimport/tags') { - mkpath('.git/archimport/tags'); - } + my $tag_file = "$ptag_dir/$tag"; + my $tag_branch_dir = dirname($tag_file); + mkpath($tag_branch_dir) unless (-d $tag_branch_dir); if ($commit) { # write - open(C,">.git/archimport/tags/$tag") + open(C,">",$tag_file) or die "Cannot create tag $tag: $!\n"; print C "$commit\n" or die "Cannot write tag $tag: $!\n"; close(C) or die "Cannot write tag $tag: $!\n"; + $rptags{$commit} = $tag + unless $tag =~ m/--base-0$/; } else { # read # if the tag isn't there, return 0 - unless ( -s ".git/archimport/tags/$tag") { + unless ( -s $tag_file) { return 0; } - open(C,"<.git/archimport/tags/$tag") + open(C,"<",$tag_file) or die "Cannot read tag $tag: $!\n"; $commit = ; chomp $commit; die "Error reading tag $tag: $!\n" unless length $commit == 40; close(C) or die "Cannot read tag $tag: $!\n"; + unless (defined $rptags{$commit}) { + $rptags{$commit} = $tag; + } return $commit; } } + +sub find_parents { + # + # Identify what branches are merging into me + # and whether we are fully merged + # git-merge-base should tell + # me what the base of the merge should be + # + my $ps = shift; + + my %branches; # holds an arrayref per branch + # the arrayref contains a list of + # merged patches between the base + # of the merge and the current head + + my @parents; # parents found for this commit + + # simple loop to split the merges + # per branch + foreach my $merge (@{$ps->{merges}}) { + my $branch = git_branchname($merge); + unless (defined $branches{$branch} ){ + $branches{$branch} = []; + } + push @{$branches{$branch}}, $merge; + } + + # + # foreach branch find a merge base and walk it to the + # head where we are, collecting the merged patchsets that + # Arch has recorded. Keep that in @have + # Compare that with the commits on the other branch + # between merge-base and the tip of the branch (@need) + # and see if we have a series of consecutive patches + # starting from the merge base. The tip of the series + # of consecutive patches merged is our new parent for + # that branch. + # + foreach my $branch (keys %branches) { + + # check that we actually know about the branch + next unless -e "$git_dir/refs/heads/$branch"; + + my $mergebase = `git-merge-base $branch $ps->{branch}`; + if ($?) { + # Don't die here, Arch supports one-way cherry-picking + # between branches with no common base (or any relationship + # at all beforehand) + warn "Cannot find merge base for $branch and $ps->{branch}"; + next; + } + chomp $mergebase; + + # now walk up to the mergepoint collecting what patches we have + my $branchtip = git_rev_parse($ps->{branch}); + my @ancestors = `git-rev-list --merge-order $branchtip ^$mergebase`; + my %have; # collected merges this branch has + foreach my $merge (@{$ps->{merges}}) { + $have{$merge} = 1; + } + my %ancestorshave; + foreach my $par (@ancestors) { + $par = commitid2pset($par); + if (defined $par->{merges}) { + foreach my $merge (@{$par->{merges}}) { + $ancestorshave{$merge}=1; + } + } + } + # print "++++ Merges in $ps->{id} are....\n"; + # my @have = sort keys %have; print Dumper(\@have); + + # merge what we have with what ancestors have + %have = (%have, %ancestorshave); + + # see what the remote branch has - these are the merges we + # will want to have in a consecutive series from the mergebase + my $otherbranchtip = git_rev_parse($branch); + my @needraw = `git-rev-list --merge-order $otherbranchtip ^$mergebase`; + my @need; + foreach my $needps (@needraw) { # get the psets + $needps = commitid2pset($needps); + # git-rev-list will also + # list commits merged in via earlier + # merges. we are only interested in commits + # from the branch we're looking at + if ($branch eq $needps->{branch}) { + push @need, $needps->{id}; + } + } + + # print "++++ Merges from $branch we want are....\n"; + # print Dumper(\@need); + + my $newparent; + while (my $needed_commit = pop @need) { + if ($have{$needed_commit}) { + $newparent = $needed_commit; + } else { + last; # break out of the while + } + } + if ($newparent) { + push @parents, $newparent; + } + + + } # end foreach branch + + # prune redundant parents + my %parents; + foreach my $p (@parents) { + $parents{$p} = 1; + } + foreach my $p (@parents) { + next unless exists $psets{$p}{merges}; + next unless ref $psets{$p}{merges}; + my @merges = @{$psets{$p}{merges}}; + foreach my $merge (@merges) { + if ($parents{$merge}) { + delete $parents{$merge}; + } + } + } + @parents = keys %parents; + @parents = map { " -p " . ptag($_) } @parents; + return @parents; +} + +sub git_rev_parse { + my $name = shift; + my $val = `git-rev-parse $name`; + die "Error: git-rev-parse $name" if $?; + chomp $val; + return $val; +} + +# resolve a SHA1 to a known patchset +sub commitid2pset { + my $commitid = shift; + chomp $commitid; + my $name = $rptags{$commitid} + || die "Cannot find reverse tag mapping for $commitid"; + $name =~ s|,|/|; + my $ps = $psets{$name} + || (print Dumper(sort keys %psets)) && die "Cannot find patchset for $name"; + return $ps; +}