- create tag objects instead of ref tags
- audit shell-escaping of filenames
- - better handling of temp directories
- - use GIT_DIR instead of hardcoded ".git"
- hide our private tags somewhere smarter
- find a way to make "cat *patches | patch" safe even when patchfiles are missing newlines
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::Path qw(mkpath);
+use File::Temp qw(tempfile tempdir);
+use File::Path qw(mkpath rmtree);
use File::Basename qw(basename dirname);
-use String::ShellQuote;
use Time::Local;
use IO::Socket;
use IO::Pipe;
$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 <<END;
Usage: ${\basename $0} # fetch/update GIT from Arch
- [ -h ] [ -v ] [ -T ] [ -t tempdir ]
+ [ -o ] [ -h ] [ -v ] [ -T ] [ -t tempdir ]
repository/arch-branch [ repository/arch-branch] ...
END
exit(1);
@ARGV >= 1 or usage();
my @arch_roots = @ARGV;
-my $tmp = $opt_t;
-$tmp ||= '/tmp';
-$tmp .= '/git-archimport/';
+$ENV{'TMPDIR'} = $opt_t if $opt_t; # $ENV{TMPDIR} will affect tempdir() calls:
+my $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
+my $TLA = $ENV{'ARCH_CLIENT'} || 'tla';
foreach my $root (@arch_roots) {
my ($arepo, $abranch) = split(m!/!, $root);
## 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`;
}
} else { # progressing an import
# load the rptags
- opendir(DIR, ".git/archimport/tags")
+ opendir(DIR, "$git_dir/archimport/tags")
|| die "can't opendir: $!";
while (my $file = readdir(DIR)) {
- # skip non-interesting-files
- next unless -f ".git/archimport/tags/$file";
- next if $file =~ m/--base-0$/; # don't care for base-0
+ # 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;
- # reconvert the 3rd '--' sequence from the end
- # into a slash
- # $file = reverse $file;
- # $file =~ s!^(.+?--.+?--.+?--.+?)--(.+)$!$1/$2!;
- # $file = reverse $file;
$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;
+}
- $ps->{branch} = branchname($ps->{id});
+# 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 <category--branch> 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;
+
+# process patchsets
+foreach my $ps (@psets) {
+ $ps->{branch} = git_branchname($ps->{id});
#
# ensure we have a clean state
#
- if (`git diff-files`) {
+ if (`git-diff-files`) {
die "Unclean tree when about to process $ps->{id} " .
" - did we fail to commit cleanly before?";
}
# 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
#
}
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}`;
+ system('git-checkout',$ps->{branch});
} else {
# new branch! we need to verify a few things
die "Branch on a non-tag!" unless $ps->{type} eq 't';
unless $branchpoint;
# find where we are supposed to branch from
- `git checkout -b $ps->{branch} $branchpoint`;
+ system('git-checkout','-b',$ps->{branch},$branchpoint);
# If we trust Arch with the fact that this is just
# a tag, and it does not affect the state of the tree
#
my $tree;
- my $commitlog = `tla cat-archive-log -A $ps->{repo} $ps->{id}`;
+ my $commitlog = safe_pipe_capture($TLA,'cat-archive-log',$ps->{id});
die "Error in cat-archive-log: $!" if $?;
# parselog will git-add/rm files
# 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`;
}
# 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 = <HEAD>;
close HEAD;
chomp $p;
if ($ps->{merges}) {
push @par, find_parents($ps);
}
- my $par = join (' ', @par);
#
# Commit, tag and clean state
$commit_rh = 'commit_rh';
$commit_wh = 'commit_wh';
- $pid = open2(*READER, *WRITER, "git-commit-tree $tree $par")
+ $pid = open2(*READER, *WRITER,'git-commit-tree',$tree,@par)
or die $!;
print WRITER $logmessage; # write
close WRITER;
#
# 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
print " + tree $tree\n";
print " + commit $commitid\n";
$opt_v && print " + commit date is $ps->{date} \n";
- $opt_v && print " + parents: $par \n";
-}
-
-sub branchname {
- my $id = shift;
- $id =~ s#^.+?/##;
- my @parts = split(m/--/, $id);
- return join('--', @parts[0..1]);
+ $opt_v && print " + parents: ",join(' ',@par),"\n";
}
sub apply_import {
my $ps = shift;
- my $bname = branchname($ps->{id});
+ my $bname = git_branchname($ps->{id});
- `mkdir -p $tmp`;
+ mkpath($tmp);
- `tla get -s --no-pristine -A $ps->{repo} $ps->{id} $tmp/import`;
+ safe_pipe_capture($TLA,'get','-s','--no-pristine',$ps->{id},"$tmp/import");
die "Cannot get import: $!" if $?;
- `rsync -v --archive --delete --exclude '.git' --exclude '.arch-ids' --exclude '{arch}' $tmp/import/* ./`;
+ system('rsync','-aI','--delete', '--exclude',$git_dir,
+ '--exclude','.arch-ids','--exclude','{arch}',
+ "$tmp/import/", './');
die "Cannot rsync import:$!" if $?;
- `rm -fr $tmp/import`;
+ rmtree("$tmp/import");
die "Cannot remove tempdir: $!" if $?;
sub apply_cset {
my $ps = shift;
- `mkdir -p $tmp`;
+ mkpath($tmp);
# get the changeset
- `tla get-changeset -A $ps->{repo} $ps->{id} $tmp/changeset`;
+ safe_pipe_capture($TLA,'get-changeset',$ps->{id},"$tmp/changeset");
die "Cannot get changeset: $!" if $?;
# apply patches
$orig =~ s/\.modified$//; # lazy
$orig =~ s!^\Q$tmp\E/changeset/patches/!!;
#print "rsync -p '$mod' '$orig'";
- `rsync -p $mod ./$orig`;
+ system('rsync','-p',$mod,"./$orig");
die "Problem applying binary changes! $!" if $?;
}
}
# bring in new files
- `rsync --archive --exclude '.git' --exclude '.arch-ids' --exclude '{arch}' $tmp/changeset/new-files-archive/* ./`;
+ system('rsync','-aI','--exclude',$git_dir,
+ '--exclude','.arch-ids',
+ '--exclude', '{arch}',
+ "$tmp/changeset/new-files-archive/",'./');
# deleted files are hinted from the commitlog processing
- `rm -fr $tmp/changeset`;
+ rmtree("$tmp/changeset");
}
next if $t =~ m!\{arch\}/!;
next if $t =~ m!\.arch-ids/!;
next if $t =~ m!\.arch-inventory$!;
- push (@tmp, shell_quote($t));
+ # 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 = (safe_pipe_capture($TLA,'escape','--unescaped',$t))[0];
+ }
+ push (@tmp, $t);
}
@$ref = @tmp;
}
# 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 = <C>;
chomp $commit;
# 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";
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 = <C>;
chomp $commit;
# simple loop to split the merges
# per branch
foreach my $merge (@{$ps->{merges}}) {
- my $branch = branchname($merge);
+ my $branch = git_branchname($merge);
unless (defined $branches{$branch} ){
$branches{$branch} = [];
}
# 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}`;
- die "Cannot find merge base for $branch and $ps->{branch}" if $?;
+ 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
}
}
}
- @parents = keys %parents;
- @parents = map { " -p " . ptag($_) } @parents;
+ @parents = ();
+ foreach (keys %parents) {
+ push @parents, '-p', ptag($_);
+ }
return @parents;
}
chomp $commitid;
my $name = $rptags{$commitid}
|| die "Cannot find reverse tag mapping for $commitid";
- # the keys in %rptag are slightly munged; unmunge
- # reconvert the 3rd '--' sequence from the end
- # into a slash
- $name = reverse $name;
- $name =~ s!^(.+?--.+?--.+?--.+?)--(.+)$!$1/$2!;
- $name = reverse $name;
+ $name =~ s|,|/|;
my $ps = $psets{$name}
|| (print Dumper(sort keys %psets)) && die "Cannot find patchset for $name";
return $ps;
}
+
+# an alterative to `command` that allows input to be passed as an array
+# to work around shell problems with weird characters in arguments
+sub safe_pipe_capture {
+ my @output;
+ if (my $pid = open my $child, '-|') {
+ @output = (<$child>);
+ close $child or die join(' ',@_).": $! $?";
+ } else {
+ exec(@_) or die $?; # exec() can fail the executable can't be found
+ }
+ return wantarray ? @output : join('',@output);
+}
+
+