diff --git a/Documentation/git-svn.txt b/Documentation/git-svn.txt index f5f57e8f87..f754d2f679 100644 --- a/Documentation/git-svn.txt +++ b/Documentation/git-svn.txt @@ -3,7 +3,7 @@ git-svn(1) NAME ---- -git-svn - bidirectional operation between a single Subversion branch and git +git-svn - bidirectional operation between Subversion and git SYNOPSIS -------- @@ -11,24 +11,20 @@ SYNOPSIS DESCRIPTION ----------- -git-svn is a simple conduit for changesets between a single Subversion -branch and git. It is not to be confused with gitlink:git-svnimport[1]. -They were designed with very different goals in mind. +git-svn is a simple conduit for changesets between Subversion and git. +It is not to be confused with gitlink:git-svnimport[1], which is +read-only and geared towards tracking multiple branches. -git-svn is designed for an individual developer who wants a +git-svn was originally designed for an individual developer who wants a bidirectional flow of changesets between a single branch in Subversion -and an arbitrary number of branches in git. git-svnimport is designed -for read-only operation on repositories that match a particular layout -(albeit the recommended one by SVN developers). +and an arbitrary number of branches in git. Since its inception, +git-svn has gained the ability to track multiple branches in a manner +similar to git-svnimport; but it cannot (yet) automatically detect new +branches and tags like git-svnimport does. -For importing svn, git-svnimport is potentially more powerful when -operating on repositories organized under the recommended -trunk/branch/tags structure, and should be faster, too. - -git-svn mostly ignores the very limited view of branching that -Subversion has. This allows git-svn to be much easier to use, -especially on repositories that are not organized in a manner that -git-svnimport is designed for. +git-svn is especially useful when it comes to tracking repositories +not organized in the way Subversion developers recommend (trunk, +branches, tags directories). COMMANDS -------- @@ -370,7 +366,7 @@ SVN was very wrong. Basic Examples ~~~~~~~~~~~~~~ -Tracking and contributing to a Subversion-managed project: +Tracking and contributing to a the trunk of a Subversion-managed project: ------------------------------------------------------------------------ # Initialize a repo (like git init-db): @@ -388,6 +384,30 @@ Tracking and contributing to a Subversion-managed project: git-svn show-ignore >> .git/info/exclude ------------------------------------------------------------------------ +Tracking and contributing to an entire Subversion-managed project +(complete with a trunk, tags and branches): +See also: +'<>' + +------------------------------------------------------------------------ +# Initialize a repo (like git init-db): + git-svn multi-init http://svn.foo.org/project \ + -T trunk -b branches -t tags +# Fetch remote revisions: + git-svn multi-fetch +# Create your own branch of trunk to hack on: + git checkout -b my-trunk remotes/trunk +# Do some work, and then commit your new changes to SVN, as well as +# automatically updating your working HEAD: + git-svn dcommit -i trunk +# Something has been committed to trunk, rebase the latest into your branch: + git-svn multi-fetch && git rebase remotes/trunk +# Append svn:ignore settings of trunk to the default git exclude file: + git-svn show-ignore -i trunk >> .git/info/exclude +# Check for new branches and tags (no arguments are needed): + git-svn multi-init +------------------------------------------------------------------------ + REBASE VS. PULL --------------- diff --git a/Makefile b/Makefile index af79895b84..0b3b7d350a 100644 --- a/Makefile +++ b/Makefile @@ -179,7 +179,7 @@ SCRIPT_SH = \ SCRIPT_PERL = \ git-add--interactive.perl \ git-archimport.perl git-cvsimport.perl git-relink.perl \ - git-cvsserver.perl \ + git-cvsserver.perl git-remote.perl \ git-svnimport.perl git-cvsexportcommit.perl \ git-send-email.perl git-svn.perl @@ -818,7 +818,7 @@ test-sha1$X: test-sha1.o $(GITLIBS) check-sha1:: test-sha1$X ./test-sha1.sh -check: +check: common-cmds.h for i in *.c; do sparse $(ALL_CFLAGS) $(SPARSE_FLAGS) $$i || exit; done diff --git a/builtin-rerere.c b/builtin-rerere.c index 7442498dee..079c0bdf36 100644 --- a/builtin-rerere.c +++ b/builtin-rerere.c @@ -350,11 +350,10 @@ static int do_plain_rerere(struct path_list *rr, int fd) fprintf(stderr, "Recorded resolution for '%s'.\n", path); copy_file(path, rr_path(name, "postimage")); tail_optimization: - if (i < rr->nr - 1) { + if (i < rr->nr - 1) memmove(rr->items + i, - rr->items + i + 1, - rr->nr - i - 1); - } + rr->items + i + 1, + sizeof(rr->items[0]) * (rr->nr - i - 1)); rr->nr--; i--; } diff --git a/cache.h b/cache.h index dbca69ddfb..36be64e386 100644 --- a/cache.h +++ b/cache.h @@ -179,6 +179,7 @@ extern int refresh_cache(unsigned int flags); struct lock_file { struct lock_file *next; + char on_list; char filename[PATH_MAX]; }; extern int hold_lock_file_for_update(struct lock_file *, const char *path, int); diff --git a/git-remote.perl b/git-remote.perl new file mode 100755 index 0000000000..059c141b68 --- /dev/null +++ b/git-remote.perl @@ -0,0 +1,277 @@ +#!/usr/bin/perl -w + +use Git; +my $git = Git->repository(); + +sub add_remote_config { + my ($hash, $name, $what, $value) = @_; + if ($what eq 'url') { + if (exists $hash->{$name}{'URL'}) { + print STDERR "Warning: more than one remote.$name.url\n"; + } + $hash->{$name}{'URL'} = $value; + } + elsif ($what eq 'fetch') { + $hash->{$name}{'FETCH'} ||= []; + push @{$hash->{$name}{'FETCH'}}, $value; + } + if (!exists $hash->{$name}{'SOURCE'}) { + $hash->{$name}{'SOURCE'} = 'config'; + } +} + +sub add_remote_remotes { + my ($hash, $file, $name) = @_; + + if (exists $hash->{$name}) { + $hash->{$name}{'WARNING'} = 'ignored due to config'; + return; + } + + my $fh; + if (!open($fh, '<', $file)) { + print STDERR "Warning: cannot open $file\n"; + return; + } + my $it = { 'SOURCE' => 'remotes' }; + $hash->{$name} = $it; + while (<$fh>) { + chomp; + if (/^URL:\s*(.*)$/) { + # Having more than one is Ok -- it is used for push. + if (! exists $it->{'URL'}) { + $it->{'URL'} = $1; + } + } + elsif (/^Push:\s*(.*)$/) { + ; # later + } + elsif (/^Pull:\s*(.*)$/) { + $it->{'FETCH'} ||= []; + push @{$it->{'FETCH'}}, $1; + } + elsif (/^\#/) { + ; # ignore + } + else { + print STDERR "Warning: funny line in $file: $_\n"; + } + } + close($fh); +} + +sub list_remote { + my ($git) = @_; + my %seen = (); + my @remotes = eval { + $git->command(qw(repo-config --get-regexp), '^remote\.'); + }; + for (@remotes) { + if (/^remote\.([^.]*)\.(\S*)\s+(.*)$/) { + add_remote_config(\%seen, $1, $2, $3); + } + } + + my $dir = $git->repo_path() . "/remotes"; + if (opendir(my $dh, $dir)) { + local $_; + while ($_ = readdir($dh)) { + chomp; + next if (! -f "$dir/$_" || ! -r _); + add_remote_remotes(\%seen, "$dir/$_", $_); + } + } + + return \%seen; +} + +sub add_branch_config { + my ($hash, $name, $what, $value) = @_; + if ($what eq 'remote') { + if (exists $hash->{$name}{'REMOTE'}) { + print STDERR "Warning: more than one branch.$name.remote\n"; + } + $hash->{$name}{'REMOTE'} = $value; + } + elsif ($what eq 'merge') { + $hash->{$name}{'MERGE'} ||= []; + push @{$hash->{$name}{'MERGE'}}, $value; + } +} + +sub list_branch { + my ($git) = @_; + my %seen = (); + my @branches = eval { + $git->command(qw(repo-config --get-regexp), '^branch\.'); + }; + for (@branches) { + if (/^branch\.([^.]*)\.(\S*)\s+(.*)$/) { + add_branch_config(\%seen, $1, $2, $3); + } + } + + return \%seen; +} + +my $remote = list_remote($git); +my $branch = list_branch($git); + +sub update_ls_remote { + my ($harder, $info) = @_; + + return if (($harder == 0) || + (($harder == 1) && exists $info->{'LS_REMOTE'})); + + my @ref = map { + s|^[0-9a-f]{40}\s+refs/heads/||; + $_; + } $git->command(qw(ls-remote --heads), $info->{'URL'}); + $info->{'LS_REMOTE'} = \@ref; +} + +sub show_wildcard_mapping { + my ($forced, $ours, $ls) = @_; + my %refs; + for (@$ls) { + $refs{$_} = 01; # bit #0 to say "they have" + } + for ($git->command('for-each-ref', "refs/remotes/$ours")) { + chomp; + next unless (s|^[0-9a-f]{40}\s[a-z]+\srefs/remotes/$ours/||); + next if ($_ eq 'HEAD'); + $refs{$_} ||= 0; + $refs{$_} |= 02; # bit #1 to say "we have" + } + my (@new, @stale, @tracked); + for (sort keys %refs) { + my $have = $refs{$_}; + if ($have == 1) { + push @new, $_; + } + elsif ($have == 2) { + push @stale, $_; + } + elsif ($have == 3) { + push @tracked, $_; + } + } + if (@new) { + print " New remote branches (next fetch will store in remotes/$ours)\n"; + print " @new\n"; + } + if (@stale) { + print " Stale tracking branches in remotes/$ours (you'd better remove them)\n"; + print " @stale\n"; + } + if (@tracked) { + print " Tracked remote branches\n"; + print " @tracked\n"; + } +} + +sub show_mapping { + my ($name, $info) = @_; + my $fetch = $info->{'FETCH'}; + my $ls = $info->{'LS_REMOTE'}; + my (@stale, @tracked); + + for (@$fetch) { + next unless (/(\+)?([^:]+):(.*)/); + my ($forced, $theirs, $ours) = ($1, $2, $3); + if ($theirs eq 'refs/heads/*' && + $ours =~ /^refs\/remotes\/(.*)\/\*$/) { + # wildcard mapping + show_wildcard_mapping($forced, $1, $ls); + } + elsif ($theirs =~ /\*/ || $ours =~ /\*/) { + print STDERR "Warning: unrecognized mapping in remotes.$name.fetch: $_\n"; + } + elsif ($theirs =~ s|^refs/heads/||) { + if (!grep { $_ eq $theirs } @$ls) { + push @stale, $theirs; + } + elsif ($ours ne '') { + push @tracked, $theirs; + } + } + } + if (@stale) { + print " Stale tracking branches in remotes/$name (you'd better remove them)\n"; + print " @stale\n"; + } + if (@tracked) { + print " Tracked remote branches\n"; + print " @tracked\n"; + } +} + +sub show_remote { + my ($name, $ls_remote) = @_; + if (!exists $remote->{$name}) { + print STDERR "No such remote $name\n"; + return; + } + my $info = $remote->{$name}; + update_ls_remote($ls_remote, $info); + + print "* remote $name\n"; + print " URL: $info->{'URL'}\n"; + for my $branchname (sort keys %$branch) { + next if ($branch->{$branchname}{'REMOTE'} ne $name); + my @merged = map { + s|^refs/heads/||; + $_; + } split(' ',"@{$branch->{$branchname}{'MERGE'}}"); + next unless (@merged); + print " Remote branch(es) merged with 'git pull' while on branch $branchname\n"; + print " @merged\n"; + } + if ($info->{'LS_REMOTE'}) { + show_mapping($name, $info); + } +} + +sub add_remote { + my ($name, $url) = @_; + if (exists $remote->{$name}) { + print STDERR "remote $name already exists.\n"; + exit(1); + } + $git->command('repo-config', "remote.$name.url", $url); + $git->command('repo-config', "remote.$name.fetch", + "+refs/heads/*:refs/remotes/$name/*"); +} + +if (!@ARGV) { + for (sort keys %$remote) { + print "$_\n"; + } +} +elsif ($ARGV[0] eq 'show') { + my $ls_remote = 1; + my $i; + for ($i = 1; $i < @ARGV; $i++) { + if ($ARGV[$i] eq '-n') { + $ls_remote = 0; + } + else { + last; + } + } + if ($i >= @ARGV) { + print STDERR "Usage: git remote show \n"; + exit(1); + } + for (; $i < @ARGV; $i++) { + show_remote($ARGV[$i], $ls_remote); + } +} +elsif ($ARGV[0] eq 'add') { + if (@ARGV != 3) { + print STDERR "Usage: git remote add \n"; + exit(1); + } + add_remote($ARGV[1], $ARGV[2]); +} + diff --git a/git-svn.perl b/git-svn.perl index b28c5bbc72..537776239c 100755 --- a/git-svn.perl +++ b/git-svn.perl @@ -102,7 +102,7 @@ my %cmt_opts = ( 'edit|e' => \$_edit, ); my %cmd = ( - fetch => [ \&fetch, "Download new revisions from SVN", + fetch => [ \&cmd_fetch, "Download new revisions from SVN", { 'revision|r=s' => \$_revision, %fc_opts } ], init => [ \&init, "Initialize a repo for tracking" . " (requires URL argument)", @@ -293,6 +293,10 @@ sub init { setup_git_svn(); } +sub cmd_fetch { + fetch_child_id($GIT_SVN, @_); +} + sub fetch { check_upgrade_needed(); $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url"); @@ -571,28 +575,25 @@ sub graft_branches { sub multi_init { my $url = shift; - $_trunk ||= 'trunk'; - $_trunk =~ s#/+$##; - $url =~ s#/+$## if $url; - if ($_trunk !~ m#^[a-z\+]+://#) { - $_trunk = '/' . $_trunk if ($_trunk !~ m#^/#); - unless ($url) { - print STDERR "E: '$_trunk' is not a complete URL ", - "and a separate URL is not specified\n"; - exit 1; + unless (defined $_trunk || defined $_branches || defined $_tags) { + usage(1); + } + if (defined $_trunk) { + my $trunk_url = complete_svn_url($url, $_trunk); + my $ch_id; + if ($GIT_SVN eq 'git-svn') { + $ch_id = 1; + $GIT_SVN = $ENV{GIT_SVN_ID} = 'trunk'; + } + init_vars(); + unless (-d $GIT_SVN_DIR) { + if ($ch_id) { + print "GIT_SVN_ID set to 'trunk' for ", + "$trunk_url ($_trunk)\n"; + } + init($trunk_url); + command_noisy('repo-config', 'svn.trunk', $trunk_url); } - $_trunk = $url . $_trunk; - } - my $ch_id; - if ($GIT_SVN eq 'git-svn') { - $ch_id = 1; - $GIT_SVN = $ENV{GIT_SVN_ID} = 'trunk'; - } - init_vars(); - unless (-d $GIT_SVN_DIR) { - print "GIT_SVN_ID set to 'trunk' for $_trunk\n" if $ch_id; - init($_trunk); - command_noisy('repo-config', 'svn.trunk', $_trunk); } complete_url_ls_init($url, $_branches, '--branches/-b', ''); complete_url_ls_init($url, $_tags, '--tags/-t', 'tags/'); @@ -839,7 +840,6 @@ sub fetch_child_id { my $ref = "$GIT_DIR/refs/remotes/$id"; defined(my $pid = open my $fh, '-|') or croak $!; if (!$pid) { - $_repack = undef; $GIT_SVN = $ENV{GIT_SVN_ID} = $id; init_vars(); fetch(@_); @@ -847,7 +847,7 @@ sub fetch_child_id { } while (<$fh>) { print $_; - check_repack() if (/^r\d+ = $sha1/); + check_repack() if (/^r\d+ = $sha1/o); } close $fh or croak $?; } @@ -872,29 +872,34 @@ sub rec_fetch { } } +sub complete_svn_url { + my ($url, $path) = @_; + $path =~ s#/+$##; + $url =~ s#/+$## if $url; + if ($path !~ m#^[a-z\+]+://#) { + $path = '/' . $path if ($path !~ m#^/#); + if (!defined $url || $url !~ m#^[a-z\+]+://#) { + fatal("E: '$path' is not a complete URL ", + "and a separate URL is not specified\n"); + } + $path = $url . $path; + } + return $path; +} + sub complete_url_ls_init { - my ($url, $var, $switch, $pfx) = @_; - unless ($var) { + my ($url, $path, $switch, $pfx) = @_; + unless ($path) { print STDERR "W: $switch not specified\n"; return; } - $var =~ s#/+$##; - if ($var !~ m#^[a-z\+]+://#) { - $var = '/' . $var if ($var !~ m#^/#); - unless ($url) { - print STDERR "E: '$var' is not a complete URL ", - "and a separate URL is not specified\n"; - exit 1; - } - $var = $url . $var; - } - my @ls = libsvn_ls_fullurl($var); - my $old = $GIT_SVN; + my $full_url = complete_svn_url($url, $path); + my @ls = libsvn_ls_fullurl($full_url); defined(my $pid = fork) or croak $!; if (!$pid) { - foreach my $u (map { "$var/$_" } (grep m!/$!, @ls)) { + foreach my $u (map { "$full_url/$_" } (grep m!/$!, @ls)) { $u =~ s#/+$##; - if ($u !~ m!\Q$var\E/(.+)$!) { + if ($u !~ m!\Q$full_url\E/(.+)$!) { print STDERR "W: Unrecognized URL: $u\n"; die "This should never happen\n"; } @@ -912,7 +917,7 @@ sub complete_url_ls_init { waitpid $pid, 0; croak $? if $?; my ($n) = ($switch =~ /^--(\w+)/); - command_noisy('repo-config', "svn.$n", $var); + command_noisy('repo-config', "svn.$n", $full_url); } sub common_prefix { @@ -1405,7 +1410,6 @@ sub git_commit { # this output is read via pipe, do not change: print "r$log_msg->{revision} = $commit\n"; - check_repack(); return $commit; } diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 2179054286..7906280f26 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -2813,8 +2813,12 @@ sub git_tags_body { print "\n"; } $alternate ^= 1; - print "$tag{'age'}\n" . - "" . + if (defined $tag{'age'}) { + print "$tag{'age'}\n"; + } else { + print "\n"; + } + print "" . $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}), -class => "list name"}, esc_html($tag{'name'})) . "\n" . @@ -3208,9 +3212,14 @@ HTML esc_html($rev)); print "\n"; } + open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^") + or die_error("could not open git-rev-parse"); + my $parent_commit = <$dd>; + close $dd; + chomp($parent_commit); my $blamed = href(action => 'blame', file_name => $meta->{'filename'}, - hash_base => $full_rev); + hash_base => $parent_commit); print ""; print $cgi->a({ -href => "$blamed#l$orig_lineno", -id => "l$lineno", diff --git a/lockfile.c b/lockfile.c index 261baff049..731bbf3c9c 100644 --- a/lockfile.c +++ b/lockfile.c @@ -27,9 +27,12 @@ static int lock_file(struct lock_file *lk, const char *path) sprintf(lk->filename, "%s.lock", path); fd = open(lk->filename, O_RDWR | O_CREAT | O_EXCL, 0666); if (0 <= fd) { - if (!lk->next) { + if (!lk->on_list) { lk->next = lock_file_list; lock_file_list = lk; + lk->on_list = 1; + } + if (lock_file_list) { signal(SIGINT, remove_lock_file_on_signal); atexit(remove_lock_file); } @@ -37,6 +40,8 @@ static int lock_file(struct lock_file *lk, const char *path) return error("cannot fix permission bits on %s", lk->filename); } + else + lk->filename[0] = 0; return fd; } diff --git a/refs.c b/refs.c index 121774cb32..52057455a2 100644 --- a/refs.c +++ b/refs.c @@ -726,7 +726,6 @@ static int repack_without_ref(const char *refname) } if (!found) return 0; - memset(&packlock, 0, sizeof(packlock)); fd = hold_lock_file_for_update(&packlock, git_path("packed-refs"), 0); if (fd < 0) return error("cannot delete '%s' from packed refs", refname); diff --git a/tree-walk.c b/tree-walk.c index 14cc5aea6c..22f4550b6c 100644 --- a/tree-walk.c +++ b/tree-walk.c @@ -113,7 +113,6 @@ void traverse_trees(int n, struct tree_desc *t, const char *base, traverse_callb struct name_entry *entry = xmalloc(n*sizeof(*entry)); for (;;) { - struct name_entry entry[3]; unsigned long mask = 0; int i, last;