{"thread":{"id":"4859","subject":"[RFC] New REuse REcorded REsolve tool.","startedAt":"2006-07-12T09:33:14Z","lastAt":"2006-07-12T09:33:14Z","messageCount":1,"participants":["Shawn Pearce"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"23699","messageId":"20060712093314.GA4571@spearce.org","threadId":"4859","inReplyTo":null,"subject":"[RFC] New REuse REcorded REsolve tool.","fromName":"Shawn Pearce","fromEmail":"spearce@spearce.org","sentAt":"2006-07-12T09:33:14Z","receivedAt":"2006-07-12T09:33:14Z","isPatch":false,"sender":{"key":"spearce@spearce.org","avatar":"https://avatars.githubusercontent.com/u/34844?v=4"},"body":"What follows is a draft implementation of a different git-rerere tool.\n\nPlease use caution when testing this on any real repository.  It has\nbeen very lightly tested and only for a small handful of cases.\nIts probably going to fail on something that I haven't tried yet.\nIt could overwrite entries in your current index, making it possible\nto commit a file with <<<< === >>>> conflict markers in it.  :-)\n\nMy goal with this tool is to get something that git-merge can use\nas a pre-filter to the current git-rerere and by which git-merge\ncan automatically reuse prior resolutions on a more predictable\nbasis then what happens with git-rerere today.  Ideally I want\ngit-rerere2 to make the merge commit for me if it was successful\nat resolving all unmerged entries and thus have git-merge return\nwith an exit code of 0 rather than 1.\n\nLots of TODOs still exist:\n- Prune cache at some threshold in size/age.\n- Anchor prior recorded merge commits along a special ref (to prevent\n  them from pruning while they are still in the cache).\n- Automatically create merge commit if successfully resolved.\n- Test cases.\n- More/better documentation.\n- Friendly error handling on invalid command line option combinations.\n\nAnd all of the above means no signoff yet.   :-)\n\n-->8--\nNew REuse REcorded REsolve tool.\n\ngit-rerere2 attempts to remember lines of development that have been\nmerged together before by tracking merge commits in .git/rr2-cache.\nWhen two lines of development are merged together again in the\nfuture git-rerere2 searches its cache for the most recent merge\ncommit between the two branches and tries to select the merge commit\nthat contains the maximum number of commits from each branch.\n\nIf such a commit exists in the cache then git-rerere2 attempts\nto resolve any unmerged index entries by comparing the conflicted\nstage 1/2/3 from the chosen prior merge against the current index;\nif stage 1 is unchanged and stage 2/3 are unchanged (but possibly\npermuted in order) then the working directory (and current index)\nare updated with resolved file as committed in the chosen commit.\n\nUnlike git-rerere, git-rerere2 can remember and reuse structual\nmodifications (adds/deletes, mode changes, type changes) as well as\nreuse binary file resolutions which the standard RCS `merge` tool\nwouldn't have been able to process.  However, also unlike git-rerere,\ngit-rerere2 does not attempt to merge the contents of a file.\n---\n .gitignore       |    1 \n Makefile         |    2 \n git-commit.sh    |    4 +\n git-merge.sh     |    4 +\n git-rerere2.perl |  281 ++++++++++++++++++++++++++++++++++++++++++++++++++++++\n 5 files changed, 291 insertions(+), 1 deletions(-)\n\ndiff --git a/.gitignore b/.gitignore\nindex fb0fa3f..1a3ed42 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -87,6 +87,7 @@ git-repack\n git-repo-config\n git-request-pull\n git-rerere\n+git-rerere2\n git-reset\n git-resolve\n git-rev-list\ndiff --git a/Makefile b/Makefile\nindex 5b7bac8..e1fa96a 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -147,7 +147,7 @@ SCRIPT_SH = \\\n \n SCRIPT_PERL = \\\n \tgit-archimport.perl git-cvsimport.perl git-relink.perl \\\n-\tgit-shortlog.perl git-rerere.perl \\\n+\tgit-shortlog.perl git-rerere.perl git-rerere2.perl \\\n \tgit-annotate.perl git-cvsserver.perl \\\n \tgit-svnimport.perl git-mv.perl git-cvsexportcommit.perl \\\n \tgit-send-email.perl git-svn.perl\ndiff --git a/git-commit.sh b/git-commit.sh\nindex 802dd72..3451072 100755\n--- a/git-commit.sh\n+++ b/git-commit.sh\n@@ -742,6 +742,10 @@ else\n fi\n ret=\"$?\"\n rm -f \"$GIT_DIR/COMMIT_MSG\" \"$GIT_DIR/COMMIT_EDITMSG\" \"$GIT_DIR/SQUASH_MSG\"\n+if test -d \"$GIT_DIR/rr2-cache\"\n+then\n+\tgit-rerere2 --record\n+fi\n if test -d \"$GIT_DIR/rr-cache\"\n then\n \tgit-rerere\ndiff --git a/git-merge.sh b/git-merge.sh\nindex a9cfafb..53a907b 100755\n--- a/git-merge.sh\n+++ b/git-merge.sh\n@@ -375,6 +375,10 @@ Conflicts:\n \t\tsed -e 's/^[^\t]*\t/\t/' |\n \t\tuniq\n \t} >>\"$GIT_DIR/MERGE_MSG\"\n+\tif test -d \"$GIT_DIR/rr2-cache\"\n+\tthen\n+\t\tgit-rerere2 --reset --remember-unmerged --reuse \"$head_arg\" \"$@\"\n+\tfi\n \tif test -d \"$GIT_DIR/rr-cache\"\n \tthen\n \t\tgit-rerere\ndiff --git a/git-rerere2.perl b/git-rerere2.perl\nnew file mode 100644\nindex 0000000..8925faf\n--- /dev/null\n+++ b/git-rerere2.perl\n@@ -0,0 +1,281 @@\n+#!/usr/bin/perl\n+#\n+# REuse REcorded REsolve.  This tool records a conflicted automerge\n+# result and its hand resolution, and helps to resolve future\n+# automerge that results in the same conflict.\n+#\n+\n+use strict;\n+use warnings;\n+use Getopt::Long;\n+use Pod::Usage;\n+use File::Spec::Functions qw(catfile);\n+use Git;\n+\n+my $repo = Git->repository;\n+my $cache_dir = catfile($repo->repo_path, 'rr2-cache');\n+my $cache_toc = catfile($cache_dir, 'contents');\n+my $tmp_lst = catfile($cache_dir, 'current_unmerged');\n+\n+my ($enable, $disable,\n+\t$reset, $remember_unmerged,\n+\t$reuse, $dry_run, $no_update,\n+\t$record,\n+\t$man, $help);\n+GetOptions(\n+\t\"enable\" => \\$enable,\n+\t\"disable\" => \\$disable,\n+\t\"reset\" => \\$reset,\n+\t\"remember-unmerged\" => \\$remember_unmerged,\n+\t\"reuse\" => \\$reuse,\n+\t\"n|dry-run\" =>, \\$dry_run,\n+\t\"no-update\" =>, \\$no_update,\n+\t\"record\" => \\$record,\n+\t\"man\" => \\$man,\n+\t\"help\" => \\$help,\n+);\n+pod2usage(1) if $help;\n+pod2usage(-exitstatus => 0, -verbose => 2) if $man;\n+\n+if ($enable) {\n+\tmkdir $cache_dir, 0777;\n+\tdie \"Failed to create $cache_dir: $!\\n\" unless -d $cache_dir;\n+\tprint \"Created $cache_dir\\n\";\n+\texit 0;\n+}\n+exit unless -d $cache_dir;\n+\n+my (%unmerged);\n+sub load_unmerged ($$);\n+sub save_unmerged ($$);\n+\n+if ($reset) {\n+\tunlink $tmp_lst;\n+} else {\n+\tif (open(T, '<',$tmp_lst)) {\n+\t\tload_unmerged \\*T, \\%unmerged;\n+\t\tclose T;\n+\t}\n+}\n+\n+if ($remember_unmerged) {\n+\tmy ($f,$c) = $repo->command_output_pipe(qw(ls-files -z --unmerged));\n+\tload_unmerged $f, \\%unmerged;\n+\t$repo->command_close_pipe($f,$c);\n+\n+\tif (%unmerged) {\n+\t\topen(T, '>',$tmp_lst) or die \"Can't overwrite $tmp_lst: $!\\n\";\n+\t\tsave_unmerged \\*T, \\%unmerged;\n+\t\tclose T;\n+\t} else {\n+\t\tunlink $tmp_lst;\n+\t}\n+}\n+\n+if ($reuse && !$record && @ARGV > 1 && %unmerged) {\n+\tmy %cache_by_parent;\n+\tif (open(T,'<',$cache_toc)) {\n+\t\twhile (<T>) {\n+\t\t\tchop;\n+\t\t\tmy ($then, $result, @parents) = split / /;\n+\t\t\tmy $e = [$then, $result, @parents];\n+\t\t\tpush @{$cache_by_parent{$_}}, $e foreach @parents;\n+\t\t}\n+\t\tclose T;\n+\t}\n+\n+\tmy $base = $repo->command_oneline('merge-base',@ARGV);\n+\tmy %possib_results;\n+\tforeach my $r (@ARGV) {\n+\t\tmy ($f,$c) = $repo->command_output_pipe('rev-list',\"^$base\",$r);\n+\t\twhile (<$f>) {\n+\t\t\tchop;\n+\t\t\tmy $e_arr = $cache_by_parent{$_};\n+\t\t\tnext unless $e_arr;\n+\t\t\t$possib_results{$_->[1]}++ foreach @$e_arr;\n+\t\t}\n+\t\t$repo->command_close_pipe($f,$c);\n+\t}\n+\tmy @p = sort {$b->[0] <=> $a->[0]}\n+\t\tgrep {$possib_results{$_} == @ARGV} keys %possib_results;\n+\tmy $best = $p[0];\n+\n+\tmy @reuse;\n+\tif (open(T, '<',catfile($cache_dir, $best))) {\n+\t\tmy %best_conflicts;\n+\t\t$_ = <T>;\n+\t\tload_unmerged \\*T, \\%best_conflicts;\n+\t\tclose T;\n+\n+\t\tprint \"Trying to reuse resolution(s) from $best.\\n\";\n+\t\tmy ($name, $u);\n+\t\twhile (($name,$u) = each %unmerged) {\n+\t\t\tmy $b = $best_conflicts{$name};\n+\t\t\tnext unless $b;  # no prior resolution\n+\t\t\tnext unless $u->[1] eq $b->[1];  # different base\n+\t\t\tnext unless (($u->[2] eq $b->[2] && $u->[3] eq $b->[3])\n+\t\t\t\t|| ($u->[2] eq $b->[3] && $u->[3] eq $b->[2]));\n+\n+\t\t\tpush @reuse, $name;\n+\t\t}\n+\t}\n+\n+\tif (@reuse) {\n+\t\tprint \"Reuse \",$_,\"\\n\" foreach @reuse;\n+\t\tprint \"\\n\";\n+\n+\t\tif ($dry_run) {\n+\t\t\tprint \"Skipped checkout (--dry-run enabled).\\n\";\n+\t\t} else {\n+\t\t\tprint \"Checking out resolved versions...\\n\";\n+\t\t\tmy $idx = catfile($repo->repo_path,'rerere2idx');\n+\t\t\tlocal $ENV{GIT_INDEX_FILE} = $idx;\n+\t\t\t$repo->command(qw(read-tree --reset),$best);\n+\t\t\tmy ($f,$c) = $repo->command_input_pipe(\n+\t\t\t\tqw(checkout-index --force -z --stdin));\n+\t\t\tprint $f $_, \"\\0\" foreach @reuse;\n+\t\t\t$repo->command_close_pipe($f,$c);\n+\t\t\tunlink $idx;\n+\t\t}\n+\t\tdelete $unmerged{$_} foreach @reuse;\n+\n+\t\tif ($dry_run) {\n+\t\t\tprint \"Skipped index update (--dry-run enabled).\\n\";\n+\t\t} elsif ($no_update) {\n+\t\t\tprint \"Skipped index update (--no-update enabled).\\n\";\n+\t\t} else {\n+\t\t\tprint \"Updating index with resolved versions...\\n\";\n+\t\t\tmy ($f,$c) = $repo->command_input_pipe(\n+\t\t\t\tqw(update-index -z --stdin));\n+\t\t\tprint $f $_, \"\\0\" foreach @reuse;\n+\t\t\t$repo->command_close_pipe($f,$c);\n+\t\t}\n+\n+\t\tif (%unmerged) {\n+\t\t\tunless ($dry_run) {\n+\t\t\t\topen(T, '>',$tmp_lst)\n+\t\t\t\t\tor die \"Can't overwrite $tmp_lst: $!\\n\";\n+\t\t\t\tsave_unmerged \\*T, \\%unmerged;\n+\t\t\t\tclose T;\n+\t\t\t}\n+\t\t\tprint \"Some conflicts remain.\\n\";\n+\t\t\texit 1;\n+\t\t} else {\n+\t\t\tunlink $tmp_lst unless $dry_run;\n+\t\t\tprint \"Everything resolved.\\n\";\n+\t\t\texit 0;\n+\t\t}\n+\t} else {\n+\t\tprint \"No matching resolutions available for reuse.\\n\";\n+\t}\n+\texit 1;\n+}\n+\n+if (!$reuse && $record && %unmerged) {\n+\tmy $head = $repo->command_oneline(qw(rev-parse --verify HEAD));\n+\tmy $now = time;\n+\n+\tmy @parents;\n+\tmy ($f,$c) = $repo->command_output_pipe(qw(cat-file commit HEAD));\n+\twhile (<$f>) {\n+\t\tpush @parents, $1 if /^parent ([0-9a-f]{40})/;\n+\t\tlast if /^author /;\n+\t}\n+\t$repo->command_close_pipe($f,$c);\n+\n+\tunlink $tmp_lst;\n+\texit unless @parents > 1;\n+\n+\topen(T, '>',catfile($cache_dir,$head)) or die \"Can't cache: $!\\n\";\n+\tprint T $now,' ',$head,' ',join(' ', @parents),\"\\n\";\n+\tsave_unmerged \\*T, \\%unmerged;\n+\tclose T;\n+\n+\tmy %toc;\n+\tif (open(T,'<',$cache_toc)) {\n+\t\twhile (<T>) {\n+\t\t\tchop;\n+\t\t\tmy ($then, $result, $ps) = split / /, $_, 3;\n+\t\t\t$toc{$ps} = [$then, $result, $ps];\n+\t\t}\n+\t\tclose T;\n+\t}\n+\n+\tmy $ps = join(' ', sort @parents);\n+\t$toc{$ps} = [$now, $head, $ps];\n+\n+\topen(T, '>',$cache_toc) or die \"Can't update $cache_toc: $!\\n\";\n+\tforeach my $e (sort {$a->[0] <=> $b->[1]} values %toc) {\n+\t\tprint T join(' ', @$e), \"\\n\";\n+\t}\n+\tclose T;\n+}\n+\n+exit;\n+\n+sub load_unmerged ($$) {\n+\tmy ($f, $um) = @_;\n+\tlocal $_;\n+\tlocal $/ = \"\\0\";\n+\tmy $cur_name;\n+\tmy @cur_stages = ('', '', '', '');\n+\twhile ($_ = <$f>) {\n+\t\tchomp;\n+\t\tmy ($info,$name) = split /\\t/, $_, 2;\n+\t\tif ($cur_name && $name ne $cur_name) {\n+\t\t\t$um->{$cur_name} = [@cur_stages];\n+\t\t\t@cur_stages = ('', '', '', '');\n+\t\t}\n+\t\t$cur_name = $name;\n+\t\t$info =~ / ([123])$/;\n+\t\t$cur_stages[$1] = $info;\n+\t}\n+\t$um->{$cur_name} = [@cur_stages] if $cur_name;\n+}\n+\n+sub save_unmerged ($$) {\n+\tmy ($f, $um) = @_;\n+\tforeach my $cur_name (sort keys %$um) {\n+\t\tmy $u = $um->{$cur_name};\n+\t\tforeach my $stage (1..3) {\n+\t\t\tmy $s = $u->[$stage];\n+\t\t\tnext unless $s;\n+\t\t\tprint $f $s,\"\\t\",$cur_name,\"\\0\";\n+\t\t}\n+\t}\n+}\n+\n+__END__\n+\n+=head1 NAME\n+\n+git-rerere2 - REuse REcorded REsolve\n+\n+=head1 SYNOPSIS\n+\n+  git-rerere2 --enable    ; #to activate\n+\n+=head1 OPTIONS\n+\n+=over 4\n+\n+=item B<--enable>\n+\n+Activate git-rerere2 in the current repository.\n+\n+=item B<-n> | B<--dry-run>\n+\n+Perform reuse actions in memory but don't alter the working directory\n+or the index.\n+\n+=item B<--no-update>\n+\n+Update the working directory with a reused resolution but don't\n+update the index.  This will leave the index entry unmerged with\n+all relevant stages still contained within it.  It is the caller's\n+responsibility to update the index prior to commit.\n+\n+=back\n+\n+=cut\n+\n-- \n1.4.1.gd3d5\n"}]}