{"thread":{"id":"46491","subject":"[PATCH 0/1] add git-splice subcommand for non-interactive branch splicing","startedAt":"2017-07-31T21:26:38Z","lastAt":"2017-08-01T01:14:30Z","messageCount":4,"participants":["Adam Spiers","Junio C Hamano"],"isPatch":true,"patchVersion":1,"patchTotal":1},"messages":[{"id":"325344","messageId":"cover.55495badd28b73b39c60ca4107b50aae7ee95028.1501535033.git-series.git@adamspiers.org","threadId":"46491","inReplyTo":null,"subject":"[PATCH 0/1] add git-splice subcommand for non-interactive branch splicing","fromName":"Adam Spiers","fromEmail":"git@adamspiers.org","sentAt":"2017-07-31T21:18:48Z","receivedAt":"2017-07-31T21:26:38Z","isPatch":true,"sender":{"key":"git@adamspiers.org","avatar":"https://avatars.githubusercontent.com/u/100738?v=4"},"body":"This patch adds a new subcommand called git-splice, which facilitates\nhigher-level workflow operations in the area of branch management, for\nexample moving commits from one branch into another, or decomposing\nlarge branches into smaller, independent branches, or vice-versa.\n\nMotivation\n----------\n\nBy now git is very mature, and excels at low-level plumbing and\nporcelain operations.  However, developer workflows have become\nincreasingly sophisticated, partially as a result of CI / review\nsystems supporting development and testing of large numbers of\nconcurrent patches to the same repository.  These systems can track\ndependencies / conflicts between those patches (gerrit) or group\nseries of related commmits into a single topic (GitLab merge requests;\nGitHub pull requests).\n\nThis trend towards higher-level workflows has also driven innovations\nin branch management tools (e.g. topgit, gitflow, gitwork,\ngit-series).  I expect this trend to continue, and git UIs to evolve\nwhich make re-organising commits between branches almost as easy as\nmoving files around a filesystem with a file manager tool.\n\nOf course, any git UI which aids the user in manipulating branches can\nalready automate the tasks using existing porcelain and plumbing.  But\nthis will typically require mid-level operations, such as:\n\n  1) removing commits from a branch\n  2) porting (copying) commits from one branch into another\n  3) moving commits from one branch into another\n  4) predicting when any of the above will cause conflicts\n  5) decomposing large branches into smaller, independent branches\n     in order to reduce conflicts\n\nPerforming these operations using existing porcelain is cumbersome,\ngenerally involving manual (i.e. interactive) use of git rebase -i\netc.  However, automated higher-level workflows will need these\noperations to be as *non*-interactive as possible.\n\nTherefore there is a risk that each new UI for higher-level workflows\nwill end up re-implementing these mid-level operations.  This\nundesirable situation could be avoided if git itself provided those\nmid-level operations.\n\nThis is where git-splice comes in.  It handles operations 1) and 2),\nand lays the foundation for git-transplant, another git subcommand I\nhave written which implements 3).  (See below for more info on this,\nand on other tools related to 4) and 5)).\n\nDescription\n-----------\n\ngit-splice(1) non-interactively splices the current branch by removing\na range of commits from within it and/or cherry-picking a range of\ncommits into it.\n\nIt's essentially a convenience wrapper around cherry-pick and\ninteractive rebase, but the workflow state is persisted to disk, and\nthereby supports standard --abort and --continue semantics just like\ngit's other extended workflow commands.  It also handles more complex\ncases, as briefly demonstrated by the examples below.\n\n(See git-splice.txt in the patch for full detail.)\n\nExample usage\n-------------\n\n    # Remove commits A..B (i.e. excluding A) from the current branch.\n    git splice A..B\n\n    # Remove commit A from the current branch.\n    git splice A^!\n\n    # Remove commits A..B from the current branch, and cherry-pick\n    # commits C..D at the same point.\n    git splice A..B C..D\n\n    # Cherry-pick commits C..D, splicing them in just after commit A.\n    git splice A C..D\n\n    # Remove all commits since 11am this morning mentioning \"foo\".\n    git splice --since=11am --grep=\"foo\" --\n\n    # Remove commit A and all its ancestors (including the root commit)\n    # from the current branch.\n    git splice --root A\n\n    # Abort a splice which failed during cherry-pick or rebase.\n    git splice --abort\n\n    # Resume a splice after manually fixing conflicts caused by\n    # cherry-pick or rebase.\n    git splice --continue\n\nRemaining work\n--------------\n\nThe code does not yet conform 100% to Documentation/CodingGuidelines.\nThe only known areas of non-conformance are:\n\n    1. It relies on bash arrays, which is a non-POSIX feature.\n\n    2. It does not support i18n.\n\nI would be more than happy to fix these if there is a chance of\ngit-splice being accepted for inclusion within the git distribution.\n\nI appreciate that adding a new subcommand to git automatically brings\nconcerns about the increase in maintenance burden.  ICBW but I would\nexpect git-splice to add significantly less burden than the last\nsubcommand I wrote (check-ignore), because it only relies on very\nmainstream porcelain commands and one plumbing command (update-ref).\nOTOH, the test suite makes very heavy use of git's test framework, so\nseparating it out into a separate tree would presumably be\nnon-trivial.\n\nPrevious and recent work\n------------------------\n\nI first announced git-splice just over a year ago:\n\n    https://www.spinics.net/lists/git/msg277346.html\n    https://public-inbox.org/git/20160527140811.GB11256@pacific.linksys.moosehall/\n\nI have just fixed all known remaining issues and further beefed up the\ntest suite, so I think it's now ready for serious consideration.\n(Previously I merely provided a URL to a branch, but did not actually\nsubmit a patch.)\n\nRelated tools\n-------------\n\nI have also implemented git-transplant as a further patch on top of\nthis one:\n\n    https://github.com/aspiers/git/compare/splice...aspiers:transplant\n\nand other tools which provide additional help with moving/copying\ncommits from one branch to another, and with predicting and avoiding\nany conflicts which could arise.  They are out of scope for this post,\nso I will just give links for anyone who is interested:\n\n    https://blog.adamspiers.org/2015/01/19/git-deps/\n    https://blog.adamspiers.org/2013/09/19/easier-upstreaming-with-git/\n\nAll feedback is of course very welcome!\n\nThanks,\nAdam\n\nAdam Spiers (1):\n  add git-splice command for non-interactive branch splicing\n\n .gitignore                   |   1 +-\n Documentation/git-splice.txt | 125 ++++++-\n Makefile                     |   1 +-\n git-splice.sh                | 737 ++++++++++++++++++++++++++++++++++++-\n t/t7900-splice.sh            | 630 +++++++++++++++++++++++++++++++-\n 5 files changed, 1494 insertions(+)\n create mode 100644 Documentation/git-splice.txt\n create mode 100755 git-splice.sh\n create mode 100755 t/t7900-splice.sh\n\nbase-commit: 5800c63717ae35286a1441f14ffff753e01f7e2b\n-- \ngit-series 0.9.1\n"},{"id":"325345","messageId":"c3213758552a02e233d9c173f0c52d05d2460a0f.1501535033.git-series.git@adamspiers.org","threadId":"46491","inReplyTo":"cover.55495badd28b73b39c60ca4107b50aae7ee95028.1501535033.git-series.git@adamspiers.org","subject":"[PATCH 1/1] add git-splice command for non-interactive branch splicing","fromName":"Adam Spiers","fromEmail":"git@adamspiers.org","sentAt":"2017-07-31T21:18:49Z","receivedAt":"2017-07-31T21:26:41Z","isPatch":true,"sender":{"key":"git@adamspiers.org","avatar":"https://avatars.githubusercontent.com/u/100738?v=4"},"body":"Add a new subcommand git-splice(1) which non-interactively splices the\ncurrent branch by removing a range of commits from within it and/or\ncherry-picking a range of commits into it.\n\nIt's essentially a convenience wrapper around cherry-pick and\ninteractive rebase, but the workflow state is persisted to disk, and\nthereby supports standard --abort and --continue semantics just like\ngit's other extended workflow commands.  It also handles more complex\ncases, as described in the manual page.\n\nSigned-off-by: Adam Spiers <git@adamspiers.org>\n---\n .gitignore                   |   1 +-\n Documentation/git-splice.txt | 125 ++++++-\n Makefile                     |   1 +-\n git-splice.sh                | 737 ++++++++++++++++++++++++++++++++++++-\n t/t7900-splice.sh            | 630 +++++++++++++++++++++++++++++++-\n 5 files changed, 1494 insertions(+)\n create mode 100644 Documentation/git-splice.txt\n create mode 100755 git-splice.sh\n create mode 100755 t/t7900-splice.sh\n\ndiff --git a/.gitignore b/.gitignore\nindex 833ef3b..4062009 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -150,6 +150,7 @@\n /git-show-branch\n /git-show-index\n /git-show-ref\n+/git-splice\n /git-stage\n /git-stash\n /git-status\ndiff --git a/Documentation/git-splice.txt b/Documentation/git-splice.txt\nnew file mode 100644\nindex 0000000..29f3ac8\n--- /dev/null\n+++ b/Documentation/git-splice.txt\n@@ -0,0 +1,125 @@\n+git-splice(1)\n+=============\n+\n+NAME\n+----\n+git-splice - Splice commits into/out of current branch\n+\n+SYNOPSIS\n+--------\n+[verse]\n+'git splice' <insertion point> <cherry pick range>\n+'git splice' <insertion point> \\-- <cherry pick range args ...>\n+'git splice' [-r|--root] <remove range> [<cherry pick range>]\n+'git splice' [-r|--root] <remove range args ...> \\-- [<cherry pick range args ...>]\n+'git splice' (--abort | --continue | --in-progress)\n+\n+DESCRIPTION\n+-----------\n+Non-interactively splice branch by removing a range of commits from\n+within the current branch, and/or cherry-picking a range of commits\n+into the current branch.\n+\n+<remove range> specifies the range of commits to remove from the\n+current branch, and <cherry-pick-range> specifies the range to insert\n+at the point where <remove-range> previously existed, or just after\n+<insertion-point>.\n+\n+<insertion point> is a commit-ish in the standard format accepted\n+by linkgit:git-rev-parse[1].\n+\n+<remove range> and <cherry pick range> are single shell words\n+specifying commit ranges in the standard format accepted by\n+linkgit:git-rev-list[1], e.g.\n+\n+    A..B\n+    A...B\n+    A^!   (just commit A)\n+\n+It is possible to pass multi-word specifications for both the removal\n+and insertion ranges, in which case they are passed to\n+linkgit:git-rev-list[1] to calculate the commits to remove or\n+cherry-pick.  For this you need to terminate <remove range args> with\n+`--` to indicate that the multi-word form of parameters is being used.\n+\n+When the `--root` option is present, a removal range can be specified\n+as a commit-ish in the standard format accepted by\n+linkgit:git-rev-parse[1], in which case the commit-ish is treated as a\n+range.  This makes it possible to remove or replace root\n+(i.e. parentless) commits.\n+\n+Currently git-splice assumes that all commits being operated on have a\n+single parent; removal and insertion of merge commits is not supported.\n+\n+N.B. Obviously this command rewrites history!  As with\n+linkgit:git-rebase[1], you should be aware of all the implications of\n+history rewriting before using it.  (And actually this command is just\n+a glorified wrapper around linkgit:git-cherry-pick[1] and\n+linkgit:git-rebase[1] in interactive mode.)\n+\n+OPTIONS\n+-------\n+\n+-r::\n+--root::\n+\tTreat (each) removal range argument as a commit-ish, and\n+\tremove all its ancestors.\n+\n+--abort::\n+\tAbort an in-progress splice.\n+\n+--continue::\n+\tResume an in-progress splice.\n+\n+--in-progress::\n+\tExit 0 if and only if a splice is in progress.\n+\n+EXAMPLES\n+--------\n+\n+`git splice A..B`::\n+\n+\tRemove commits A..B (i.e. excluding A) from the current branch.\n+\n+`git splice A^!`::\n+\n+\tRemove commit A from the current branch.\n+\n+`git splice --root A`::\n+\n+\tRemove commit A and all its ancestors (including the root commit)\n+\tfrom the current branch.\n+\n+`git splice A..B C..D`::\n+\n+\tRemove commits A..B from the current branch, and cherry-pick\n+\tcommits C..D at the same point.\n+\n+`git splice A C..D`::\n+\n+\tCherry-pick commits C..D, splicing them in just after commit A.\n+\n+`git splice --since=11am --grep=\"foo\" --`::\n+\n+\tRemove all commits since 11am this morning mentioning \"foo\".\n+\n+`git splice --abort`::\n+\n+\tAbort a splice which failed during cherry-pick or rebase.\n+\n+`git splice --continue`::\n+\n+\tResume a splice after manually fixing conflicts caused by\n+\tcherry-pick or rebase.\n+\n+`git splice --in-progress && git splice --abort`::\n+\n+\tAbort if there is a splice in progress.\n+\n+SEE ALSO\n+--------\n+linkgit:git-rebase[1], linkgit:git-cherry-pick[1]\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\ndiff --git a/Makefile b/Makefile\nindex 461c845..eeaabc2 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -547,6 +547,7 @@ SCRIPT_SH += git-quiltimport.sh\n SCRIPT_SH += git-rebase.sh\n SCRIPT_SH += git-remote-testgit.sh\n SCRIPT_SH += git-request-pull.sh\n+SCRIPT_SH += git-splice.sh\n SCRIPT_SH += git-stash.sh\n SCRIPT_SH += git-submodule.sh\n SCRIPT_SH += git-web--browse.sh\ndiff --git a/git-splice.sh b/git-splice.sh\nnew file mode 100755\nindex 0000000..e4f3e53\n--- /dev/null\n+++ b/git-splice.sh\n@@ -0,0 +1,737 @@\n+#!/bin/bash\n+#\n+# git-splice - splice commits into/out of current branch\n+# Copyright (c) 2016 Adam Spiers\n+#\n+# This program is free software: you can redistribute it and/or modify\n+# it under the terms of the GNU General Public License as published by\n+# the Free Software Foundation, either version 2 of the License, or\n+# (at your option) any later version.\n+#\n+# This program is distributed in the hope that it will be useful,\n+# but WITHOUT ANY WARRANTY; without even the implied warranty of\n+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n+# GNU General Public License for more details.\n+#\n+# You should have received a copy of the GNU General Public License\n+# along with this program.  If not, see <http://www.gnu.org/licenses/>.\n+#\n+# ---------------------------------------------------------------------\n+#\n+\n+dashless=$(basename \"$0\" | sed -e 's/-/ /')\n+USAGE=\"<insertion point> <cherry pick range>\n+   or: $dashless <insertion point> -- <cherry pick range args ...>\n+   or: $dashless [-r|--root] <remove range> [<cherry pick range>]\n+   or: $dashless [-r|--root] <remove range args> ... -- <cherry pick range args ...>\n+   or: $dashless (--abort | --continue | --in-progress)\"\n+LONG_USAGE=\\\n+'    -h, --help     Show this help and exit\n+     -r, root      Treat (each) removal range argument as a commit-ish, and\n+                   remove all its ancestors.\n+    --abort        Abort an in-progress splice\n+    --continue     Continue an in-progress splice\n+    --in-progress  Exit 0 if and only if a splice is in progress'\n+\n+OPTIONS_SPEC=\n+. git-sh-setup\n+\n+export PS4=\"+\\${BASH_SOURCE/\\$HOME/\\~}@\\${LINENO}(\\${FUNCNAME[0]}): \"\n+\n+me=$(basename $0)\n+git_dir=$(git rev-parse --git-dir) || exit 1\n+splice_dir=\"$git_dir/splice\"\n+base_file=\"$splice_dir/base\"\n+branch_file=\"$splice_dir/branch\"\n+insert_todo=\"$splice_dir/insert-todo\"\n+remove_todo=\"$splice_dir/remove-todo\"\n+rebase_exit=\"$splice_dir/rebase-exit\"\n+rebase_cancelled=\"$splice_dir/rebase-cancelled\"\n+TMP_BRANCH=\"tmp/splice\"\n+\n+main () {\n+\tparse_opts \"$@\"\n+\n+\tif test -n \"$in_progress\"\n+\tthen\n+\t\tif in_progress\n+\t\tthen\n+\t\t\techo \"Splice in progress: $reason\"\n+\t\t\texit 0\n+\t\telse\n+\t\t\techo \"Splice not in progress\"\n+\t\t\texit 1\n+\t\tfi\n+\tfi\n+\n+\tif test -n \"$abort\" || test -n \"$continue\" || test -n \"$rebase_edit\"\n+\tthen\n+\t\tensure_splice_in_progress\n+\telse\n+\t\t# Needs to happen before parse_args(), otherwise the in-flight\n+\t\t# files will already exist.\n+\t\tensure_splice_not_in_progress\n+\tfi\n+\n+\tparse_args \"${ARGV[@]}\"\n+\n+\tif test -n \"$rebase_edit\"\n+\tthen\n+\t\t# We're being invoked by git rebase as the rebase todo list editor,\n+\t\t# rather than by the user.  This mode is for internal use only.\n+\t\trebase_edit\n+\t\treturn\n+\tfi\n+\n+\tif test -n \"$abort\"\n+\tthen\n+\t\tsplice_abort\n+\t\treturn\n+\tfi\n+\n+\t# Handle both normal execution and --continue\n+\tsplice\n+}\n+\n+prepare_tmp_branch () {\n+\tif valid_ref \"$TMP_BRANCH\"\n+\tthen\n+\t\tif test -z \"$continue\"\n+\t\tthen\n+\t\t\tdie \"BUG: $TMP_BRANCH exists but no --continue\"\n+\t\tfi\n+\n+\t\tif ! on_tmp_branch\n+\t\tthen\n+\t\t\t: \"Presumably on a detached head in the middle of a rebase\"\n+\t\tfi\n+\telse\n+\t\tif removing_root\n+\t\tthen\n+\t\t\techo git checkout -q --orphan \"$TMP_BRANCH\"\n+\t\t\tgit checkout -q --orphan \"$TMP_BRANCH\"\n+\t\t\tgit reset --hard\n+\t\telse\n+\t\t\techo git checkout -q -B \"$TMP_BRANCH\" \"$base\"\n+\t\t\tgit checkout -q -B \"$TMP_BRANCH\" \"$base\"\n+\t\tfi\n+\tfi\n+}\n+\n+do_cherry_picks () {\n+\tif cherry_pick_active\n+\tthen\n+\t\tif ! git cherry-pick --continue\n+\t\tthen\n+\t\t\terror_and_pause \"git cherry-pick --continue failed!\"\n+\t\tfi\n+\telse\n+\t\treason=\"cat $insert_todo | xargs git cherry-pick\"\n+\t\tif ! cat $insert_todo | xargs -t git cherry-pick\n+\t\tthen\n+\t\t\terror_and_pause \"git cherry-pick failed!\"\n+\t\tfi\n+\t\trm \"$insert_todo\"\n+\tfi\n+}\n+\n+do_rebase () {\n+\tif rebase_active\n+\tthen\n+\t\targs=( --continue )\n+\telif removing_root\n+\tthen\n+\t\targs=( -i --root \"$branch\" )\n+\telse\n+\t\targs=( -i --onto \"$TMP_BRANCH\" \"$base\" \"$branch\" )\n+\tfi\n+\n+\t# We make git rebase -i use a special internal-only invocation of\n+\t# git-splice which non-interactively edits the temporary\n+\t# $rebase_todo file.\n+\texport GIT_SEQUENCE_EDITOR=\"$0 $debug --rebase-edit\"\n+\n+\techo git rebase \"${args[@]}\"\n+\t# git rebase can output messages on STDOUT or STDERR depending\n+\t# on whether verbose is enabled.  Either way we want to catch\n+\t# references to \"git rebase --continue\" / \"git rebase --abort\"\n+\t# and tweak them to refer to git splice instead.\n+\t#\n+\t# To achieve that, we filter both STDOUT and STDERR through pipes,\n+\t# using a clever technique explained here:\n+\t# http://wiki.bash-hackers.org/howto/redirection_tutorial\n+\trm -f \"$rebase_exit\"\n+\t{\n+\t\t{\n+\t\t\t{\n+\t\t\t\tgit rebase \"${args[@]}\" 3>&-;\n+\t\t\t\techo $? >\"$rebase_exit\"\n+\t\t\t} |\n+\t\t\t\ttweak_rebase_error 2>&3 3>&-\n+\t\t} 2>&1 >&4 4>&- |\n+\t\t\ttweak_rebase_error 3>&- 4>&-\n+\t} 3>&2 4>&1\n+\trebase_exitcode=\"$(cat $rebase_exit)\"\n+\trm -f \"$rebase_exit\"\n+\tif test \"$rebase_exitcode\" != 0\n+\tthen\n+\t\tif test -e \"$rebase_cancelled\"\n+\t\tthen\n+\t\t\t: \"happens if there were no commits (left) to rebase\"\n+\t\t\tgit reset --hard \"$TMP_BRANCH\"\n+\t\t\trm \"$rebase_cancelled\"\n+\t\telse\n+\t\t\terror_and_pause \"git rebase ${args[*]} failed!\"\n+\t\tfi\n+\tfi\n+}\n+\n+splice () {\n+\tbase=\"$(cat $base_file)\"\n+\tbranch=\"$(cat $branch_file)\"\n+\n+\tvalidate_base\n+\n+\tif removing_root\n+\tthen\n+\t\tif test -s \"$insert_todo\"\n+\t\tthen\n+\t\t\t# If we're creating a new root commit, it will either come\n+\t\t\t# by cherry-picking onto a new orphaned $TMP_BRANCH, if we\n+\t\t\t# have any cherry-picking to do:\n+\t\t\tprepare_tmp_branch\n+\t\telse\n+\t\t\t# or it will come via rebase --root, in which case we don't\n+\t\t\t# need a temporary branch.\n+\t\t\tno_tmp_branch=y\n+\t\tfi\n+\telse\n+\t\tprepare_tmp_branch\n+\tfi\n+\n+\tif test -s \"$insert_todo\"\n+\tthen\n+\t\tdo_cherry_picks\n+\tfi\n+\n+\tif ! removing_root && test \"$base\" = \"$branch\"\n+\tthen\n+\t\techo git checkout -B \"$branch\" \"$TMP_BRANCH\"\n+\t\tgit checkout -B \"$branch\" \"$TMP_BRANCH\"\n+\telse\n+\t\tdo_rebase\n+\tfi\n+\n+\tif test -z \"$no_tmp_branch\"\n+\tthen\n+\t\tgit branch -d \"$TMP_BRANCH\"\n+\tfi\n+\trm -rf \"$splice_dir\"\n+}\n+\n+tweak_rebase_error () {\n+\tgrep -v 'When you have resolved this problem, run \"git rebase --continue\"\\.' |\n+\t\tsed -e 's/git rebase \\(--continue\\|--abort\\)/git splice \\1/g'\n+}\n+\n+valid_ref () {\n+\tgit rev-parse --quiet --verify \"$@\" >/dev/null\n+}\n+\n+# Returns true (0) iff the arguments passed explicitly describe a\n+# range of commits (e.g. A..B).  Note that this deliberately returns\n+# false when fed a single commit-ish A, even though a commit-ish\n+# technically describes a range covering A and all its ancestors.\n+# This is used to infer whether the user intended this commit to be\n+# interpreted as an insertion point or a removal range, when it is not\n+# made clear by the use of --root or a particular combination of\n+# arguments on ARGV.\n+valid_commit_range () {\n+\tif ! parsed=( $(git rev-parse \"$@\" 2>/dev/null) )\n+\tthen\n+\t\tcleanup\n+\t\tfatal \"Failed to parse commit range $1\"\n+\tfi\n+\ttest \"${#parsed[@]}\" -gt 1\n+}\n+\n+cherry_pick_active () {\n+\t# Ideally git rebase would have some plumbing for this, so\n+\t# we wouldn't have to assume knowledge of internals.\n+\tvalid_ref CHERRY_PICK_HEAD\n+}\n+\n+rebase_active () {\n+\t# Ideally git rebase would have some plumbing for this, so\n+\t# we wouldn't have to assume knowledge of internals.  See:\n+\t# http://stackoverflow.com/questions/3921409/how-to-know-if-there-is-a-git-rebase-in-progress\n+\ttest -e \"$git_dir/rebase-merge\" ||\n+\t\ttest -e \"$git_dir/rebase-apply\"\n+}\n+\n+removing_root () {\n+\ttest \"$base\" = 'ROOT'\n+}\n+\n+validate_base () {\n+\tif test -z \"$base\"\n+\tthen\n+\t\tdie \"BUG: base should not be empty\"\n+\tfi\n+\n+\tif removing_root\n+\tthen\n+\t\t: \"We're removing the root commit\"\n+\t\treturn\n+\tfi\n+\n+\tif ! valid_ref \"$base\"\n+\tthen\n+\t\tcleanup\n+\t\tdie \"BUG: base commit $base was not valid\"\n+\tfi\n+}\n+\n+error_and_pause () {\n+\twarn \"$*\"\n+\twarn \"When you have resolved this problem, run \\\"git splice --continue\\\",\"\n+\twarn \"or run \\\"git splice --abort\\\" to abandon the splice.\"\n+\texit 1\n+}\n+\n+in_progress () {\n+\tif test -e \"$insert_todo\"\n+\tthen\n+\t\treason=\"$insert_todo exists\"\n+\t\treturn 0\n+\tfi\n+\n+\tif test -e \"$remove_todo\"\n+\tthen\n+\t\treason=\"$remove_todo exists\"\n+\t\treturn 0\n+\tfi\n+\n+\tif test -d \"$splice_dir\"\n+\tthen\n+\t\treason=\"$splice_dir exists\"\n+\t\treturn 0\n+\tfi\n+\n+\tif on_tmp_branch\n+\tthen\n+\t\treason=\"on $TMP_BRANCH branch\"\n+\t\treturn 0\n+\tfi\n+\n+\treason=\n+\treturn 1\n+}\n+\n+cleanup () {\n+\taborted=\n+\n+\tif test -e \"$insert_todo\"\n+\tthen\n+\t\t# Can we be sure that the in-flight cherry-pick was started by\n+\t\t# git splice?  Probably, because otherwise\n+\t\t# ensure_cherry_pick_not_in_progress should have prevented us\n+\t\t# from reaching this point in the code.\n+\t\tif cherry_pick_active\n+\t\tthen\n+\t\t\tgit cherry-pick --abort\n+\t\tfi\n+\n+\t\trm \"$insert_todo\"\n+\t\taborted=y\n+\tfi\n+\n+\tif test -e \"$remove_todo\"\n+\tthen\n+\t\tif rebase_active\n+\t\tthen\n+\t\t\tgit rebase --abort\n+\t\tfi\n+\n+\t\trm \"$remove_todo\"\n+\t\taborted=y\n+\tfi\n+\n+\tif valid_ref \"$TMP_BRANCH\"\n+\tthen\n+\t\tif on_tmp_branch\n+\t\tthen\n+\t\t\tgit checkout \"$(cat $branch_file)\"\n+\t\tfi\n+\n+\t\tgit branch -d \"$TMP_BRANCH\"\n+\t\taborted=y\n+\tfi\n+\n+\tif test -d \"$splice_dir\"\n+\tthen\n+\t\trm -rf \"$splice_dir\"\n+\t\taborted=y\n+\tfi\n+}\n+\n+splice_abort () {\n+\tcleanup\n+\n+\tif test -z \"$aborted\"\n+\tthen\n+\t\tfatal \"No splice in progress\"\n+\tfi\n+}\n+\n+head_ref () {\n+\tgit symbolic-ref --short -q HEAD\n+}\n+\n+on_branch () {\n+\t[ \"$(head_ref)\" = \"$1\" ]\n+}\n+\n+on_tmp_branch () {\n+\ton_branch \"$TMP_BRANCH\"\n+}\n+\n+ensure_splice_in_progress () {\n+\tif ! in_progress\n+\tthen\n+\t\tfatal \"Splice not in progress\"\n+\tfi\n+}\n+\n+ensure_splice_not_in_progress () {\n+\tfor file in \"$insert_todo\" \"$remove_todo\"\n+\tdo\n+\t\tif test -e \"$file\"\n+\t\tthen\n+\t\t\tin_progress_error \"$file already exists.\"\n+\t\tfi\n+\tdone\n+\n+\tensure_cherry_pick_not_in_progress\n+\tensure_rebase_not_in_progress\n+\n+\tif on_tmp_branch\n+\tthen\n+\t\tfatal \"On $TMP_BRANCH branch, but no splice in progress.\"\\\n+\t\t    \"Try switching to another branch first.\"\n+\tfi\n+\n+\tif valid_ref \"$TMP_BRANCH\"\n+\tthen\n+\t\tfatal \"$TMP_BRANCH branch exists, but no splice in\"\\\n+\t\t    \"progress. Try deleting $TMP_BRANCH first.\"\n+\tfi\n+}\n+\n+in_progress_error () {\n+\tcat <<EOF >&2\n+$*\n+\n+git splice already in progress; please complete it, or run\n+\n+  git splice --abort\n+EOF\n+\texit 1\n+}\n+\n+ensure_cherry_pick_not_in_progress () {\n+\tif cherry_pick_active\n+\tthen\n+\t\tfatal \"Can't start git splice when there is a\"\\\n+\t\t      \"cherry-pick in progress\"\n+\tfi\n+}\n+\n+ensure_rebase_not_in_progress () {\n+\tif rebase_active\n+\tthen\n+\t\twarn \"Can't start git splice when there is a rebase in progress.\"\n+\n+\t\t# We know this will fail; we run it because we want to output\n+\t\t# the same error message which git-rebase uses to tell the user\n+\t\t# to finish or abort their in-flight rebase.\n+\t\tgit rebase\n+\t\texit 1\n+\tfi\n+}\n+\n+rebase_edit () {\n+\tif ! test -e \"$rebase_todo\"\n+\tthen\n+\t\tdie \"BUG: $me invoked in rebase edit mode,\"\\\n+\t\t    \"but $rebase_todo was missing\"\n+\tfi\n+\n+\tif test -e \"$remove_todo\"\n+\tthen\n+\t\tsed -i 's/^\\([0-9a-f]\\+\\)$/^pick \\1/' \"$remove_todo\"\n+\t\tgrep -v -f \"$remove_todo\" \"$rebase_todo\" >\"$rebase_todo\".new\n+\t\tif test -n \"$debug\"\n+\t\tthen\n+\t\t\tset +x\n+\t\t\techo -e \"-----------------------------------\"\n+\t\t\techo \"$rebase_todo\"\n+\t\t\tcat \"$rebase_todo\"\n+\t\t\techo -e \"-----------------------------------\"\n+\t\t\techo \"$remove_todo\"\n+\t\t\tcat \"$remove_todo\"\n+\t\t\techo -e \"-----------------------------------\"\n+\t\t\techo \"$rebase_todo.new\"\n+\t\t\tcat \"$rebase_todo.new\"\n+\t\t\tset -x\n+\t\tfi\n+\t\tmv \"$rebase_todo\".new \"$rebase_todo\"\n+\tfi\n+\n+\tif ! grep '^ *[a-z]' \"$rebase_todo\"\n+\tthen\n+\t\techo \"Nothing left to rebase; cancelling.\"\n+\t\t>\"$rebase_todo\"\n+\t\ttouch \"$rebase_cancelled\"\n+\tfi\n+}\n+\n+warn () {\n+\techo >&2 \"$*\"\n+}\n+\n+fatal () {\n+\tdie \"fatal: $*\"\n+}\n+\n+parse_opts () {\n+\tORIG_ARGV=( \"$@\" )\n+\twhile test $# != 0\n+\tdo\n+\t\tcase \"$1\" in\n+\t\t-h|--help)\n+\t\t\tusage\n+\t\t\t;;\n+\t\t-v|--version)\n+\t\t\techo \"$me $VERSION\"\n+\t\t\t;;\n+\t\t-d|--debug)\n+\t\t\tdebug=--debug\n+\t\t\techo >&2 \"#-------------------------------------------------\"\n+\t\t\techo >&2 \"# Invocation: $0 ${ORIG_ARGV[@]}\"\n+\t\t\tset -x\n+\n+\t\t\tshift\n+\t\t\t;;\n+\t\t--continue)\n+\t\t\tcontinue=yes\n+\t\t\tshift\n+\t\t\t;;\n+\t\t--abort)\n+\t\t\tabort=yes\n+\t\t\tshift\n+\t\t\t;;\n+\t\t--in-progress)\n+\t\t\tin_progress=yes\n+\t\t\tshift\n+\t\t\t;;\n+\t\t-r|--root)\n+\t\t\troot=yes\n+\t\t\tshift\n+\t\t\t;;\n+\t\t# for internal use only\n+\t\t--rebase-edit)\n+\t\t\trebase_edit=yes\n+\t\t\trebase_todo=\"$2\"\n+\t\t\tshift 2\n+\t\t\t;;\n+\t\t*)\n+\t\t\tbreak\n+\t\t\t;;\n+\t\tesac\n+\tdone\n+\n+\tif echo \"$continue$abort$in_progress\" | grep -q yesyes\n+\tthen\n+\t\tfatal \"You must only select one of --abort, --continue,\"\\\n+\t\t    \"and --in-progress.\"\n+\tfi\n+\n+\tARGV=( \"$@\" )\n+}\n+\n+detect_remove_range_or_insertion_point () {\n+\t# Figure out whether the first parameter is a remove range\n+\t# or insertion point.\n+\tif test -z \"$root\"\n+\tthen\n+\t\tif valid_commit_range \"$@\"\n+\t\tthen\n+\t\t\t: \"$1 must be a removal range\"\n+\t\t\tremove_range=( \"$@\" )\n+\t\telse\n+\t\t\t: \"$* must be an insertion point\"\n+\t\t\tinsertion_point=\"$@\"\n+\t\tfi\n+\telse\n+\t\t# The user has explicitly requested a removal of the\n+\t\t# commit-ish and all its ancestors.\n+\t\tremove_range=( \"$@\" )\n+\tfi\n+}\n+\n+parse_args () {\n+\tif test -n \"$abort\" || test -n \"$continue\" ||\n+\t\t\ttest -n \"$in_progress\" || test -n \"$rebase_edit\"\n+\tthen\n+\t\treturn\n+\tfi\n+\n+\tcount=$#\n+\tfor word in \"$@\"\n+\tdo\n+\t\tif test \"$word\" = '--'\n+\t\tthen\n+\t\t\tmulti_word=yes\n+\t\t\tcount=$(( $count - 1 ))\n+\t\t\tbreak\n+\t\tfi\n+\tdone\n+\n+\tif test $count -eq 0\n+\tthen\n+\t\tfatal \"You must specify at least one range to splice.\"\n+\tfi\n+\n+\tif test -z \"$multi_word\"\n+\tthen\n+\t\t# No \"--\" argument present, so the number of arguments is significant.\n+\t\tif test $# -eq 1\n+\t\tthen\n+\t\t\tif test -z \"$root\"\n+\t\t\tthen\n+\t\t\t\t# In this invocation form, $1 must be a removal range,\n+\t\t\t\t# because nothing has been given to cherry-pick.\n+\t\t\t\tif ! valid_commit_range \"$1\"\n+\t\t\t\tthen\n+\t\t\t\t\tfatal \"$1 is not a valid removal range\"\n+\t\t\t\tfi\n+\t\t\telse\n+\t\t\t\t# The user has explicitly requested a removal of the\n+\t\t\t\t# commit-ish and all its ancestors.\n+\t\t\t\tif ! valid_ref \"$1\"\n+\t\t\t\tthen\n+\t\t\t\t\tfatal \"$1 is not a valid removal commit-ish\"\n+\t\t\t\tfi\n+\t\t\tfi\n+\t\t\tremove_range=( \"$1\" )\n+\t\telif test $# -eq 2\n+\t\tthen\n+\t\t\tinsert_range=( \"$2\" )\n+\t\t\tdetect_remove_range_or_insertion_point \"$1\"\n+\t\telif test $# -ge 2\n+\t\tthen\n+\t\t\tfatal \"Use of multiple words in the removal or insertion\"\\\n+\t\t\t    \"ranges requires the -- separator\"\n+\t\tfi\n+\telse\n+\t\t# \"--\" argument is present, so split\n+\t\tremove_range_or_insertion_base=()\n+\t\tfor word in \"$@\"\n+\t\tdo\n+\t\t\tif test \"$word\" = '--'\n+\t\t\tthen\n+\t\t\t\tshift\n+\t\t\t\tinsert_range=( \"$@\" )\n+\t\t\t\tbreak\n+\t\t\tfi\n+\t\t\tremove_range_or_insertion_base+=( \"$word\" )\n+\t\t\tshift\n+\t\tdone\n+\n+\t\tdetect_remove_range_or_insertion_point \\\n+\t\t\t\"${remove_range_or_insertion_base[@]}\"\n+\tfi\n+\n+\tmkdir -p \"$splice_dir\"\n+\n+\tif ! head_ref >\"$branch_file\"\n+\tthen\n+\t\trm \"$branch_file\"\n+\t\tfatal \"Cannot run $me on detached head\"\n+\tfi\n+\n+\tif [ \"${#remove_range[@]}\" -gt 0 ]\n+\tthen\n+\t\t# In this case we already know it's a range\n+\t\t: \"removing range ${remove_range[@]}\"\n+\t\tcheck_no_merge_commits \"Removing\" \"${remove_range[@]}\"\n+\t\tpopulate_remove_todo \"${remove_range[@]}\"\n+\t\tpopulate_base_file \"${remove_range[@]}\"\n+\telif test -n \"$insertion_point\"\n+\tthen\n+\t\techo \"$insertion_point\" >\"$base_file\"\n+\telse\n+\t\tdie \"BUG: didn't get removal range or insertion point\"\n+\tfi\n+\n+\tif [ \"${#insert_range[@]}\" -gt 0 ]\n+\tthen\n+\t\tif ! valid_commit_range \"${insert_range[@]}\"\n+\t\tthen\n+\t\t\tcleanup\n+\t\t\tfatal \"Failed to parse ${insert_range[*]} as insertion range\"\n+\t\tfi\n+\n+\t\tcheck_no_merge_commits \"Inserting\" \"${insert_range[@]}\"\n+\n+\t\tif [ \"${#insert_range[@]}\" -eq 1 ]\n+\t\tthen\n+\t\t\techo \"${insert_range[@]}\" >\"$insert_todo\"\n+\t\telse\n+\t\t\tgit rev-list --reverse \"${insert_range[@]}\" >\"$insert_todo\"\n+\t\tfi\n+\tfi\n+}\n+\n+check_no_merge_commits () {\n+\taction=\"$1\"\n+\tshift\n+\tif git rev-list --min-parents=2 -n1 \"$@\" | grep -q .\n+\tthen\n+\t\tcleanup\n+\t\tfatal \"$action merge commits is not supported\"\n+\tfi\n+}\n+\n+populate_remove_todo () {\n+\tgit rev-list --abbrev-commit \"$@\" >\"$remove_todo\"\n+\tif ! test -s \"$remove_todo\"\n+\tthen\n+\t\tcleanup\n+\t\tfatal \"No commits found in removal range $*\"\n+\tfi\n+\tnewest=$(head -n1 \"$remove_todo\")\n+\tnewest=$(git rev-parse \"$newest\") # unabbreviate for comparison below\n+\thead=$(head_ref)\n+\tmb=$(git merge-base \"$newest\" \"$head\")\n+\tif test \"$mb\" != \"$newest\"\n+\tthen\n+\t\tcleanup\n+\t\tfatal \"$newest is in removal range but not in $head branch\"\n+\tfi\n+}\n+\n+populate_base_file () {\n+\tearliest=$(tail -n1 \"$remove_todo\")\n+\techo \"Earliest commit in $@ is $earliest\"\n+\tif git rev-list --min-parents=1 -n1 \"${earliest}\" | grep -q .\n+\tthen\n+\t\t# Earliest in removal range has a parent\n+\t\techo \"${earliest}^\" >\"$base_file\"\n+\telse\n+\t\techo \"ROOT\" >\"$base_file\"\n+\tfi\n+}\n+\n+main \"$@\"\ndiff --git a/t/t7900-splice.sh b/t/t7900-splice.sh\nnew file mode 100755\nindex 0000000..5654309\n--- /dev/null\n+++ b/t/t7900-splice.sh\n@@ -0,0 +1,630 @@\n+#!/bin/sh\n+#\n+# Copyright (c) 2016 Adam Spiers\n+#\n+\n+test_description='git splice\n+\n+This tests all features of git-splice.\n+'\n+\n+. ./test-lib.sh\n+\n+TMP_BRANCH=tmp/splice\n+\n+#############################################################################\n+# Setup\n+\n+for i in one two three\n+do\n+\tfor j in a b\n+\tdo\n+\t\ttag=$i-$j\n+\t\ttest_expect_success \"setup $i\" \"\n+\t\t\techo $i $j >> $i &&\n+\t\t\tgit add $i &&\n+\t\t\tgit commit -m \\\"$i $j\\\" &&\n+\t\t\tgit tag $tag\"\n+\tdone\n+done\n+git_dir=`git rev-parse --git-dir`\n+latest_tag=$tag\n+\n+setup_other_branch () {\n+\tbranch=\"$1\" base=\"$2\"\n+\tshift 2\n+\tgit checkout -b $branch $base &&\n+\tfor i in \"$@\"\n+\tdo\n+\t\techo $branch $i >> $branch &&\n+\t\tgit add $branch &&\n+\t\tgit commit -m \"$branch $i\" &&\n+\t\tgit tag \"$branch-$i\"\n+\tdone\n+}\n+\n+test_expect_success \"setup four branch\" '\n+\tsetup_other_branch four one-b a b c &&\n+\tgit checkout master\n+'\n+\n+test_debug 'git show-ref'\n+\n+del_tmp_branch () {\n+\tgit update-ref -d refs/heads/$TMP_BRANCH\n+}\n+\n+reset () {\n+\t# First check that tests don't leave a splice in progress,\n+\t# as they should always do --abort or --continue if necessary\n+\ttest_splice_not_in_progress &&\n+\ton_branch master &&\n+\tgit reset --hard $latest_tag &&\n+\tdel_tmp_branch &&\n+\trm -f stdout stderr\n+}\n+\n+git_splice () {\n+\tgit splice ${debug:+-d} \"$@\" >stdout 2>stderr\n+\tret=$?\n+\tset +x\n+\tif [ -s stdout ]\n+\tthen\n+\t\techo \"====== STDOUT from git splice $* ======\"\n+\tfi\n+\tcat stdout\n+\tif [ -s stderr ]\n+\tthen\n+\t\techo \"------ STDERR from git splice $* ------\"\n+\t\tcat stderr\n+\tfi\n+\techo \"====== exit $ret from git splice $* ======\"\n+\tif test -n \"$trace\"\n+\tthen\n+\t\tset -x\n+\tfi\n+\treturn $ret\n+}\n+\n+test_splice_in_progress () {\n+\tgit splice --in-progress\n+}\n+\n+head_ref () {\n+\tgit symbolic-ref --short -q HEAD\n+}\n+\n+on_branch () {\n+\tif test \"`head_ref`\" = \"$1\"\n+\tthen\n+\t\treturn 0\n+\telse\n+\t\techo \"not on $1 branch\" >&2\n+\t\treturn 1\n+\tfi\n+}\n+\n+test_splice_not_in_progress () {\n+\ttest_must_fail test_splice_in_progress &&\n+\ttest_must_fail git_splice --continue &&\n+\t\tgrep -q \"Splice not in progress\" stderr &&\n+\t\ttest_debug 'echo \"--continue failed as expected - good\"' &&\n+\ttest_must_fail git_splice --abort    &&\n+\t\tgrep -q \"Splice not in progress\" stderr &&\n+\t\ttest_debug 'echo \"--abort failed as expected - good\"'\n+}\n+\n+#############################################################################\n+# Invalid arguments\n+\n+test_expect_success 'empty command line' '\n+\ttest_must_fail git_splice &&\n+\tgrep \"You must specify at least one range to splice\" stderr\n+'\n+\n+test_expect_success 'too many arguments' '\n+\ttest_must_fail git_splice a b c &&\n+\tgrep \"Use of multiple words in the removal or insertion ranges requires the -- separator\" stderr\n+'\n+\n+test_only_one_option () {\n+\ttest_splice_not_in_progress &&\n+\ttest_must_fail git_splice \"$@\" &&\n+\tgrep \"You must only select one of --abort, --continue, and --in-progress\" stderr &&\n+\ttest_splice_not_in_progress\n+}\n+\n+for combo in \\\n+\t'--abort --continue' \\\n+\t'--continue --abort' \\\n+\t'--abort --in-progress' \\\n+\t'--in-progress --abort' \\\n+\t'--continue --in-progress' \\\n+\t'--in-progress --continue'\n+do\n+\ttest_expect_success \"$combo\" \"\n+\t\ttest_only_one_option $combo\n+\t\"\n+done\n+\n+test_expect_success 'insertion point without insertion range' '\n+\ttest_must_fail git_splice one &&\n+\tgrep \"fatal: one is not a valid removal range\" stderr &&\n+\ttest_splice_not_in_progress\n+'\n+\n+test_failed_to_parse_removal_spec () {\n+\ttest_must_fail git_splice \"$@\" &&\n+\tgrep \"fatal: Failed to parse commit range $*\" stderr &&\n+\ttest_splice_not_in_progress\n+}\n+\n+test_expect_success 'remove invalid single commit' '\n+\ttest_failed_to_parse_removal_spec five\n+'\n+\n+test_expect_success 'remove range with invalid start' '\n+\ttest_failed_to_parse_removal_spec five..two-b\n+'\n+\n+test_expect_success 'remove range with invalid end' '\n+\ttest_failed_to_parse_removal_spec two-b..five\n+'\n+\n+test_expect_success 'empty removal range' '\n+\ttest_must_fail git_splice two-a..two-a &&\n+\tgrep \"^fatal: No commits found in removal range two-a..two-a\" stderr &&\n+\ttest_splice_not_in_progress\n+'\n+\n+#############################################################################\n+# Invalid initial state\n+\n+test_expect_success \"checkout $TMP_BRANCH; ensure splice won't start\" \"\n+\ttest_when_finished 'git checkout master; del_tmp_branch' &&\n+\treset &&\n+\tgit checkout -b $TMP_BRANCH &&\n+\ttest_must_fail git_splice two-b^! &&\n+\tgrep 'fatal: On $TMP_BRANCH branch, but no splice in progress' stderr &&\n+\tgit checkout master &&\n+\tdel_tmp_branch &&\n+\ttest_splice_not_in_progress\n+\"\n+\n+test_expect_success \"create $TMP_BRANCH; ensure splice won't start\" \"\n+\ttest_when_finished 'del_tmp_branch' &&\n+\treset &&\n+\tgit branch $TMP_BRANCH master &&\n+\ttest_must_fail git_splice two-b^! &&\n+\tgrep '$TMP_BRANCH branch exists, but no splice in progress' stderr &&\n+\tdel_tmp_branch &&\n+\ttest_splice_not_in_progress\n+\"\n+\n+test_expect_success \"start cherry-pick with conflicts; ensure splice won't start\" '\n+\ttest_when_finished \"git cherry-pick --abort\" &&\n+\treset &&\n+\ttest_must_fail git cherry-pick four-b >stdout 2>stderr &&\n+\tgrep \"error: could not apply .* four b\" stderr &&\n+\ttest_must_fail git_splice two-b^! &&\n+\tgrep \"Can'\\''t start git splice when there is a cherry-pick in progress\" stderr &&\n+\ttest_splice_not_in_progress\n+'\n+\n+test_expect_success \"start rebase with conflicts; ensure splice won't start\" '\n+\ttest_when_finished \"git rebase --abort\" &&\n+\treset &&\n+\ttest_must_fail git rebase --onto one-b two-a >stdout 2>stderr &&\n+\tgrep \"CONFLICT\" stdout &&\n+\tgrep \"Failed to merge in the changes\" stderr &&\n+\ttest_must_fail git_splice two-b^! &&\n+\tgrep \"Can'\\''t start git splice when there is a rebase in progress\" stderr &&\n+\ttest_splice_not_in_progress\n+'\n+\n+test_expect_success 'cause conflict; ensure not re-entrant' '\n+\ttest_when_finished \"\n+\t\tgit_splice --abort &&\n+\t\ttest_splice_not_in_progress\n+\t\" &&\n+\treset &&\n+\ttest_must_fail git_splice two-a^! &&\n+\ttest_splice_in_progress &&\n+\ttest_must_fail git_splice two-a^! &&\n+\tgrep \"git splice already in progress; please complete it, or run\" stderr &&\n+\tgrep \"git splice --abort\" stderr &&\n+\ttest_splice_in_progress\n+'\n+\n+#############################################################################\n+# Removing a single commit\n+\n+test_remove_two_b () {\n+\treset &&\n+\tgit_splice two-b^! \"$@\" &&\n+\tgrep \"one b\"   one   &&\n+\tgrep \"three b\" three &&\n+\tgrep \"two a\"   two   &&\n+\t! grep \"two b\" two   &&\n+\ttest_splice_not_in_progress\n+}\n+\n+test_expect_success 'remove single commit' '\n+\ttest_remove_two_b\n+'\n+\n+test_expect_success 'remove single commit with --' '\n+\ttest_remove_two_b --\n+'\n+\n+test_expect_success 'remove single commit causing conflict; abort' '\n+\treset &&\n+\ttest_must_fail git_splice two-a^! &&\n+\tgrep \"Could not apply .* two b\" stdout stderr &&\n+\tgrep \"When you have resolved this problem, run \\\"git splice --continue\\\"\" stdout stderr &&\n+\tgrep \"or run \\\"git splice --abort\\\"\" stdout stderr &&\n+\ttest_splice_in_progress &&\n+\tgit_splice --abort &&\n+\ttest_splice_not_in_progress\n+'\n+\n+test_expect_success 'remove single commit causing conflict; fix; continue' '\n+\treset &&\n+\ttest_must_fail git_splice two-a^! &&\n+\tgrep \"Could not apply .* two b\" stdout stderr &&\n+\tgrep \"When you have resolved this problem, run \\\"git splice --continue\\\"\" stdout stderr &&\n+\tgrep \"or run \\\"git splice --abort\\\"\" stdout stderr &&\n+\ttest_splice_in_progress &&\n+\techo two merged >two &&\n+\tgit add two &&\n+\tgit_splice --continue &&\n+\tgrep \"two merged\" two &&\n+\tgrep \"three b\" three &&\n+\ttest_splice_not_in_progress\n+'\n+\n+test_expect_success 'remove root commit' '\n+\t# We have to remove one-b first, in order to avoid conflicts when\n+\t# we remove one-a.\n+\treset &&\n+\tgit_splice one-b^! &&\n+\t! grep \"one b\" one &&\n+\tgit_splice --root one-a &&\n+\t! test -e one &&\n+\tgrep \"three b\" three &&\n+\ttest_splice_not_in_progress\n+'\n+\n+test_expect_success 'remove root commit causing conflict; abort' '\n+\treset &&\n+\ttest_must_fail git_splice --root one-a &&\n+\tgrep \"Could not apply .* one b\" stdout stderr &&\n+\tgrep \"When you have resolved this problem, run \\\"git splice --continue\\\"\" stdout stderr &&\n+\tgrep \"or run \\\"git splice --abort\\\"\" stdout stderr &&\n+\ttest_splice_in_progress &&\n+\tgit_splice --abort &&\n+\ttest_splice_not_in_progress\n+'\n+\n+test_expect_success 'remove root commit causing conflict; fix; continue' '\n+\treset &&\n+\ttest_must_fail git_splice --root one-a &&\n+\tgrep \"Could not apply .* one b\" stdout stderr &&\n+\tgrep \"When you have resolved this problem, run \\\"git splice --continue\\\"\" stdout stderr &&\n+\tgrep \"or run \\\"git splice --abort\\\"\" stdout stderr &&\n+\ttest_splice_in_progress &&\n+\techo one merged >one &&\n+\tgit add one &&\n+\tgit_splice --continue &&\n+\tgrep \"one merged\" one &&\n+\tgrep \"three b\" three &&\n+\ttest_splice_not_in_progress\n+'\n+\n+#############################################################################\n+# Removing a range of commits\n+\n+test_remove_range_of_commits () {\n+\treset &&\n+\tgit_splice one-b..two-b \"$@\" &&\n+\tgrep \"one b\"   one   &&\n+\tgrep \"three b\" three &&\n+\t! test -e two        &&\n+\ttest_splice_not_in_progress\n+}\n+\n+test_expect_success 'remove range of commits' '\n+\ttest_remove_range_of_commits\n+'\n+\n+test_expect_success 'remove range of commits with --' '\n+\ttest_remove_range_of_commits --\n+'\n+\n+test_expect_success 'remove commit from branch tip' '\n+\treset &&\n+\tgit_splice HEAD^! &&\n+\ttest `git rev-parse HEAD` = `git rev-parse three-a` &&\n+\ttest_splice_not_in_progress\n+'\n+\n+test_expect_success 'remove commits from branch tip' '\n+\treset &&\n+\tgit_splice HEAD~3..HEAD &&\n+\ttest `git rev-parse HEAD` = `git rev-parse two-a` &&\n+\ttest_splice_not_in_progress\n+'\n+\n+test_expect_success 'remove range of commits starting at root' '\n+\treset &&\n+\tgit_splice --root one-b &&\n+\t! test -e one &&\n+\tgrep \"three b\" three &&\n+\ttest_splice_not_in_progress\n+'\n+\n+test_expect_success 'remove range of commits starting at root' '\n+\treset &&\n+\tgit_splice --root one-b -- &&\n+\t! test -e one &&\n+\ttest_splice_not_in_progress\n+'\n+\n+test_expect_success 'remove range of commits outside branch' '\n+\treset &&\n+\ttest_must_fail git_splice four-a..four-c &&\n+\tgrep \"^fatal: .* is in removal range but not in master\" stderr &&\n+\t! test -e four &&\n+\tgrep \"three b\" three &&\n+\ttest_splice_not_in_progress\n+'\n+\n+test_expect_success 'dirty working tree prevents removing commit on same file' '\n+\treset &&\n+\techo dirty >>two &&\n+\ttest_when_finished \"\n+\t\tgit_splice --abort &&\n+\t\ttest_splice_not_in_progress\n+\t\" &&\n+\ttest_must_fail git_splice two-b^! &&\n+\tgrep \"^error: Your local changes to the following files would be overwritten by checkout:\" stderr &&\n+\tgrep \"^[[:space:]]*two\" stderr &&\n+\tgrep \"^Please commit your changes or stash them before you switch branches\" stderr &&\n+\tgrep dirty two &&\n+\ttest_splice_in_progress\n+'\n+\n+test_expect_success 'dirty working tree prevents removing commit on other file' '\n+\treset &&\n+\techo dirty >>three &&\n+\ttest_when_finished \"\n+\t\tgit_splice --abort &&\n+\t\ttest_splice_not_in_progress\n+\t\" &&\n+\ttest_must_fail git_splice two-b^! &&\n+\tgrep \"^error: Your local changes to the following files would be overwritten by checkout:\" stderr &&\n+\tgrep \"^[[:space:]]*three\" stderr &&\n+\tgrep \"^Please commit your changes or stash them before you switch branches\" stderr &&\n+\ttest_splice_in_progress\n+'\n+\n+create_merge_commit () {\n+\ttest_when_finished \"git tag -d four-merge\" &&\n+\treset &&\n+\tgit merge four &&\n+\tgit tag four-merge &&\n+\techo \"four d\" >>four &&\n+\tgit commit -am\"four d\"\n+}\n+\n+test_expect_success 'abort when trying to remove a merge commit' '\n+\tcreate_merge_commit &&\n+\ttest_must_fail git_splice four-merge^! &&\n+\tgrep \"^fatal: Removing merge commits is not supported\" stderr &&\n+\ttest_splice_not_in_progress\n+'\n+\n+test_expect_success 'abort when removal range contains merge commits' '\n+\tcreate_merge_commit &&\n+\ttest_must_fail git_splice four-merge^^..HEAD &&\n+\tgrep \"^fatal: Removing merge commits is not supported\" stderr &&\n+\ttest_splice_not_in_progress\n+'\n+\n+# The foo.. notation doesn't naturally play nice with our implementation,\n+# since HEAD gets moved around during the splice.\n+test_expect_success 'abort when removal range contains merge commits (2)' '\n+\tcreate_merge_commit &&\n+\ttest_must_fail git_splice four-merge^^.. &&\n+\tgrep \"^fatal: Removing merge commits is not supported\" stderr &&\n+\ttest_splice_not_in_progress\n+'\n+\n+#############################################################################\n+# Inserting a single commit\n+\n+test_expect_success 'insert single commit at HEAD' '\n+\treset &&\n+\tgit_splice HEAD four-a^! &&\n+\tgrep \"two b\" two &&\n+\tgrep \"three a\" three &&\n+\tgrep \"four a\" four &&\n+\t! grep \"four b\" four &&\n+\tgit log --format=format:%s, | xargs |\n+\t\tgrep \"four a, three b, three a, two b,\" &&\n+\ttest_splice_not_in_progress\n+'\n+\n+test_expect_success 'insert single commit within branch' '\n+\treset &&\n+\tgit_splice two-b four-a^! &&\n+\tgrep \"two b\" two &&\n+\tgrep \"three a\" three &&\n+\tgrep \"four a\" four &&\n+\t! grep \"four b\" four &&\n+\tgit log --format=format:%s, | xargs |\n+\t\tgrep \"three b, three a, four a, two b,\" &&\n+\ttest_splice_not_in_progress\n+'\n+\n+create_five_branch () {\n+\ttest_when_finished \"\n+\t\tgit branch -D five &&\n+\t\tgit tag -d five-{a,b,c,merge}\n+\t\" &&\n+\tsetup_other_branch five one-b a b &&\n+\tgit checkout five &&\n+\tgit merge four-a &&\n+\tgit tag five-merge &&\n+\techo \"five c\" >>five &&\n+\tgit commit -am\"five c\" &&\n+\tgit tag five-c &&\n+\tgit checkout master\n+}\n+\n+test_expect_success 'abort when appending a single merge commit on HEAD' '\n+\treset &&\n+\tcreate_five_branch &&\n+\ttest_must_fail git_splice HEAD five-merge^! &&\n+\tgrep \"^fatal: Inserting merge commits is not supported\" stderr &&\n+\ttest_splice_not_in_progress\n+'\n+\n+test_expect_success 'abort when inserting a single merge commit within branch' '\n+\treset &&\n+\tcreate_five_branch &&\n+\ttest_must_fail git_splice HEAD~2 five-merge^! &&\n+\tgrep \"^fatal: Inserting merge commits is not supported\" stderr &&\n+\ttest_splice_not_in_progress\n+'\n+\n+#############################################################################\n+# Inserting a range of commits\n+\n+test_expect_success 'insert commit range' '\n+\treset &&\n+\tgit_splice two-b one-b..four-b &&\n+\tgrep \"two b\" two &&\n+\tgrep \"three a\" three &&\n+\tgrep \"four b\" four &&\n+\tgit log --format=format:%s, | xargs |\n+\t\tgrep \"three b, three a, four b, four a, two b,\" &&\n+\ttest_splice_not_in_progress\n+'\n+\n+test_expect_success 'insert commit causing conflict; abort' '\n+\treset &&\n+\ttest_must_fail git_splice two-b four-b^! &&\n+\tgrep \"could not apply .* four b\" stderr &&\n+\tgrep \"git cherry-pick failed\" stderr &&\n+\tgrep \"When you have resolved this problem, run \\\"git splice --continue\\\"\" stdout stderr &&\n+\tgrep \"or run \\\"git splice --abort\\\"\" stdout stderr &&\n+\ttest_splice_in_progress &&\n+\tgit_splice --abort &&\n+\ttest_splice_not_in_progress\n+'\n+\n+test_expect_success 'insert commit causing conflict; fix; continue' '\n+\treset &&\n+\ttest_must_fail git_splice two-b four-b^! &&\n+\tgrep \"could not apply .* four b\" stderr &&\n+\tgrep \"git cherry-pick failed\" stderr &&\n+\tgrep \"When you have resolved this problem, run \\\"git splice --continue\\\"\" stdout stderr &&\n+\tgrep \"or run \\\"git splice --abort\\\"\" stdout stderr &&\n+\ttest_splice_in_progress &&\n+\techo four merged >four &&\n+\tgit add four &&\n+\tgit_splice --continue &&\n+\tgrep \"four merged\" four &&\n+\tgrep \"three b\" three &&\n+\ttest_splice_not_in_progress\n+'\n+\n+test_expect_success 'abort when appending range includes a merge commit' '\n+\treset &&\n+\tcreate_five_branch &&\n+\ttest_must_fail git_splice HEAD five-a^..five &&\n+\tgrep \"^fatal: Inserting merge commits is not supported\" stderr &&\n+\ttest_splice_not_in_progress\n+'\n+\n+test_expect_success 'abort when inserting range includes a merge commit' '\n+\treset &&\n+\tcreate_five_branch &&\n+\ttest_must_fail git_splice HEAD~2 five-a^..five &&\n+\tgrep \"^fatal: Inserting merge commits is not supported\" stderr &&\n+\ttest_splice_not_in_progress\n+'\n+\n+#############################################################################\n+# Removing a range and inserting one or more commits\n+\n+test_expect_success 'remove range; insert commit' '\n+\treset &&\n+\tgit_splice two-a^..two-b four-a^! &&\n+\tgrep \"four a\" four &&\n+\t! grep \"four b\" four &&\n+\tgrep \"three b\" three &&\n+\t! test -e two &&\n+\ttest_splice_not_in_progress\n+'\n+\n+test_expect_success 'remove range; insert commit range' '\n+\treset &&\n+\tgit_splice two-a^..two-b four-a^..four-b &&\n+\tgrep \"four b\" four &&\n+\t! grep \"four c\" four &&\n+\tgrep \"three b\" three &&\n+\t! test -e two &&\n+\ttest_splice_not_in_progress\n+'\n+\n+test_expect_success 'remove range; insert commit causing conflict; abort' '\n+\treset &&\n+\ttest_must_fail git_splice two-a^..two-b four-b^! &&\n+\tgrep \"could not apply .* four b\" stderr &&\n+\tgrep \"git cherry-pick failed\" stderr &&\n+\tgrep \"When you have resolved this problem, run \\\"git splice --continue\\\"\" stderr &&\n+\tgrep \"or run \\\"git splice --abort\\\" to abandon the splice\" stderr &&\n+\ttest_splice_in_progress &&\n+\tgit_splice --abort &&\n+\ttest_splice_not_in_progress\n+'\n+\n+test_remove_range_insert_commit_fix_conflict_continue () {\n+\treset &&\n+\ttest_must_fail git_splice two-a^..two-b \"$@\" four-b^! &&\n+\tgrep \"could not apply .* four b\" stderr &&\n+\tgrep \"git cherry-pick failed\" stderr &&\n+\tgrep \"When you have resolved this problem, run \\\"git splice --continue\\\"\" stdout stderr &&\n+\tgrep \"or run \\\"git splice --abort\\\"\" stdout stderr &&\n+\ttest_splice_in_progress &&\n+\techo four merged >four &&\n+\tgit add four &&\n+\tgit_splice --continue &&\n+\tgrep \"four merged\" four &&\n+\tgrep \"three b\" three &&\n+\t! test -e two &&\n+\ttest_splice_not_in_progress\n+}\n+\n+test_expect_success 'remove range; insert commit causing conflict; fix; continue' '\n+\ttest_remove_range_insert_commit_fix_conflict_continue\n+'\n+\n+test_expect_success 'remove range -- insert commit causing conflict; fix; continue' '\n+\ttest_remove_range_insert_commit_fix_conflict_continue --\n+'\n+\n+test_expect_success 'remove grepped commits; insert grepped commits' '\n+\treset &&\n+\tgit_splice --grep=two -n1 three-b -- --grep=four --skip=1 four &&\n+\tgrep \"two a\" two &&\n+\t! grep \"two b\" two &&\n+\tgrep \"four b\" four &&\n+\t! grep \"four c\" four &&\n+\tgrep \"three b\" three &&\n+\ttest_splice_not_in_progress\n+'\n+\n+test_done\n-- \ngit-series 0.9.1\n"},{"id":"325349","messageId":"xmqqo9s0gsia.fsf@gitster.mtv.corp.google.com","threadId":"46491","inReplyTo":"cover.55495badd28b73b39c60ca4107b50aae7ee95028.1501535033.git-series.git@adamspiers.org","subject":"Re: [PATCH 0/1] add git-splice subcommand for non-interactive branch splicing","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2017-07-31T22:18:21Z","receivedAt":"2017-07-31T22:18:28Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Adam Spiers <git@adamspiers.org> writes:\n\n> Therefore there is a risk that each new UI for higher-level workflows\n> will end up re-implementing these mid-level operations.  This\n> undesirable situation could be avoided if git itself provided those\n> mid-level operations.\n\nLet me make sure if I get your general idea right, first.\n\nIs your aim is to give a single unified mid-layer that these other\ntools can build on instead of rolling their own \"cherry-pick these\nranges, then squash that in, and then merge the other one in, ...\"\nsequencing machinery?\n\nIf so, I think that is a very good goal.\n\n>     # Remove commits A..B (i.e. excluding A) from the current branch.\n>     git splice A..B\n>     # Remove commit A from the current branch.\n>     git splice A^!\n>     # Remove commits A..B from the current branch, and cherry-pick\n>     # commits C..D at the same point.\n>     git splice A..B C..D\n\nWe need to make sure that the mid-layer tool offers a good set of\nprimitive operations that serve all of these other tools' needs.  I\ndo not know offhand if what you implemented that are illustrated by\nthese examples is or isn't that \"good set\".\n\nAssuming that there is such a \"good set of primitives\" surfaced at\nthe UI level so that these other tools can express what they want to\nperform with, I'd personally prefer to see a solution that extends\nand uses the common \"sequencer\" machinery we have been using to\ndrive cherry-picks, reverts and interactive rebases that work on\nmultiple commits.  IOW, it would be nice to see that the only thing\n\"git splice A..B\" does is to prepare a series of instructions in a\nfile, e.g. .git/sequencer/todo, just like \"git cherry-pick A..B\"\nwould, and let the sequencer machinery to handle the sequencing.\n\nE.g. In a history like\n\n    ---o---A---o---B---X---Y---Z   HEAD\n\n\"git splice A..B\" command would write something like this:\n\n    reset to A\n    pick X\n    pick Y\n    pick Z\n\nto the todo file and drive the sequencer.  As you notice, you would\nneed to extend the vocabulary of the sequencer a bit to allow\nvarious things that the current users of the sequencer machinery do\nnot need, like resetting the HEAD to a specific commit, merging a\nside branch, remembering the result of an operation, and referring\nto such a commit in later operation.  For example, if you tell \"git\nsplice\" to expunge A from this sample history (I am not sure how you\nexpress that operation in your UI):\n\n         B---C---D\n        /         \\\n    ---o---A---E---F---G   HEAD\n\nit might create a \"todo\" list like this to rebuild the history:\n\n    reset to A^\n    pick B\n    pick C\n    pick D\n    mark :1\n    reset to A^\n    pick E\n    merge :1 using F's log message and conflict resolution as reference\n    pick G\n\nto result in:\n\n         B---C---D\n        /         \\\n    ---o-------E---F---G   HEAD\n\nDo not pay too much attention to how the hypothetical \"extended todo\ninstruction set\" is spelled in the above illustration (e.g. I am not\nadvocating for multi-word command like \"reset to\"); these are only\nto illustrate what kind of features would be needed for the job.  In\nthe final shape of the system, \"merge\" in the illustration above may\nbe a more succinct \"merge F :1\", for example (i.e. the first\nparameter would name an existing merge to use as reference, the\nremainder is a list of commits to be merged to the current HEAD),\njust like \"pick X\" is a succinct way to say \"cherry-pick the change\nintroduced by existing commit X to HEAD, reusing X's log message\nand author information\".\n\nSomething like that may have a place in the git-core, I would think. \n\nI am not sure if a bash script that calls rebase/cherry-pick/commit\nmanually can serve as a good \"universal mid-layer\" or just adding\nanother random command to the set of existing third-party commands\nfor \"higher-level workflows\".\n"},{"id":"325361","messageId":"20170801011421.veyuviur3mi4hjir@pacific.linksys.moosehall","threadId":"46491","inReplyTo":"xmqqo9s0gsia.fsf@gitster.mtv.corp.google.com","subject":"Re: [PATCH 0/1] add git-splice subcommand for non-interactive branch splicing","fromName":"Adam Spiers","fromEmail":"git@adamspiers.org","sentAt":"2017-08-01T01:14:21Z","receivedAt":"2017-08-01T01:14:30Z","isPatch":true,"sender":{"key":"git@adamspiers.org","avatar":"https://avatars.githubusercontent.com/u/100738?v=4"},"body":"On 31 July 2017 at 23:18, Junio C Hamano <gitster@pobox.com> wrote:\n> Adam Spiers <git@adamspiers.org> writes:\n>\n> > Therefore there is a risk that each new UI for higher-level workflows\n> > will end up re-implementing these mid-level operations.  This\n> > undesirable situation could be avoided if git itself provided those\n> > mid-level operations.\n>\n> Let me make sure if I get your general idea right, first.\n>\n> Is your aim is to give a single unified mid-layer that these other\n> tools can build on instead of rolling their own \"cherry-pick these\n> ranges, then squash that in, and then merge the other one in, ...\"\n> sequencing machinery?\n\nPretty much, yes.  The original itch I wanted to scratch was\nimplementing git-explode, which aims to automatically explode a large\ntopic branch into a set of smaller, independent topic branches, by\nharnessing my git-deps for automatically detecting inter-dependencies\nbetween commits in the large source branch and using that dependency\ntree to construct the smaller topic branches.  (Before anyone protests\nat this point, yes, I am fully aware that it is not possible to\nautomate 100% accurate detection of these dependencies, and no, that\ndoes not completely invalidate the approach.[0])\n\nMy initial thought was that in order to be able to automatically\ndecompose a branch into smaller branches, I would need a mid-layer\noperation \"git-transplant\" somewhat analogous to mv(1), which would\nlet me easily move commits out of the source branch into a new target\nbranch.  And then I realised that, in the same way that\n(simplistically speaking) mv(1) could be reimplemented as cp(1)\nfollowed by rm(1), implementing \"git-transplant\" in turn would require\nmore primitive operations for copying commits between branches, and\nremoving commits from branches.  At this point I saw value in\ngeneralising those operations; hence the idea for git-splice was born.\n\nConsequently I implemented prototypes for splice and transplant, which\ndidn't take too long.  (The real work was writing comprehensive test\nsuites and polishing the tools until they were reliable enough to pass\n100%.)\n\nIronically, soon after I started to implement git-explode, I realised\nthat the order in which I needed to walk the dependency tree\ndiscovered by git-deps actually meant that I couldn't use\ngit-transplant for this particular use case, so in the end I\nimplemented it with pygit2.  (I still need to polish it up a bit more\nbefore releasing.)\n\nHowever, even though splice and transplant are not useful for this\nparticular use case, I still believe that they (or similar tools) have\nthe potential to serve as a useful foundation for other workflows.\n\n> If so, I think that is a very good goal.\n\nGlad to hear it :-)\n\n> >     # Remove commits A..B (i.e. excluding A) from the current branch.\n> >     git splice A..B\n> >     # Remove commit A from the current branch.\n> >     git splice A^!\n> >     # Remove commits A..B from the current branch, and cherry-pick\n> >     # commits C..D at the same point.\n> >     git splice A..B C..D\n>\n> We need to make sure that the mid-layer tool offers a good set of\n> primitive operations that serve all of these other tools' needs.  I\n> do not know offhand if what you implemented that are illustrated by\n> these examples is or isn't that \"good set\".\n\nAgreed.  That's why I sent the RFC to this list last year: in the hope\nthat these details could be hashed out and guide my development in the\nright direction.  Unfortunately I didn't get much response at the\ntime, which was probably my fault for not explaining my \"mission\ngoals\" clearly enough.  Although in fairness to myself, I think I\nneeded a year anyway to let the ideas in my head mature to the point\nwhere I understood them well enough myself to communicate them clearly\nto others :-)\n\n> Assuming that there is such a \"good set of primitives\" surfaced at\n> the UI level so that these other tools can express what they want to\n> perform with, I'd personally prefer to see a solution that extends\n> and uses the common \"sequencer\" machinery we have been using to\n> drive cherry-picks, reverts and interactive rebases that work on\n> multiple commits.  IOW, it would be nice to see that the only thing\n> \"git splice A..B\" does is to prepare a series of instructions in a\n> file, e.g. .git/sequencer/todo, just like \"git cherry-pick A..B\"\n> would, and let the sequencer machinery to handle the sequencing.\n>\n> E.g. In a history like\n>\n>     ---o---A---o---B---X---Y---Z   HEAD\n>\n> \"git splice A..B\" command would write something like this:\n>\n>     reset to A\n>     pick X\n>     pick Y\n>     pick Z\n>\n> to the todo file and drive the sequencer.\n\nThat sounds great to me!  At this point sadly I'm currently a bit\nignorant of the intricacies of the sequencer, otherwise I might have\nadopted this approach from day 0.  But I'm pleased to be able to say\nthat under the hood, the way I implemented splice and transplant isn't\ntoo dissimilar to this: they both write \"todo\" files, under\n.git/splice and .git/transplant respectively, and then execute the\ninstructions in those files.  So hopefully it wouldn't be much work to\nbring them closer to the kind of format you describe above, and then\nfeed that to the sequencer instead of have them process the tasks\nthemselves.\n\n> As you notice, you would\n> need to extend the vocabulary of the sequencer a bit to allow\n> various things that the current users of the sequencer machinery do\n> not need, like resetting the HEAD to a specific commit, merging a\n> side branch, remembering the result of an operation, and referring\n> to such a commit in later operation.  For example, if you tell \"git\n> splice\" to expunge A from this sample history (I am not sure how you\n> express that operation in your UI):\n>\n>          B---C---D\n>         /         \\\n>     ---o---A---E---F---G   HEAD\n\nCurrently splice explicitly avoids editing history with merge commits,\nalthough this example has made me realise that there's a bug with the\nway it currently does that: it only checks that the removal and\ninsertion ranges are all non-merge commits before starting execution,\nwhereas it actually needs to check all the descendant commits too.\nFortunately that's easy to fix :-)\n\n> it might create a \"todo\" list like this to rebuild the history:\n>\n>     reset to A^\n>     pick B\n>     pick C\n>     pick D\n>     mark :1\n>     reset to A^\n>     pick E\n>     merge :1 using F's log message and conflict resolution as reference\n>     pick G\n>\n> to result in:\n>\n>          B---C---D\n>         /         \\\n>     ---o-------E---F---G   HEAD\n>\n> Do not pay too much attention to how the hypothetical \"extended todo\n> instruction set\" is spelled in the above illustration (e.g. I am not\n> advocating for multi-word command like \"reset to\"); these are only\n> to illustrate what kind of features would be needed for the job.  In\n> the final shape of the system, \"merge\" in the illustration above may\n> be a more succinct \"merge F :1\", for example (i.e. the first\n> parameter would name an existing merge to use as reference, the\n> remainder is a list of commits to be merged to the current HEAD),\n> just like \"pick X\" is a succinct way to say \"cherry-pick the change\n> introduced by existing commit X to HEAD, reusing X's log message\n> and author information\".\n\nYep, that all makes perfect sense.  It seems to me that there would be\nthree main strands of work required here:\n\n     (0) gather use cases for automated higher-level workflows\n         from users, so we're clear what kinds of problems are\n         most worth solving\n\n     (1) automate generation of instruction sequences which\n         reflect those workflows (or parts thereof)\n\n     (2) extend the sequencer as/when required by (1)\n\n> Something like that may have a place in the git-core, I would think.\n\nOK, good to know.\n\n> I am not sure if a bash script that calls rebase/cherry-pick/commit\n> manually can serve as a good \"universal mid-layer\" or just adding\n> another random command to the set of existing third-party commands\n> for \"higher-level workflows\".\n\nI'm not sure either.  It might or might not be, but I think a debate\non that topic would be worthwhile and something in which I'd be very\ninterested in taking part.\n\nMy first hunch is that if we were to attempt to design this\n\"mid-layer\" of operations, it would make sense to start with the more\nprimitive operations in that layer, and then build the more\nsophisticated ones later - on top of the primitives, if that made\nsense.\n\nFor example first we could focus on sequences which achieve simple\nthings like removing a range of commits from a branch where the\ndescendants of that range are all non-merge commits, and inserting a\nrange of commits into a branch which satisfies the same \"no merge\ncommits\" constraint.  This would achieve parity with git-splice.\n\nNext we could add support for the same operations with the \"no merge\ncommits\" constraint dropped, so that your example scenario above could\nbe handled correctly.\n\nThen we could add support for more complicated operations such as\ntransplants, and removing / transplanting a whole range of commits\nwhich can form an arbitrarily complex commit graph.  This last one\nsounds pretty hairy, which reinforces the value of starting simple.\n\nAlso, implementing the more primitive operations first would allow us\nto extend the sequencer's capabilities in a more incremental and\nrisk-averse manner.\n\nThanks a lot for the reply!  What would you recommend as the next\nsteps?\n\n\n[0] This has been discussed before, e.g.\n     https://public-inbox.org/git/20160528112417.GD11256@pacific.linksys.moosehall/\n"}]}