From 1084b845d9d77bcb2e8255636358dd0dc35360a5 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Tue, 2 Jan 2007 11:19:05 -0800 Subject: [PATCH 01/11] Fix infinite loop when deleting multiple packed refs. It was stupid to link the same element twice to lock_file_list and end up in a loop, so we certainly need a fix. But it is not like we are taking a lock on multiple files in this case. It is just that we leave the linked element on the list even after commit_lock_file() successfully removes the cruft. We cannot remove the list element in commit_lock_file(); if we are interrupted in the middle of list manipulation, the call to remove_lock_file_on_signal() will happen with a broken list structure pointed by lock_file_list, which would cause the cruft to remain, so not removing the list element is the right thing to do. Instead we should be reusing the element already on the list. There is already a code for that in lock_file() function in lockfile.c. The code checks lk->next and the element is linked only when it is not already on the list -- which is incorrect for the last element on the list (which has NULL in its next field), but if you read the check as "is this element already on the list?" it actually makes sense. We do not want to link it on the list again, nor we would want to set up signal/atexit over and over. Signed-off-by: Junio C Hamano --- cache.h | 1 + lockfile.c | 7 ++++++- refs.c | 1 - 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/cache.h b/cache.h index f2ec5c8c14..a0e9727a0b 100644 --- a/cache.h +++ b/cache.h @@ -174,6 +174,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/lockfile.c b/lockfile.c index 2a2fea3cb6..143d7d85b6 100644 --- a/lockfile.c +++ b/lockfile.c @@ -28,9 +28,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); } @@ -38,6 +41,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 0e156c5dee..1549f2ae06 100644 --- a/refs.c +++ b/refs.c @@ -644,7 +644,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); From 8977c110b5bbd230c28c727ddb85856067d55cfb Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Wed, 3 Jan 2007 23:09:08 -0800 Subject: [PATCH 02/11] pack-check.c::verify_packfile(): don't run SHA-1 update on huge data Running the SHA1_Update() on the whole packfile in a single call revealed an overflow problem we had in the SHA-1 implementation on POWER architecture some time ago, which was fixed with commit b47f509b (June 19, 2006). Other SHA-1 implementations may have a similar problem. The sliding mmap() series already makes chunked calls to SHA1_Update(), so this patch itself will become moot when it graduates to "master", but in the meantime, run the hash function in smaller chunks to prevent possible future problems. Signed-off-by: Junio C Hamano --- pack-check.c | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/pack-check.c b/pack-check.c index c0caaee093..8e123b71ed 100644 --- a/pack-check.c +++ b/pack-check.c @@ -1,16 +1,18 @@ #include "cache.h" #include "pack.h" +#define BATCH (1u<<20) + static int verify_packfile(struct packed_git *p) { unsigned long index_size = p->index_size; void *index_base = p->index_base; SHA_CTX ctx; unsigned char sha1[20]; - unsigned long pack_size = p->pack_size; - void *pack_base; struct pack_header *hdr; int nr_objects, err, i; + unsigned char *packdata; + unsigned long datasize; /* Header consistency check */ hdr = p->pack_base; @@ -25,11 +27,19 @@ static int verify_packfile(struct packed_git *p) "while idx size expects %d", nr_objects, num_packed_objects(p)); + /* Check integrity of pack data with its SHA-1 checksum */ SHA1_Init(&ctx); - pack_base = p->pack_base; - SHA1_Update(&ctx, pack_base, pack_size - 20); + packdata = p->pack_base; + datasize = p->pack_size - 20; + while (datasize) { + unsigned long batch = (datasize < BATCH) ? datasize : BATCH; + SHA1_Update(&ctx, packdata, batch); + datasize -= batch; + packdata += batch; + } SHA1_Final(sha1, &ctx); - if (hashcmp(sha1, (unsigned char *)pack_base + pack_size - 20)) + + if (hashcmp(sha1, (unsigned char *)(p->pack_base) + p->pack_size - 20)) return error("Packfile %s SHA1 mismatch with itself", p->pack_name); if (hashcmp(sha1, (unsigned char *)index_base + index_size - 40)) From 3a2d3e8678a01e327c8e6c851ed6d2cfd3c21aaf Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 4 Jan 2007 00:06:50 -0800 Subject: [PATCH 03/11] rerere: Fix removal of already resolved path. There was an obvious thinko in memmove() to remove an entry that was resolved from the in-core data structure. Signed-off-by: Junio C Hamano --- builtin-rerere.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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--; } From 7c4c9f4cd9c36694ce0de5674f263585064cceec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Scharfe?= Date: Thu, 4 Jan 2007 19:33:48 +0100 Subject: [PATCH 04/11] Make check target depend on common-cmds.h This fixes sparse complaining about a missing include file if 'make check' is run on clean sources. Signed-off-by: Rene Scharfe Signed-off-by: Junio C Hamano --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index fa1a02289c..180e1e0b94 100644 --- a/Makefile +++ b/Makefile @@ -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 From 4fe2cc0c8906ba6d39c4fab89fe88dcf2226b184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Scharfe?= Date: Thu, 4 Jan 2007 19:33:36 +0100 Subject: [PATCH 05/11] Remove shadowing variable from traverse_trees() The variable named entry is allocated using malloc() and then forgotten, it being shadowed by an automatic variable of the same name. Fixing the array size at 3 worked so far because the only caller of traverse_trees() needed only as much entries. Simply remove the shadowing varaible and we're able to traverse more than three trees and save stack space at the same time! Signed-off-by: Rene Scharfe Signed-off-by: Junio C Hamano --- tree-walk.c | 1 - 1 file changed, 1 deletion(-) 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; From 98327e5891471e7baceda5c6543a387f0dd21d3a Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Thu, 4 Jan 2007 18:02:00 -0800 Subject: [PATCH 06/11] git-svn: make multi-init less confusing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It now requires at least one of the (trunk|branch|tags) arguments (either from the command-line or in .git/config). Also we make sure that anything that is passed as a URL ('help') in David's case is actually a URL. Thanks to David Kågedal for reporting this issue. Signed-off-by: Eric Wong Signed-off-by: Junio C Hamano --- git-svn.perl | 78 +++++++++++++++++++++++++++------------------------- 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/git-svn.perl b/git-svn.perl index b28c5bbc72..0fc386a715 100755 --- a/git-svn.perl +++ b/git-svn.perl @@ -571,28 +571,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/'); @@ -872,29 +869,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 +914,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 { From 0d313b2b7bb219542473a25ad042f4b990e69a45 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Thu, 4 Jan 2007 18:04:06 -0800 Subject: [PATCH 07/11] git-svn: update documentation for multi-{init|fetch} Signed-off-by: Eric Wong Signed-off-by: Junio C Hamano --- Documentation/git-svn.txt | 54 +++++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 17 deletions(-) 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 --------------- From 2a3240beaad73dd86d9220c8a8a6e76c6d034a57 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Thu, 4 Jan 2007 18:09:56 -0800 Subject: [PATCH 08/11] git-svn: make --repack work consistently between fetch and multi-fetch Since fetch reforks itself at most every 1000 revisions, we need to update the counter in the parent process to have a working count if we set our repack interval to be > ~1000 revisions. multi-fetch has always done this correctly because of an extra process; now fetch uses the extra process; as well. While we're at it, only compile the $sha1 regex that checks for repacking once. Signed-off-by: Eric Wong Signed-off-by: Junio C Hamano --- git-svn.perl | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/git-svn.perl b/git-svn.perl index 0fc386a715..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"); @@ -836,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(@_); @@ -844,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 $?; } @@ -1407,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; } From 27dd1a83cbedb65d7116814ef2c0bf9d4986c8f5 Mon Sep 17 00:00:00 2001 From: Jakub Narebski Date: Wed, 3 Jan 2007 22:54:29 +0100 Subject: [PATCH 09/11] gitweb: Fix "Use of uninitialized value" warning in git_tags_body Fix "Use of uninitialized value" warning in git_tags_body generated for lightweight tags of tree and blob object; those don't have age ($tag{'age'}) defined. Signed-off-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/gitweb.perl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 2179054286..fd671f3eb2 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" . From 244a70e608204a515c214a11c43f3ecf7642533a Mon Sep 17 00:00:00 2001 From: Luben Tuikov Date: Thu, 4 Jan 2007 18:37:45 -0800 Subject: [PATCH 10/11] Blame "linenr" link jumps to previous state at "orig_lineno" Blame currently displays the commit id which introduced a block of one or more lines, the line numbers wrt the current listing of the file and the file's line contents. The commit id displayed is hyperlinked to the commit. Currently the linenr links are hyperlinked to the same commit id displayed to the left, which is _no_ different than the block of lines displayed, since it is the _same commit_ that is hyperlinked. And thus clicking on it leads to the same state of the file for that chunk of lines. I.e. data mining is not currently possible with gitweb given a chunk of lines introduced by a commit. This patch makes such data mining possible. The line numbers are now hyperlinked to the parent of the commit id of the block of lines. Furthermore they are linked to the line where that block was introduced. Thus clicking on a linenr link will show you the file's line(s) state prior to the commit id you were viewing. So clicking continually on a linenr link shows you how this line and its line number changed over time, leading to the initial commit where it was first introduced. Signed-off-by: Luben Tuikov Signed-off-by: Junio C Hamano --- gitweb/gitweb.perl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index fd671f3eb2..7906280f26 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -3212,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", From e194cd1e0e08611462eb9c5a731a7a3d797f9252 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Wed, 3 Jan 2007 12:13:04 -0800 Subject: [PATCH 11/11] git-remote It might be handy to have a single command that helps you manage your configuration that relates to downloading from remote repositories. This currently does only about 20% of what I want it to do. $ git remote shows the list of 'remotes' you have defined somewhere, and $ git remote origin shows the details about the named remote (in this case "origin"). How the branches are tracked, if you have a tracking branch that is stale, etc. $ git add another git://git.kernel.org/pub/... defines the default remote.another.url and remote.another.fetch entries just like a clone does; you can say "git fetch another" afterwards. For it to be useful, I think it should be enhanced to: - check overlaps of tracking branches and warn; - offer to remove stale tracking branches in one go; - offer ways to remove or rename remote; - offer ways to update an existing remote, perhaps have an interactive mode; Other enhancements might be also possible, but I do not think of anything that is absolutely necessary other than the above right now. Signed-off-by: Junio C Hamano --- Makefile | 2 +- git-remote.perl | 277 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 278 insertions(+), 1 deletion(-) create mode 100755 git-remote.perl diff --git a/Makefile b/Makefile index 180e1e0b94..5c712b94c1 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 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]); +} +