git-svn: add a 'rebase' command

This works similarly to 'svn update' or 'git pull' except that
it preserves linear history with 'git rebase' instead of 'git
merge' for ease of dcommit-ing with git-svn.

While we're at it, put the working_head_info() logic
into its own function and allow --fetch-all/--all for
dcommit and rebase (which will fetch all refs in the
current [svn-remote] instead of just the working one).

Note that the '-a' switch (short for --fetch-all/--all) has been
removed as it conflicts with the non-svn 'git fetch'

Signed-off-by: Eric Wong <normalperson@yhbt.net>
diff --git a/git-svn.perl b/git-svn.perl
index 7ffbf64..eca08bd 100755
--- a/git-svn.perl
+++ b/git-svn.perl
@@ -56,7 +56,7 @@
 	$_template, $_shared,
 	$_version, $_fetch_all,
 	$_merge, $_strategy, $_dry_run,
-	$_prefix, $_no_checkout);
+	$_prefix, $_no_checkout, $_verbose);
 $Git::SVN::_follow_parent = 1;
 my %remote_opts = ( 'username=s' => \$Git::SVN::Prompt::_username,
                     'config-dir=s' => \$Git::SVN::Ra::config_dir,
@@ -88,7 +88,7 @@
 my %cmd = (
 	fetch => [ \&cmd_fetch, "Download new revisions from SVN",
 			{ 'revision|r=s' => \$_revision,
-			  'all|a' => \$_fetch_all,
+			  'fetch-all|all' => \$_fetch_all,
 			   %fc_opts } ],
 	init => [ \&cmd_init, "Initialize a repo for tracking" .
 			  " (requires URL argument)",
@@ -101,7 +101,9 @@
 	             'Commit several diffs to merge with upstream',
 			{ 'merge|m|M' => \$_merge,
 			  'strategy|s=s' => \$_strategy,
+			  'verbose|v' => \$_verbose,
 			  'dry-run|n' => \$_dry_run,
+			  'fetch-all|all' => \$_fetch_all,
 			%cmt_opts, %fc_opts } ],
 	'set-tree' => [ \&cmd_set_tree,
 	                "Set an SVN repository to a git tree-ish",
@@ -129,6 +131,12 @@
 			  'color' => \$Git::SVN::Log::color,
 			  'pager=s' => \$Git::SVN::Log::pager,
 			} ],
+	'rebase' => [ \&cmd_rebase, "Fetch and rebase your working directory",
+			{ 'merge|m|M' => \$_merge,
+			  'verbose|v' => \$_verbose,
+			  'strategy|s=s' => \$_strategy,
+			  'fetch-all|all' => \$_fetch_all,
+			  %fc_opts } ],
 	'commit-diff' => [ \&cmd_commit_diff,
 	                   'Commit a diff between two trees',
 			{ 'message|m=s' => \$_message,
@@ -248,7 +256,7 @@
 	}
 	my ($remote) = @_;
 	if (@_ > 1) {
-		die "Usage: $0 fetch [--all|-a] [svn-remote]\n";
+		die "Usage: $0 fetch [--all] [svn-remote]\n";
 	}
 	$remote ||= $Git::SVN::default_repo_id;
 	if ($_fetch_all) {
@@ -296,21 +304,12 @@
 sub cmd_dcommit {
 	my $head = shift;
 	$head ||= 'HEAD';
-	my ($url, $rev, $uuid);
-	my ($fh, $ctx) = command_output_pipe('rev-list', $head);
 	my @refs;
-	my $c;
-	while (<$fh>) {
-		$c = $_;
-		chomp $c;
-		($url, $rev, $uuid) = cmt_metadata($c);
-		last if (defined $url && defined $rev && defined $uuid);
-		unshift @refs, $c;
-	}
-	close $fh; # most likely breaking the pipe
+	my ($url, $rev, $uuid) = working_head_info($head, \@refs);
+	my $c = $refs[-1];
 	unless (defined $url && defined $rev && defined $uuid) {
 		die "Unable to determine upstream SVN information from ",
-		    "$head history:\n  $ctx\n";
+		    "$head history\n";
 	}
 	my $gs = Git::SVN->find_by_url($url);
 	my $last_rev;
@@ -354,15 +353,13 @@
 		     "now resync your SVN::Mirror repository.\n";
 		return;
 	}
-	$gs->fetch;
+	$_fetch_all ? $gs->fetch_all : $gs->fetch;
 	# we always want to rebase against the current HEAD, not any
 	# head that was passed to us
 	my @diff = command('diff-tree', 'HEAD', $gs->refname, '--');
 	my @finish;
 	if (@diff) {
-		@finish = qw/rebase/;
-		push @finish, qw/--merge/ if $_merge;
-		push @finish, "--strategy=$_strategy" if $_strategy;
+		@finish = rebase_cmd();
 		print STDERR "W: HEAD and ", $gs->refname, " differ, ",
 		             "using @finish:\n", "@diff";
 	} else {
@@ -374,6 +371,24 @@
 	command_noisy(@finish, $gs->refname);
 }
 
+sub cmd_rebase {
+	command_noisy(qw/update-index --refresh/);
+	my $url = (working_head_info('HEAD'))[0];
+	if (!defined $url) {
+		die "Unable to determine upstream SVN information from ",
+		    "working tree history\n";
+	}
+
+	my $gs = Git::SVN->find_by_url($url);
+	if (command(qw/diff-index HEAD --/)) {
+		print STDERR "Cannot rebase with uncommited changes:\n";
+		command_noisy('status');
+		exit 1;
+	}
+	$_fetch_all ? $gs->fetch_all : $gs->fetch;
+	command_noisy(rebase_cmd(), $gs->refname);
+}
+
 sub cmd_show_ignore {
 	my $gs = Git::SVN->new;
 	my $r = (defined $_revision ? $_revision : $gs->ra->get_latest_revnum);
@@ -468,6 +483,14 @@
 
 ########################### utility functions #########################
 
+sub rebase_cmd {
+	my @cmd = qw/rebase/;
+	push @cmd, '-v' if $_verbose;
+	push @cmd, qw/--merge/ if $_merge;
+	push @cmd, "--strategy=$_strategy" if $_strategy;
+	@cmd;
+}
+
 sub post_fetch_checkout {
 	return if $_no_checkout;
 	my $gs = $Git::SVN::_head or return;
@@ -687,6 +710,20 @@
 		command(qw/cat-file commit/, shift)))[-1]);
 }
 
+sub working_head_info {
+	my ($head, $refs) = @_;
+	my ($url, $rev, $uuid);
+	my ($fh, $ctx) = command_output_pipe('rev-list', $head);
+	while (<$fh>) {
+		chomp;
+		($url, $rev, $uuid) = cmt_metadata($_);
+		last if (defined $url && defined $rev && defined $uuid);
+		unshift @$refs, $_ if $refs;
+	}
+	close $fh; # break the pipe
+	($url, $rev, $uuid);
+}
+
 package Git::SVN;
 use strict;
 use warnings;
@@ -783,6 +820,12 @@
 
 sub fetch_all {
 	my ($repo_id, $remotes) = @_;
+	if (ref $repo_id) {
+		my $gs = $repo_id;
+		$repo_id = undef;
+		$repo_id = $gs->{repo_id};
+	}
+	$remotes ||= read_all_remotes();
 	my $remote = $remotes->{$repo_id} or
 	             die "[svn-remote \"$repo_id\"] unknown\n";
 	my $fetch = $remote->{fetch};
@@ -3085,15 +3128,7 @@
 		last;
 	}
 
-	my $url;
-	my ($fh, $ctx) = command_output_pipe('rev-list', $head);
-	while (<$fh>) {
-		chomp;
-		$url = (::cmt_metadata($_))[0];
-		last if defined $url;
-	}
-	close $fh; # break the pipe
-
+	my $url = (::working_head_info($head))[0];
 	my $gs = Git::SVN->find_by_url($url) || Git::SVN->_new;
 	my @cmd = (qw/log --abbrev-commit --pretty=raw --default/,
 	           $gs->refname);