summaryrefslogtreecommitdiff
path: root/contrib/examples
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/examples')
-rwxr-xr-xcontrib/examples/git-checkout.sh302
-rwxr-xr-xcontrib/examples/git-remote.perl477
-rwxr-xr-xcontrib/examples/git-rerere.perl284
3 files changed, 1063 insertions, 0 deletions
diff --git a/contrib/examples/git-checkout.sh b/contrib/examples/git-checkout.sh
new file mode 100755
index 0000000000..1a7689a48f
--- /dev/null
+++ b/contrib/examples/git-checkout.sh
@@ -0,0 +1,302 @@
+#!/bin/sh
+
+OPTIONS_KEEPDASHDASH=t
+OPTIONS_SPEC="\
+git-checkout [options] [<branch>] [<paths>...]
+--
+b= create a new branch started at <branch>
+l create the new branch's reflog
+track arrange that the new branch tracks the remote branch
+f proceed even if the index or working tree is not HEAD
+m merge local modifications into the new branch
+q,quiet be quiet
+"
+SUBDIRECTORY_OK=Sometimes
+. git-sh-setup
+require_work_tree
+
+old_name=HEAD
+old=$(git rev-parse --verify $old_name 2>/dev/null)
+oldbranch=$(git symbolic-ref $old_name 2>/dev/null)
+new=
+new_name=
+force=
+branch=
+track=
+newbranch=
+newbranch_log=
+merge=
+quiet=
+v=-v
+LF='
+'
+
+while test $# != 0; do
+ case "$1" in
+ -b)
+ shift
+ newbranch="$1"
+ [ -z "$newbranch" ] &&
+ die "git checkout: -b needs a branch name"
+ git show-ref --verify --quiet -- "refs/heads/$newbranch" &&
+ die "git checkout: branch $newbranch already exists"
+ git check-ref-format "heads/$newbranch" ||
+ die "git checkout: we do not like '$newbranch' as a branch name."
+ ;;
+ -l)
+ newbranch_log=-l
+ ;;
+ --track|--no-track)
+ track="$1"
+ ;;
+ -f)
+ force=1
+ ;;
+ -m)
+ merge=1
+ ;;
+ -q|--quiet)
+ quiet=1
+ v=
+ ;;
+ --)
+ shift
+ break
+ ;;
+ *)
+ usage
+ ;;
+ esac
+ shift
+done
+
+arg="$1"
+rev=$(git rev-parse --verify "$arg" 2>/dev/null)
+if rev=$(git rev-parse --verify "$rev^0" 2>/dev/null)
+then
+ [ -z "$rev" ] && die "unknown flag $arg"
+ new_name="$arg"
+ if git show-ref --verify --quiet -- "refs/heads/$arg"
+ then
+ rev=$(git rev-parse --verify "refs/heads/$arg^0")
+ branch="$arg"
+ fi
+ new="$rev"
+ shift
+elif rev=$(git rev-parse --verify "$rev^{tree}" 2>/dev/null)
+then
+ # checking out selected paths from a tree-ish.
+ new="$rev"
+ new_name="$rev^{tree}"
+ shift
+fi
+[ "$1" = "--" ] && shift
+
+case "$newbranch,$track" in
+,--*)
+ die "git checkout: --track and --no-track require -b"
+esac
+
+case "$force$merge" in
+11)
+ die "git checkout: -f and -m are incompatible"
+esac
+
+# The behaviour of the command with and without explicit path
+# parameters is quite different.
+#
+# Without paths, we are checking out everything in the work tree,
+# possibly switching branches. This is the traditional behaviour.
+#
+# With paths, we are _never_ switching branch, but checking out
+# the named paths from either index (when no rev is given),
+# or the named tree-ish (when rev is given).
+
+if test "$#" -ge 1
+then
+ hint=
+ if test "$#" -eq 1
+ then
+ hint="
+Did you intend to checkout '$@' which can not be resolved as commit?"
+ fi
+ if test '' != "$newbranch$force$merge"
+ then
+ die "git checkout: updating paths is incompatible with switching branches/forcing$hint"
+ fi
+ if test '' != "$new"
+ then
+ # from a specific tree-ish; note that this is for
+ # rescuing paths and is never meant to remove what
+ # is not in the named tree-ish.
+ git ls-tree --full-name -r "$new" "$@" |
+ git update-index --index-info || exit $?
+ fi
+
+ # Make sure the request is about existing paths.
+ git ls-files --full-name --error-unmatch -- "$@" >/dev/null || exit
+ git ls-files --full-name -- "$@" |
+ (cd_to_toplevel && git checkout-index -f -u --stdin)
+
+ # Run a post-checkout hook -- the HEAD does not change so the
+ # current HEAD is passed in for both args
+ if test -x "$GIT_DIR"/hooks/post-checkout; then
+ "$GIT_DIR"/hooks/post-checkout $old $old 0
+ fi
+
+ exit $?
+else
+ # Make sure we did not fall back on $arg^{tree} codepath
+ # since we are not checking out from an arbitrary tree-ish,
+ # but switching branches.
+ if test '' != "$new"
+ then
+ git rev-parse --verify "$new^{commit}" >/dev/null 2>&1 ||
+ die "Cannot switch branch to a non-commit."
+ fi
+fi
+
+# We are switching branches and checking out trees, so
+# we *NEED* to be at the toplevel.
+cd_to_toplevel
+
+[ -z "$new" ] && new=$old && new_name="$old_name"
+
+# If we don't have an existing branch that we're switching to,
+# and we don't have a new branch name for the target we
+# are switching to, then we are detaching our HEAD from any
+# branch. However, if "git checkout HEAD" detaches the HEAD
+# from the current branch, even though that may be logically
+# correct, it feels somewhat funny. More importantly, we do not
+# want "git checkout" nor "git checkout -f" to detach HEAD.
+
+detached=
+detach_warn=
+
+describe_detached_head () {
+ test -n "$quiet" || {
+ printf >&2 "$1 "
+ GIT_PAGER= git log >&2 -1 --pretty=oneline --abbrev-commit "$2" --
+ }
+}
+
+if test -z "$branch$newbranch" && test "$new_name" != "$old_name"
+then
+ detached="$new"
+ if test -n "$oldbranch" && test -z "$quiet"
+ then
+ detach_warn="Note: moving to \"$new_name\" which isn't a local branch
+If you want to create a new branch from this checkout, you may do so
+(now or later) by using -b with the checkout command again. Example:
+ git checkout -b <new_branch_name>"
+ fi
+elif test -z "$oldbranch" && test "$new" != "$old"
+then
+ describe_detached_head 'Previous HEAD position was' "$old"
+fi
+
+if [ "X$old" = X ]
+then
+ if test -z "$quiet"
+ then
+ echo >&2 "warning: You appear to be on a branch yet to be born."
+ echo >&2 "warning: Forcing checkout of $new_name."
+ fi
+ force=1
+fi
+
+if [ "$force" ]
+then
+ git read-tree $v --reset -u $new
+else
+ git update-index --refresh >/dev/null
+ git read-tree $v -m -u --exclude-per-directory=.gitignore $old $new || (
+ case "$merge,$v" in
+ ,*)
+ exit 1 ;;
+ 1,)
+ ;; # quiet
+ *)
+ echo >&2 "Falling back to 3-way merge..." ;;
+ esac
+
+ # Match the index to the working tree, and do a three-way.
+ git diff-files --name-only | git update-index --remove --stdin &&
+ work=`git write-tree` &&
+ git read-tree $v --reset -u $new || exit
+
+ eval GITHEAD_$new='${new_name:-${branch:-$new}}' &&
+ eval GITHEAD_$work=local &&
+ export GITHEAD_$new GITHEAD_$work &&
+ git merge-recursive $old -- $new $work
+
+ # Do not register the cleanly merged paths in the index yet.
+ # this is not a real merge before committing, but just carrying
+ # the working tree changes along.
+ unmerged=`git ls-files -u`
+ git read-tree $v --reset $new
+ case "$unmerged" in
+ '') ;;
+ *)
+ (
+ z40=0000000000000000000000000000000000000000
+ echo "$unmerged" |
+ sed -e 's/^[0-7]* [0-9a-f]* /'"0 $z40 /"
+ echo "$unmerged"
+ ) | git update-index --index-info
+ ;;
+ esac
+ exit 0
+ )
+ saved_err=$?
+ if test "$saved_err" = 0 && test -z "$quiet"
+ then
+ git diff-index --name-status "$new"
+ fi
+ (exit $saved_err)
+fi
+
+#
+# Switch the HEAD pointer to the new branch if we
+# checked out a branch head, and remove any potential
+# old MERGE_HEAD's (subsequent commits will clearly not
+# be based on them, since we re-set the index)
+#
+if [ "$?" -eq 0 ]; then
+ if [ "$newbranch" ]; then
+ git branch $track $newbranch_log "$newbranch" "$new_name" || exit
+ branch="$newbranch"
+ fi
+ if test -n "$branch"
+ then
+ old_branch_name=`expr "z$oldbranch" : 'zrefs/heads/\(.*\)'`
+ GIT_DIR="$GIT_DIR" git symbolic-ref -m "checkout: moving from ${old_branch_name:-$old} to $branch" HEAD "refs/heads/$branch"
+ if test -n "$quiet"
+ then
+ true # nothing
+ elif test "refs/heads/$branch" = "$oldbranch"
+ then
+ echo >&2 "Already on branch \"$branch\""
+ else
+ echo >&2 "Switched to${newbranch:+ a new} branch \"$branch\""
+ fi
+ elif test -n "$detached"
+ then
+ old_branch_name=`expr "z$oldbranch" : 'zrefs/heads/\(.*\)'`
+ git update-ref --no-deref -m "checkout: moving from ${old_branch_name:-$old} to $arg" HEAD "$detached" ||
+ die "Cannot detach HEAD"
+ if test -n "$detach_warn"
+ then
+ echo >&2 "$detach_warn"
+ fi
+ describe_detached_head 'HEAD is now at' HEAD
+ fi
+ rm -f "$GIT_DIR/MERGE_HEAD"
+else
+ exit 1
+fi
+
+# Run a post-checkout hook
+if test -x "$GIT_DIR"/hooks/post-checkout; then
+ "$GIT_DIR"/hooks/post-checkout $old $new 1
+fi
diff --git a/contrib/examples/git-remote.perl b/contrib/examples/git-remote.perl
new file mode 100755
index 0000000000..b30ed734e7
--- /dev/null
+++ b/contrib/examples/git-remote.perl
@@ -0,0 +1,477 @@
+#!/usr/bin/perl -w
+
+use strict;
+use Git;
+my $git = Git->repository();
+
+sub add_remote_config {
+ my ($hash, $name, $what, $value) = @_;
+ if ($what eq 'url') {
+ # Having more than one is Ok -- it is used for push.
+ if (! exists $hash->{'URL'}) {
+ $hash->{$name}{'URL'} = $value;
+ }
+ }
+ elsif ($what eq 'fetch') {
+ $hash->{$name}{'FETCH'} ||= [];
+ push @{$hash->{$name}{'FETCH'}}, $value;
+ }
+ elsif ($what eq 'push') {
+ $hash->{$name}{'PUSH'} ||= [];
+ push @{$hash->{$name}{'PUSH'}}, $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*(.*)$/) {
+ $it->{'PUSH'} ||= [];
+ push @{$it->{'PUSH'}}, $1;
+ }
+ 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(config --get-regexp), '^remote\.');
+ };
+ for (@remotes) {
+ if (/^remote\.(\S+?)\.([^.\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(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 list_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, $_;
+ }
+ }
+ return \@new, \@stale, \@tracked;
+}
+
+sub list_mapping {
+ my ($name, $info) = @_;
+ my $fetch = $info->{'FETCH'};
+ my $ls = $info->{'LS_REMOTE'};
+ my (@new, @stale, @tracked);
+
+ for (@$fetch) {
+ next unless (/(\+)?([^:]+):(.*)/);
+ my ($forced, $theirs, $ours) = ($1, $2, $3);
+ if ($theirs eq 'refs/heads/*' &&
+ $ours =~ /^refs\/remotes\/(.*)\/\*$/) {
+ # wildcard mapping
+ my ($w_new, $w_stale, $w_tracked)
+ = list_wildcard_mapping($forced, $1, $ls);
+ push @new, @$w_new;
+ push @stale, @$w_stale;
+ push @tracked, @$w_tracked;
+ }
+ 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;
+ }
+ }
+ }
+ return \@new, \@stale, \@tracked;
+}
+
+sub show_mapping {
+ my ($name, $info) = @_;
+ my ($new, $stale, $tracked) = list_mapping($name, $info);
+ if (@$new) {
+ print " New remote branches (next fetch will store in remotes/$name)\n";
+ print " @$new\n";
+ }
+ if (@$stale) {
+ print " Stale tracking branches in remotes/$name (use 'git remote prune')\n";
+ print " @$stale\n";
+ }
+ if (@$tracked) {
+ print " Tracked remote branches\n";
+ print " @$tracked\n";
+ }
+}
+
+sub prune_remote {
+ my ($name, $ls_remote) = @_;
+ if (!exists $remote->{$name}) {
+ print STDERR "No such remote $name\n";
+ return 1;
+ }
+ my $info = $remote->{$name};
+ update_ls_remote($ls_remote, $info);
+
+ my ($new, $stale, $tracked) = list_mapping($name, $info);
+ my $prefix = "refs/remotes/$name";
+ foreach my $to_prune (@$stale) {
+ my @v = $git->command(qw(rev-parse --verify), "$prefix/$to_prune");
+ $git->command(qw(update-ref -d), "$prefix/$to_prune", $v[0]);
+ }
+ return 0;
+}
+
+sub show_remote {
+ my ($name, $ls_remote) = @_;
+ if (!exists $remote->{$name}) {
+ print STDERR "No such remote $name\n";
+ return 1;
+ }
+ 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 unless (defined $branch->{$branchname}{'REMOTE'} &&
+ $branch->{$branchname}{'REMOTE'} eq $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);
+ }
+ if ($info->{'PUSH'}) {
+ my @pushed = map {
+ s|^refs/heads/||;
+ s|^\+refs/heads/|+|;
+ s|:refs/heads/|:|;
+ $_;
+ } @{$info->{'PUSH'}};
+ print " Local branch(es) pushed with 'git push'\n";
+ print " @pushed\n";
+ }
+ return 0;
+}
+
+sub add_remote {
+ my ($name, $url, $opts) = @_;
+ if (exists $remote->{$name}) {
+ print STDERR "remote $name already exists.\n";
+ exit(1);
+ }
+ $git->command('config', "remote.$name.url", $url);
+ my $track = $opts->{'track'} || ["*"];
+
+ for (@$track) {
+ $git->command('config', '--add', "remote.$name.fetch",
+ $opts->{'mirror'} ?
+ "+refs/$_:refs/$_" :
+ "+refs/heads/$_:refs/remotes/$name/$_");
+ }
+ if ($opts->{'fetch'}) {
+ $git->command('fetch', $name);
+ }
+ if (exists $opts->{'master'}) {
+ $git->command('symbolic-ref', "refs/remotes/$name/HEAD",
+ "refs/remotes/$name/$opts->{'master'}");
+ }
+}
+
+sub update_remote {
+ my ($name) = @_;
+ my @remotes;
+
+ my $conf = $git->config("remotes." . $name);
+ if (defined($conf)) {
+ @remotes = split(' ', $conf);
+ } elsif ($name eq 'default') {
+ @remotes = ();
+ for (sort keys %$remote) {
+ my $do_fetch = $git->config_bool("remote." . $_ .
+ ".skipDefaultUpdate");
+ unless ($do_fetch) {
+ push @remotes, $_;
+ }
+ }
+ } else {
+ print STDERR "Remote group $name does not exists.\n";
+ exit(1);
+ }
+ for (@remotes) {
+ print "Updating $_\n";
+ $git->command('fetch', "$_");
+ }
+}
+
+sub rm_remote {
+ my ($name) = @_;
+ if (!exists $remote->{$name}) {
+ print STDERR "No such remote $name\n";
+ return 1;
+ }
+
+ $git->command('config', '--remove-section', "remote.$name");
+
+ eval {
+ my @trackers = $git->command('config', '--get-regexp',
+ 'branch.*.remote', $name);
+ for (@trackers) {
+ /^branch\.(.*)?\.remote/;
+ $git->config('--unset', "branch.$1.remote");
+ $git->config('--unset', "branch.$1.merge");
+ }
+ };
+
+ my @refs = $git->command('for-each-ref',
+ '--format=%(refname) %(objectname)', "refs/remotes/$name");
+ for (@refs) {
+ my ($ref, $object) = split;
+ $git->command(qw(update-ref -d), $ref, $object);
+ }
+ return 0;
+}
+
+sub add_usage {
+ print STDERR "Usage: git remote add [-f] [-t track]* [-m master] <name> <url>\n";
+ exit(1);
+}
+
+my $VERBOSE = 0;
+@ARGV = grep {
+ if ($_ eq '-v' or $_ eq '--verbose') {
+ $VERBOSE=1;
+ 0
+ } else {
+ 1
+ }
+} @ARGV;
+
+if (!@ARGV) {
+ for (sort keys %$remote) {
+ print "$_";
+ print "\t$remote->{$_}->{URL}" if $VERBOSE;
+ 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 <remote>\n";
+ exit(1);
+ }
+ my $status = 0;
+ for (; $i < @ARGV; $i++) {
+ $status |= show_remote($ARGV[$i], $ls_remote);
+ }
+ exit($status);
+}
+elsif ($ARGV[0] eq 'update') {
+ if (@ARGV <= 1) {
+ update_remote("default");
+ exit(1);
+ }
+ for (my $i = 1; $i < @ARGV; $i++) {
+ update_remote($ARGV[$i]);
+ }
+}
+elsif ($ARGV[0] eq 'prune') {
+ 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 prune <remote>\n";
+ exit(1);
+ }
+ my $status = 0;
+ for (; $i < @ARGV; $i++) {
+ $status |= prune_remote($ARGV[$i], $ls_remote);
+ }
+ exit($status);
+}
+elsif ($ARGV[0] eq 'add') {
+ my %opts = ();
+ while (1 < @ARGV && $ARGV[1] =~ /^-/) {
+ my $opt = $ARGV[1];
+ shift @ARGV;
+ if ($opt eq '-f' || $opt eq '--fetch') {
+ $opts{'fetch'} = 1;
+ next;
+ }
+ if ($opt eq '-t' || $opt eq '--track') {
+ if (@ARGV < 1) {
+ add_usage();
+ }
+ $opts{'track'} ||= [];
+ push @{$opts{'track'}}, $ARGV[1];
+ shift @ARGV;
+ next;
+ }
+ if ($opt eq '-m' || $opt eq '--master') {
+ if ((@ARGV < 1) || exists $opts{'master'}) {
+ add_usage();
+ }
+ $opts{'master'} = $ARGV[1];
+ shift @ARGV;
+ next;
+ }
+ if ($opt eq '--mirror') {
+ $opts{'mirror'} = 1;
+ next;
+ }
+ add_usage();
+ }
+ if (@ARGV != 3) {
+ add_usage();
+ }
+ add_remote($ARGV[1], $ARGV[2], \%opts);
+}
+elsif ($ARGV[0] eq 'rm') {
+ if (@ARGV <= 1) {
+ print STDERR "Usage: git remote rm <remote>\n";
+ exit(1);
+ }
+ exit(rm_remote($ARGV[1]));
+}
+else {
+ print STDERR "Usage: git remote\n";
+ print STDERR " git remote add <name> <url>\n";
+ print STDERR " git remote rm <name>\n";
+ print STDERR " git remote show <name>\n";
+ print STDERR " git remote prune <name>\n";
+ print STDERR " git remote update [group]\n";
+ exit(1);
+}
diff --git a/contrib/examples/git-rerere.perl b/contrib/examples/git-rerere.perl
new file mode 100755
index 0000000000..4f692091e7
--- /dev/null
+++ b/contrib/examples/git-rerere.perl
@@ -0,0 +1,284 @@
+#!/usr/bin/perl
+#
+# REuse REcorded REsolve. This tool records a conflicted automerge
+# result and its hand resolution, and helps to resolve future
+# automerge that results in the same conflict.
+#
+# To enable this feature, create a directory 'rr-cache' under your
+# .git/ directory.
+
+use Digest;
+use File::Path;
+use File::Copy;
+
+my $git_dir = $::ENV{GIT_DIR} || ".git";
+my $rr_dir = "$git_dir/rr-cache";
+my $merge_rr = "$git_dir/rr-cache/MERGE_RR";
+
+my %merge_rr = ();
+
+sub read_rr {
+ if (!-f $merge_rr) {
+ %merge_rr = ();
+ return;
+ }
+ my $in;
+ local $/ = "\0";
+ open $in, "<$merge_rr" or die "$!: $merge_rr";
+ while (<$in>) {
+ chomp;
+ my ($name, $path) = /^([0-9a-f]{40})\t(.*)$/s;
+ $merge_rr{$path} = $name;
+ }
+ close $in;
+}
+
+sub write_rr {
+ my $out;
+ open $out, ">$merge_rr" or die "$!: $merge_rr";
+ for my $path (sort keys %merge_rr) {
+ my $name = $merge_rr{$path};
+ print $out "$name\t$path\0";
+ }
+ close $out;
+}
+
+sub compute_conflict_name {
+ my ($path) = @_;
+ my @side = ();
+ my $in;
+ open $in, "<$path" or die "$!: $path";
+
+ my $sha1 = Digest->new("SHA-1");
+ my $hunk = 0;
+ while (<$in>) {
+ if (/^<<<<<<< .*/) {
+ $hunk++;
+ @side = ([], undef);
+ }
+ elsif (/^=======$/) {
+ $side[1] = [];
+ }
+ elsif (/^>>>>>>> .*/) {
+ my ($one, $two);
+ $one = join('', @{$side[0]});
+ $two = join('', @{$side[1]});
+ if ($two le $one) {
+ ($one, $two) = ($two, $one);
+ }
+ $sha1->add($one);
+ $sha1->add("\0");
+ $sha1->add($two);
+ $sha1->add("\0");
+ @side = ();
+ }
+ elsif (@side == 0) {
+ next;
+ }
+ elsif (defined $side[1]) {
+ push @{$side[1]}, $_;
+ }
+ else {
+ push @{$side[0]}, $_;
+ }
+ }
+ close $in;
+ return ($sha1->hexdigest, $hunk);
+}
+
+sub record_preimage {
+ my ($path, $name) = @_;
+ my @side = ();
+ my ($in, $out);
+ open $in, "<$path" or die "$!: $path";
+ open $out, ">$name" or die "$!: $name";
+
+ while (<$in>) {
+ if (/^<<<<<<< .*/) {
+ @side = ([], undef);
+ }
+ elsif (/^=======$/) {
+ $side[1] = [];
+ }
+ elsif (/^>>>>>>> .*/) {
+ my ($one, $two);
+ $one = join('', @{$side[0]});
+ $two = join('', @{$side[1]});
+ if ($two le $one) {
+ ($one, $two) = ($two, $one);
+ }
+ print $out "<<<<<<<\n";
+ print $out $one;
+ print $out "=======\n";
+ print $out $two;
+ print $out ">>>>>>>\n";
+ @side = ();
+ }
+ elsif (@side == 0) {
+ print $out $_;
+ }
+ elsif (defined $side[1]) {
+ push @{$side[1]}, $_;
+ }
+ else {
+ push @{$side[0]}, $_;
+ }
+ }
+ close $out;
+ close $in;
+}
+
+sub find_conflict {
+ my $in;
+ local $/ = "\0";
+ my $pid = open($in, '-|');
+ die "$!" unless defined $pid;
+ if (!$pid) {
+ exec(qw(git ls-files -z -u)) or die "$!: ls-files";
+ }
+ my %path = ();
+ my @path = ();
+ while (<$in>) {
+ chomp;
+ my ($mode, $sha1, $stage, $path) =
+ /^([0-7]+) ([0-9a-f]{40}) ([123])\t(.*)$/s;
+ $path{$path} |= (1 << $stage);
+ }
+ close $in;
+ while (my ($path, $status) = each %path) {
+ if ($status == 14) { push @path, $path; }
+ }
+ return @path;
+}
+
+sub merge {
+ my ($name, $path) = @_;
+ record_preimage($path, "$rr_dir/$name/thisimage");
+ unless (system('git', 'merge-file', map { "$rr_dir/$name/${_}image" }
+ qw(this pre post))) {
+ my $in;
+ open $in, "<$rr_dir/$name/thisimage" or
+ die "$!: $name/thisimage";
+ my $out;
+ open $out, ">$path" or die "$!: $path";
+ while (<$in>) { print $out $_; }
+ close $in;
+ close $out;
+ return 1;
+ }
+ return 0;
+}
+
+sub garbage_collect_rerere {
+ # We should allow specifying these from the command line and
+ # that is why the caller gives @ARGV to us, but I am lazy.
+
+ my $cutoff_noresolve = 15; # two weeks
+ my $cutoff_resolve = 60; # two months
+ my @to_remove;
+ while (<$rr_dir/*/preimage>) {
+ my ($dir) = /^(.*)\/preimage$/;
+ my $cutoff = ((-f "$dir/postimage")
+ ? $cutoff_resolve
+ : $cutoff_noresolve);
+ my $age = -M "$_";
+ if ($cutoff <= $age) {
+ push @to_remove, $dir;
+ }
+ }
+ if (@to_remove) {
+ rmtree(\@to_remove);
+ }
+}
+
+-d "$rr_dir" || exit(0);
+
+read_rr();
+
+if (@ARGV) {
+ my $arg = shift @ARGV;
+ if ($arg eq 'clear') {
+ for my $path (keys %merge_rr) {
+ my $name = $merge_rr{$path};
+ if (-d "$rr_dir/$name" &&
+ ! -f "$rr_dir/$name/postimage") {
+ rmtree(["$rr_dir/$name"]);
+ }
+ }
+ unlink $merge_rr;
+ }
+ elsif ($arg eq 'status') {
+ for my $path (keys %merge_rr) {
+ print $path, "\n";
+ }
+ }
+ elsif ($arg eq 'diff') {
+ for my $path (keys %merge_rr) {
+ my $name = $merge_rr{$path};
+ system('diff', ((@ARGV == 0) ? ('-u') : @ARGV),
+ '-L', "a/$path", '-L', "b/$path",
+ "$rr_dir/$name/preimage", $path);
+ }
+ }
+ elsif ($arg eq 'gc') {
+ garbage_collect_rerere(@ARGV);
+ }
+ else {
+ die "$0 unknown command: $arg\n";
+ }
+ exit 0;
+}
+
+my %conflict = map { $_ => 1 } find_conflict();
+
+# MERGE_RR records paths with conflicts immediately after merge
+# failed. Some of the conflicted paths might have been hand resolved
+# in the working tree since then, but the initial run would catch all
+# and register their preimages.
+
+for my $path (keys %conflict) {
+ # This path has conflict. If it is not recorded yet,
+ # record the pre-image.
+ if (!exists $merge_rr{$path}) {
+ my ($name, $hunk) = compute_conflict_name($path);
+ next unless ($hunk);
+ $merge_rr{$path} = $name;
+ if (! -d "$rr_dir/$name") {
+ mkpath("$rr_dir/$name", 0, 0777);
+ print STDERR "Recorded preimage for '$path'\n";
+ record_preimage($path, "$rr_dir/$name/preimage");
+ }
+ }
+}
+
+# Now some of the paths that had conflicts earlier might have been
+# hand resolved. Others may be similar to a conflict already that
+# was resolved before.
+
+for my $path (keys %merge_rr) {
+ my $name = $merge_rr{$path};
+
+ # We could resolve this automatically if we have images.
+ if (-f "$rr_dir/$name/preimage" &&
+ -f "$rr_dir/$name/postimage") {
+ if (merge($name, $path)) {
+ print STDERR "Resolved '$path' using previous resolution.\n";
+ # Then we do not have to worry about this path
+ # anymore.
+ delete $merge_rr{$path};
+ next;
+ }
+ }
+
+ # Let's see if we have resolved it.
+ (undef, my $hunk) = compute_conflict_name($path);
+ next if ($hunk);
+
+ print STDERR "Recorded resolution for '$path'.\n";
+ copy($path, "$rr_dir/$name/postimage");
+ # And we do not have to worry about this path anymore.
+ delete $merge_rr{$path};
+}
+
+# Write out the rest.
+write_rr();