Git Mailing List Archive mirror
 help / color / mirror / Atom feed
* [PATCH 00/14] Introduce new `git replay` command
@ 2023-04-07  7:24 Christian Couder
  2023-04-07  7:24 ` [PATCH 01/14] replay: introduce new builtin Christian Couder
                   ` (17 more replies)
  0 siblings, 18 replies; 208+ messages in thread
From: Christian Couder @ 2023-04-07  7:24 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Christian Couder

# Intro

`git replay` has initially been developed entirely by Elijah Newren
mostly between June and July 2022 at:

https://github.com/newren/git/commits/replay

I took over a few months ago to polish and upstream it as GitLab is
interested in replacing libgit2, and for that purpose needs a command
to do server side (so without using a worktree) rebases, cherry-picks
and reverts.

I reduced the number of commits and features in this first patch
series, compared to what Elijah already developed. Especially I
stopped short of replaying merge commits and replaying
interactively. These and other features might be upstreamed in the
future after this patch series has graduated.

Thanks to Elijah, Patrick Steinhardt and Dscho for early reviews and
discussions.

Based on ae73b2c8f1 (The seventh batch, 2023-04-04)

# Quick Overview (from Elijah)

`git replay`, at a basic level, can perhaps be thought of as a
"default-to-dry-run rebase" -- meaning no updates to the working tree,
or to the index, or to any references.  However, it differs from
rebase in that it:

  * Works for branches that aren't checked out
  * Works in a bare repository
  * Can replay multiple branches simultaneously (with or without common
    history in the range being replayed)
  * Preserves relative topology by default (merges are replayed too)
  * Focuses on performance
  * Has several altered defaults as a result of the above

I sometimes think of `git replay` as "fast-replay", a patch-based
analogue to the snapshot-based fast-export & fast-import tools.

# Reasons for diverging from cherry-pick & rebase (from Elijah)

There are multiple reasons to diverge from the defaults in cherry-pick and
rebase.

* Server side needs

  * Both cherry-pick and rebase, via the sequencer, are heavily tied
    to updating the working tree, index, some refs, and a lot of
    control files with every commit replayed, and invoke a mess of
    hooks[1] that might be hard to avoid for backward compatibility
    reasons (at least, that's been brought up a few times on the
    list).

  * cherry-pick and rebase both fork various subprocesses
    unnecessarily, but somewhat intrinsically in part to ensure the
    same hooks are called that old scripted implementations would
    have called.

  * "Dry run" behavior, where there are no updates to worktree, index,
    or even refs might be important.

  * Should not assume users only want to operate on HEAD (see next
    section)

* Decapitate HEAD-centric assumptions

  * cherry-pick forces commits to be played on top of HEAD; inflexible.

  * rebase assumes the range of commits to be replayed is
    upstream..HEAD by default, though it allows one to replay
    upstream..otherbranch -- but it still forcibly and needlessly
    checks out otherbranch before starting to replay things.

  * Assuming HEAD is involved severely limits replaying multiple
    (possibly divergent) branches.

  * Once you stop assuming HEAD has a certain meaning, there's not
    much reason to have two separate commands anymore (except for the
    funny extra not-necessarily-compatible options both have gained
    over time).

  * (Micro issue: Assuming HEAD is involved also makes it harder for
    new users to learn what rebase means and does; it makes command
    lines hard to parse.  Not sure I want to harp on this too much, as
    I have a suspicion I might be creating a tool for experts with
    complicated use cases, but it's a minor quibble.)

* Performance

  * jj is slaughtering us on rebase speed[2].  I would like us to become
    competitive.  (I dropped a few comments in the link at [2] about why
    git is currently so bad.)

  * From [3], there was a simple 4-patch series in linux.git that took
    53 seconds to rebase.  Switching to ort dropped it to 16 seconds.
    While that sounds great, only 11 *milliseconds* were needed to do
    the actual merges.  That means almost *all* the time (>99%) was
    overhead!  Big offenders:

    * --reapply-cherry-picks should be the default

    * can_fast_forward() should be ripped out, and perhaps other extraneous
      revision walks

    * avoid updating working tree, index, refs, reflogs, and control
      structures except when needed (e.g. hitting a conflict, or operation
      finished)

  * Other performance ideas:

    * single-file control structures instead of directory of files

    * avoid forking subprocesses unless explicitly requested (e.g.
      --exec, --strategy, --run-hooks).  For example, definitely do not
      invoke `git commit` or `git merge`.

    * Sanitize hooks:

      * dispense with all per-commit hooks for sure (pre-commit,
        post-commit, post-checkout).

      * pre-rebase also seems to assume exactly 1 ref is written, and
        invoking it repeatedly would be stupid.  Plus, it's specific
        to "rebase".  So...ignore?  (Stolee's --ref-update option for
        rebase probably broke the pre-rebase assumptions already...)

      * post-rewrite hook might make sense, but fast-import got
        exempted, and I think of replay like a patch-based analogue
        to the snapshot-based fast-import.

    * When not running server side, resolve conflicts in a sparse-cone
      sparse-index worktree to reduce number of files written to a
      working tree.  (See below as well)

    * [High risk of possible premature optimization] Avoid large
      numbers of newly created loose objects, when replaying large
      numbers of commits.  Two possibilities: (1) Consider using
      tmp-objdir and pack objects from the tmp-objdir at end of
      exercise, (2) Lift code from git-fast-import to immediately
      stuff new objects into a pack?

* Multiple branches and non-checked out branches

  * The ability to operate on non-checked out branches also implies
    that we should generally be able to replay when in a dirty working
    tree (exception being when we expect to update HEAD and any of the
    dirty files is one that needs to be updated by the replay).

  * Also, if we are operating locally on a non-checked out branch and
    hit a conflict, we should have a way to resolve the conflict without
    messing with the user's work on their current branch.

    * Idea: new worktree with sparse cone + sparse index checkout,
      containing only files in the root directory, and whatever is
      necessary to get the conflicts

    * Companion to above idea: control structures should be written to
      $GIT_COMMON_DIR/replay-${worktree}, so users can have multiple
      replay sessions, and so we know which worktrees are associated
      with which replay operations.

  - [1] https://lore.kernel.org/git/pull.749.v3.git.git.1586044818132.gitgitgadget@gmail.com/
  - [2] https://github.com/martinvonz/jj/discussions/49
  - [3] https://lore.kernel.org/git/CABPp-BE48=97k_3tnNqXPjSEfA163F8hoE+HY0Zvz1SWB2B8EA@mail.gmail.com/

# Important limitations

* The code die()s if there are any conflict. No resumability. No nice
  output. No interactivity.

* No replaying merges, nor root commits. Only regular commits.

* Signed commits are not properly handled. It's not clear what to do
  to such commits when replaying on the server side.

# Commit overview

* 1/14 replay: introduce new builtin

     This creates a minimal `git replay` command by moving the code
     from the `fast-rebase` test helper from `t/helper/` into
     `builtin/` and doing some renames and a few other needed changes.

* - 2/14 replay: start using parse_options API
  - 3/14 replay: die() instead of failing assert()
  - 4/14 replay: introduce pick_regular_commit()
  - 5/14 replay: don't simplify history
  - 6/14 replay: add an important FIXME comment about gpg signing
  - 7/14 replay: remove progress and info output
  - 8/14 replay: remove HEAD related sanity check

     These slowly change the command to make it behave more like a
     regular commands and to start cleaning up its output. 

* 9/14 replay: very coarse worktree updating

     Make it handle conflicts in a very coarse way. This might not
     work on bare repos, but it allows existing tests to pass and it's
     nice to help cli users a bit when they get conflicts.

* 10/14 replay: make it a minimal server side command

     After the cleaning up in previous ommits, it's now time to
     radically change the way it works by stopping it to do ref
     updates, to update the index and worktree, to consider HEAD as
     special. Instead just make it output commands that should be
     passed to `git update-ref --stdin`.

* - 11/14 replay: use standard revision ranges
  - 12/14 replay: introduce guess_new_base()
  - 13/14 replay: add different modes
  - 14/14 replay: stop assuming replayed branches do not diverge

      These finish the clean up and add new interesting features at
      the same time, as well as related documentation and tests.

# Note about tests and documentation

Note that the `fast-rebase` test helper was used before this series in

t6429-merge-sequence-rename-caching.sh

So when `git replay` is created from `fast-rebase` in patch 1/14, this
test script is also converted to use `git replay`. This ensures that
`git replay` doesn't break too badly during the first 10 patches in
this patch series.

Tests and documentation are introduced specifically for `git replay`
only in 11/14 and later patches as it doesn't make much sense to
document and test behavior that we know is going to change soon. So
it's only when the command is crystalizing towards its final form that
we start documenting and testing it.

# Possibly controversial issues 

* bare or not bare: this series works towards a command with the end
  goal of it being usable and used on bare repos, contrary to existing
  commands like `git rebase` and `git cherry-pick`, but the tests
  currently don't check that, and in case of conflicts it won't
  currently work on bare repos. One reason for that is that existing
  tests in t6429 should continue to work, and one of these tests
  requires some output in case of conflict. And it's nice for users to
  get some help in case of conflict. It's also nice for users if
  commands that should work on both bare and non bare repos work well
  on non bare repos first as they are less likely to use them on bare
  repos. So let's have a command that works well on non-bare repos
  first, even if its end goal is to work fine on bare repos too. We
  plan to improve things for bare repos soon after this first patch
  series graduates.

* exit status: a successful, non-conflicted replay exits with code
  0. When the replay has conflicts, the exit status is 1. If the
  replay is not able to complete (or start) due to some kind of error,
  the exit status is something other than 0 or 1. It has been
  suggested in an internal review that conflicts might want to get a
  more specific error code as an error code of 1 might be quite easy
  to return by accident. It doesn't seem to me from their docs (which
  might want to be improved, I didn't look at the code) that other
  commands like `git merge` and `git rebase` exit with a special error
  code in case of conflict.

* to guess or not to guess: commit 12/14 introduces the
  guess_new_base() function which tries to find a base to rebase onto
  when the --onto option is not provided, making this option actually
  optional instead of mandatory. Given that it's an heuristic and the
  command end goal is to be used on server side, we might want to
  introduce this as an iterative improvement later. I still think it's
  interesting to have it in for now though, as it shows that --onto
  and --advance (which is introduced in the following commit) should
  indeed be options. If --onto was always mandatory in the series,
  people could argue that it shouldn't be an option and its argument
  should always be the first (unconditional) argument of the command.

* make worktree and index changes optional: commit 10/14 stops
  updating the index and worktree, but it might be better especially
  for cli users to make that optional. The issue is that this would
  make the command more complex while we are developing a number of
  important features. It seems to me that this should rather be done
  in an iterative improvement after the important features have
  landed.

* when and where to add tests and docs: although t6429 has tests that
  are changed to use the new command instead of the fast-rebase
  test-tool command as soon as the former is introduced, there is no
  specific test script and no doc for the new command until commit
  11/14 when standard revision ranges are used. This is done to avoid
  churn in tests and docs while the final form of the command hasn't
  crystalized enough. Adding tests and doc at this point makes this
  commit quite big and possibly more difficult to review than if they
  were in separate commits though. On the other hand when tests and
  docs are added in specific commits some reviewers say it would be
  better to introduce them when the related changes are made.

* --advance and --contained: these two advanced options might not
  belong to this first series and could perhaps be added in a followup
  series in separate commits. On the other hand the code for
  --contained seems involved with the code of --advance and it's nice
  to see soon that git replay can indeed do cherry-picking and rebase
  many refs at once, and this way fullfil these parts of its promise.

* replaying diverging branches: 14/14 the last patch in the series,
  which allow replaying diverging branches, can be seen as a
  fundamental fix or alternatively as adding an interesting
  feature. So it's debatable if it should be in its own patch along
  with its own tests as in this series, or if it should be merged into
  a previous patch and which one.

* only 2 patches: this patch series can be seen from a high level
  point of view as 1) introducing the new `git replay` command, and 2)
  using `git replay` to replace, and get rid of, the fast-rebase
  test-tool command. The fact that not much of the original
  fast-rebase code and interface is left would agree with that point
  of view. On the other hand, fast-rebase can also be seen as a first
  iteration towards `git replay`. So it can also make sense to see how
  `git replay` evolved from it.


Elijah Newren (14):
  replay: introduce new builtin
  replay: start using parse_options API
  replay: die() instead of failing assert()
  replay: introduce pick_regular_commit()
  replay: don't simplify history
  replay: add an important FIXME comment about gpg signing
  replay: remove progress and info output
  replay: remove HEAD related sanity check
  replay: very coarse worktree updating
  replay: make it a minimal server side command
  replay: use standard revision ranges
  replay: introduce guess_new_base()
  replay: add different modes
  replay: stop assuming replayed branches do not diverge

 .gitignore                               |   1 +
 Documentation/git-replay.txt             | 130 +++++++
 Makefile                                 |   2 +-
 builtin.h                                |   1 +
 builtin/replay.c                         | 419 +++++++++++++++++++++++
 command-list.txt                         |   1 +
 git.c                                    |   1 +
 t/helper/test-fast-rebase.c              | 233 -------------
 t/helper/test-tool.c                     |   1 -
 t/helper/test-tool.h                     |   1 -
 t/t3650-replay-basics.sh                 | 160 +++++++++
 t/t6429-merge-sequence-rename-caching.sh |  43 ++-
 12 files changed, 739 insertions(+), 254 deletions(-)
 create mode 100644 Documentation/git-replay.txt
 create mode 100644 builtin/replay.c
 delete mode 100644 t/helper/test-fast-rebase.c
 create mode 100755 t/t3650-replay-basics.sh

-- 
2.40.0.228.gb2eb5bb98e


^ permalink raw reply	[flat|nested] 208+ messages in thread

* [PATCH 01/14] replay: introduce new builtin
  2023-04-07  7:24 [PATCH 00/14] Introduce new `git replay` command Christian Couder
@ 2023-04-07  7:24 ` Christian Couder
  2023-04-07  7:24 ` [PATCH 02/14] replay: start using parse_options API Christian Couder
                   ` (16 subsequent siblings)
  17 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-04-07  7:24 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Christian Couder

From: Elijah Newren <newren@gmail.com>

For now, this is just a rename from `t/helper/test-fast-rebase.c` into
`builtin/replay.c` with minimal changes to make it build appropriately.

Subsequent commits will flesh out its capabilities and make it a more
standard regular builtin.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 .gitignore                                    |  1 +
 Makefile                                      |  2 +-
 builtin.h                                     |  1 +
 .../test-fast-rebase.c => builtin/replay.c    | 23 ++++------------
 command-list.txt                              |  1 +
 git.c                                         |  1 +
 t/helper/test-tool.c                          |  1 -
 t/helper/test-tool.h                          |  1 -
 t/t6429-merge-sequence-rename-caching.sh      | 27 +++++++------------
 9 files changed, 19 insertions(+), 39 deletions(-)
 rename t/helper/test-fast-rebase.c => builtin/replay.c (89%)

diff --git a/.gitignore b/.gitignore
index e875c59054..b5f025a296 100644
--- a/.gitignore
+++ b/.gitignore
@@ -135,6 +135,7 @@
 /git-remote-ext
 /git-repack
 /git-replace
+/git-replay
 /git-request-pull
 /git-rerere
 /git-reset
diff --git a/Makefile b/Makefile
index 60ab1a8b4f..44546f74dc 100644
--- a/Makefile
+++ b/Makefile
@@ -799,7 +799,6 @@ TEST_BUILTINS_OBJS += test-dump-split-index.o
 TEST_BUILTINS_OBJS += test-dump-untracked-cache.o
 TEST_BUILTINS_OBJS += test-env-helper.o
 TEST_BUILTINS_OBJS += test-example-decorate.o
-TEST_BUILTINS_OBJS += test-fast-rebase.o
 TEST_BUILTINS_OBJS += test-fsmonitor-client.o
 TEST_BUILTINS_OBJS += test-genrandom.o
 TEST_BUILTINS_OBJS += test-genzeros.o
@@ -1286,6 +1285,7 @@ BUILTIN_OBJS += builtin/remote-fd.o
 BUILTIN_OBJS += builtin/remote.o
 BUILTIN_OBJS += builtin/repack.o
 BUILTIN_OBJS += builtin/replace.o
+BUILTIN_OBJS += builtin/replay.o
 BUILTIN_OBJS += builtin/rerere.o
 BUILTIN_OBJS += builtin/reset.o
 BUILTIN_OBJS += builtin/rev-list.o
diff --git a/builtin.h b/builtin.h
index 46cc789789..6ad9efd6cd 100644
--- a/builtin.h
+++ b/builtin.h
@@ -204,6 +204,7 @@ int cmd_remote(int argc, const char **argv, const char *prefix);
 int cmd_remote_ext(int argc, const char **argv, const char *prefix);
 int cmd_remote_fd(int argc, const char **argv, const char *prefix);
 int cmd_repack(int argc, const char **argv, const char *prefix);
+int cmd_replay(int argc, const char **argv, const char *prefix);
 int cmd_rerere(int argc, const char **argv, const char *prefix);
 int cmd_reset(int argc, const char **argv, const char *prefix);
 int cmd_restore(int argc, const char **argv, const char *prefix);
diff --git a/t/helper/test-fast-rebase.c b/builtin/replay.c
similarity index 89%
rename from t/helper/test-fast-rebase.c
rename to builtin/replay.c
index b1edb92a03..154a976ca6 100644
--- a/t/helper/test-fast-rebase.c
+++ b/builtin/replay.c
@@ -1,18 +1,11 @@
 /*
- * "git fast-rebase" builtin command
- *
- * FAST: Forking Any Subprocesses (is) Taboo
- *
- * This is meant SOLELY as a demo of what is possible.  sequencer.c and
- * rebase.c should be refactored to use the ideas here, rather than attempting
- * to extend this file to replace those (unless Phillip or Dscho say that
- * refactoring is too hard and we need a clean slate, but I'm guessing that
- * refactoring is the better route).
+ * "git replay" builtin command
  */
 
 #define USE_THE_INDEX_VARIABLE
-#include "test-tool.h"
+#include "git-compat-util.h"
 
+#include "builtin.h"
 #include "cache-tree.h"
 #include "commit.h"
 #include "hex.h"
@@ -86,7 +79,7 @@ static struct commit *create_commit(struct tree *tree,
 	return (struct commit *)obj;
 }
 
-int cmd__fast_rebase(int argc, const char **argv)
+int cmd_replay(int argc, const char **argv, const char *prefix)
 {
 	struct commit *onto;
 	struct commit *last_commit = NULL, *last_picked_commit = NULL;
@@ -102,12 +95,6 @@ int cmd__fast_rebase(int argc, const char **argv)
 	struct strbuf branch_name = STRBUF_INIT;
 	int ret = 0;
 
-	/*
-	 * test-tool stuff doesn't set up the git directory by default; need to
-	 * do that manually.
-	 */
-	setup_git_directory();
-
 	if (argc == 2 && !strcmp(argv[1], "-h")) {
 		printf("Sorry, I am not a psychiatrist; I can not give you the help you need.  Oh, you meant usage...\n");
 		exit(129);
@@ -128,7 +115,7 @@ int cmd__fast_rebase(int argc, const char **argv)
 	if (repo_read_index(the_repository) < 0)
 		BUG("Could not read index");
 
-	repo_init_revisions(the_repository, &revs, NULL);
+	repo_init_revisions(the_repository, &revs, prefix);
 	revs.verbose_header = 1;
 	revs.max_parents = 1;
 	revs.cherry_mark = 1;
diff --git a/command-list.txt b/command-list.txt
index 54b2a50f5f..d74836ab21 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -160,6 +160,7 @@ git-reflog                              ancillarymanipulators           complete
 git-remote                              ancillarymanipulators           complete
 git-repack                              ancillarymanipulators           complete
 git-replace                             ancillarymanipulators           complete
+git-replay                              mainporcelain           history
 git-request-pull                        foreignscminterface             complete
 git-rerere                              ancillaryinterrogators
 git-reset                               mainporcelain           history
diff --git a/git.c b/git.c
index ae2134f29a..e4ce39fd4d 100644
--- a/git.c
+++ b/git.c
@@ -571,6 +571,7 @@ static struct cmd_struct commands[] = {
 	{ "remote-fd", cmd_remote_fd, NO_PARSEOPT },
 	{ "repack", cmd_repack, RUN_SETUP },
 	{ "replace", cmd_replace, RUN_SETUP },
+	{ "replay", cmd_replay, RUN_SETUP },
 	{ "rerere", cmd_rerere, RUN_SETUP },
 	{ "reset", cmd_reset, RUN_SETUP },
 	{ "restore", cmd_restore, RUN_SETUP | NEED_WORK_TREE },
diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
index abe8a785eb..9ca1586de7 100644
--- a/t/helper/test-tool.c
+++ b/t/helper/test-tool.c
@@ -30,7 +30,6 @@ static struct test_cmd cmds[] = {
 	{ "dump-untracked-cache", cmd__dump_untracked_cache },
 	{ "env-helper", cmd__env_helper },
 	{ "example-decorate", cmd__example_decorate },
-	{ "fast-rebase", cmd__fast_rebase },
 	{ "fsmonitor-client", cmd__fsmonitor_client },
 	{ "genrandom", cmd__genrandom },
 	{ "genzeros", cmd__genzeros },
diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h
index ea2672436c..a03bbfc6b2 100644
--- a/t/helper/test-tool.h
+++ b/t/helper/test-tool.h
@@ -24,7 +24,6 @@ int cmd__dump_untracked_cache(int argc, const char **argv);
 int cmd__dump_reftable(int argc, const char **argv);
 int cmd__env_helper(int argc, const char **argv);
 int cmd__example_decorate(int argc, const char **argv);
-int cmd__fast_rebase(int argc, const char **argv);
 int cmd__fsmonitor_client(int argc, const char **argv);
 int cmd__genrandom(int argc, const char **argv);
 int cmd__genzeros(int argc, const char **argv);
diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh
index d02fa16614..40505c9054 100755
--- a/t/t6429-merge-sequence-rename-caching.sh
+++ b/t/t6429-merge-sequence-rename-caching.sh
@@ -71,8 +71,7 @@ test_expect_success 'caching renames does not preclude finding new ones' '
 
 		git switch upstream &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
-		#git cherry-pick upstream~1..topic
+		git replay --onto HEAD upstream~1 topic &&
 
 		git ls-files >tracked-files &&
 		test_line_count = 2 tracked-files &&
@@ -140,8 +139,7 @@ test_expect_success 'cherry-pick both a commit and its immediate revert' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
-		#git cherry-pick upstream~1..topic &&
+		git replay --onto HEAD upstream~1 topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 1 calls
@@ -199,8 +197,7 @@ test_expect_success 'rename same file identically, then reintroduce it' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
-		#git cherry-pick upstream~1..topic &&
+		git replay --onto HEAD upstream~1 topic &&
 
 		git ls-files >tracked &&
 		test_line_count = 2 tracked &&
@@ -276,8 +273,7 @@ test_expect_success 'rename same file identically, then add file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
-		#git cherry-pick upstream~1..topic &&
+		git replay --onto HEAD upstream~1 topic &&
 
 		git ls-files >tracked &&
 		test_line_count = 4 tracked &&
@@ -353,8 +349,7 @@ test_expect_success 'cached dir rename does not prevent noticing later conflict'
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test_must_fail test-tool fast-rebase --onto HEAD upstream~1 topic >output &&
-		#git cherry-pick upstream..topic &&
+		test_must_fail git replay --onto HEAD upstream~1 topic >output &&
 
 		grep CONFLICT..rename/rename output &&
 
@@ -455,8 +450,7 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
-		#git cherry-pick upstream..topic &&
+		git replay --onto HEAD upstream~1 topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls &&
@@ -521,8 +515,7 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
-		#git cherry-pick upstream..topic &&
+		git replay --onto HEAD upstream~1 topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 3 calls &&
@@ -623,8 +616,7 @@ test_expect_success 'caching renames only on upstream side, part 1' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
-		#git cherry-pick upstream..topic &&
+		git replay --onto HEAD upstream~1 topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 1 calls &&
@@ -681,8 +673,7 @@ test_expect_success 'caching renames only on upstream side, part 2' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
-		#git cherry-pick upstream..topic &&
+		git replay --onto HEAD upstream~1 topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls &&
-- 
2.40.0.228.gb2eb5bb98e


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH 02/14] replay: start using parse_options API
  2023-04-07  7:24 [PATCH 00/14] Introduce new `git replay` command Christian Couder
  2023-04-07  7:24 ` [PATCH 01/14] replay: introduce new builtin Christian Couder
@ 2023-04-07  7:24 ` Christian Couder
  2023-04-07  7:24 ` [PATCH 03/14] replay: die() instead of failing assert() Christian Couder
                   ` (15 subsequent siblings)
  17 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-04-07  7:24 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Christian Couder

From: Elijah Newren <newren@gmail.com>

Instead of manually parsing arguments, let's start using the parse_options
API. This way this new builtin will look more standard, and in some
upcoming commits will more easily be able to handle more command line
options.

Note that we plan to later use standard revision ranges instead of
hardcoded "<oldbase> <branch>" arguments. When we will use standard
revision ranges, it will be easier to check if there are no spurious
arguments if we keep ARGV[0], so let's call parse_options() with
PARSE_OPT_KEEP_ARGV0 even if we don't need ARGV[0] right now to avoid
some useless code churn.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 44 ++++++++++++++++++++++++++++++++------------
 1 file changed, 32 insertions(+), 12 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 154a976ca6..ca931dee41 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -11,6 +11,7 @@
 #include "hex.h"
 #include "lockfile.h"
 #include "merge-ort.h"
+#include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
 #include "sequencer.h"
@@ -82,6 +83,7 @@ static struct commit *create_commit(struct tree *tree,
 int cmd_replay(int argc, const char **argv, const char *prefix)
 {
 	struct commit *onto;
+	const char *onto_name = NULL;
 	struct commit *last_commit = NULL, *last_picked_commit = NULL;
 	struct object_id head;
 	struct lock_file lock = LOCK_INIT;
@@ -95,16 +97,32 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct strbuf branch_name = STRBUF_INIT;
 	int ret = 0;
 
-	if (argc == 2 && !strcmp(argv[1], "-h")) {
-		printf("Sorry, I am not a psychiatrist; I can not give you the help you need.  Oh, you meant usage...\n");
-		exit(129);
+	const char * const replay_usage[] = {
+		N_("git replay --onto <newbase> <oldbase> <branch>"),
+		NULL
+	};
+	struct option replay_options[] = {
+		OPT_STRING(0, "onto", &onto_name,
+			   N_("revision"),
+			   N_("replay onto given commit")),
+		OPT_END()
+	};
+
+	argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
+			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
+
+	if (!onto_name) {
+		error(_("option --onto is mandatory"));
+		usage_with_options(replay_usage, replay_options);
 	}
 
-	if (argc != 5 || strcmp(argv[1], "--onto"))
-		die("usage: read the code, figure out how to use it, then do so");
+	if (argc != 3) {
+		error(_("bad number of arguments"));
+		usage_with_options(replay_usage, replay_options);
+	}
 
-	onto = peel_committish(argv[2]);
-	strbuf_addf(&branch_name, "refs/heads/%s", argv[4]);
+	onto = peel_committish(onto_name);
+	strbuf_addf(&branch_name, "refs/heads/%s", argv[2]);
 
 	/* Sanity check */
 	if (get_oid("HEAD", &head))
@@ -116,6 +134,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		BUG("Could not read index");
 
 	repo_init_revisions(the_repository, &revs, prefix);
+
 	revs.verbose_header = 1;
 	revs.max_parents = 1;
 	revs.cherry_mark = 1;
@@ -124,7 +143,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	revs.right_only = 1;
 	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
 	revs.topo_order = 1;
-	strvec_pushl(&rev_walk_args, "", argv[4], "--not", argv[3], NULL);
+
+	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
 
 	if (setup_revisions(rev_walk_args.nr, rev_walk_args.v, &revs, NULL) > 1) {
 		ret = error(_("unhandled options"));
@@ -187,8 +207,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			       &last_commit->object.oid,
 			       &last_picked_commit->object.oid,
 			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
-			error(_("could not update %s"), argv[4]);
-			die("Failed to update %s", argv[4]);
+			error(_("could not update %s"), argv[2]);
+			die("Failed to update %s", argv[2]);
 		}
 		if (create_symref("HEAD", branch_name.buf, reflog_msg.buf) < 0)
 			die(_("unable to update HEAD"));
@@ -203,8 +223,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			       &last_commit->object.oid,
 			       &head,
 			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
-			error(_("could not update %s"), argv[4]);
-			die("Failed to update %s", argv[4]);
+			error(_("could not update %s"), argv[2]);
+			die("Failed to update %s", argv[2]);
 		}
 	}
 	if (write_locked_index(&the_index, &lock,
-- 
2.40.0.228.gb2eb5bb98e


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH 03/14] replay: die() instead of failing assert()
  2023-04-07  7:24 [PATCH 00/14] Introduce new `git replay` command Christian Couder
  2023-04-07  7:24 ` [PATCH 01/14] replay: introduce new builtin Christian Couder
  2023-04-07  7:24 ` [PATCH 02/14] replay: start using parse_options API Christian Couder
@ 2023-04-07  7:24 ` Christian Couder
  2023-04-07  7:24 ` [PATCH 04/14] replay: introduce pick_regular_commit() Christian Couder
                   ` (14 subsequent siblings)
  17 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-04-07  7:24 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Christian Couder

From: Elijah Newren <newren@gmail.com>

It's not a good idea for regular Git commands to use an assert() to
check for things that could happen but are not supported.

Let's die() with an explanation of the issue instead.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index ca931dee41..16efac11ab 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -170,7 +170,12 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 
 		fprintf(stderr, "Rebasing %s...\r",
 			oid_to_hex(&commit->object.oid));
-		assert(commit->parents && !commit->parents->next);
+
+		if (!commit->parents)
+			die(_("replaying down to root commit is not supported yet!"));
+		if (commit->parents->next)
+			die(_("replaying merge commits is not supported yet!"));
+
 		base = commit->parents->item;
 
 		next_tree = get_commit_tree(commit);
-- 
2.40.0.228.gb2eb5bb98e


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH 04/14] replay: introduce pick_regular_commit()
  2023-04-07  7:24 [PATCH 00/14] Introduce new `git replay` command Christian Couder
                   ` (2 preceding siblings ...)
  2023-04-07  7:24 ` [PATCH 03/14] replay: die() instead of failing assert() Christian Couder
@ 2023-04-07  7:24 ` Christian Couder
  2023-04-07  7:24 ` [PATCH 05/14] replay: don't simplify history Christian Couder
                   ` (13 subsequent siblings)
  17 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-04-07  7:24 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Christian Couder

From: Elijah Newren <newren@gmail.com>

Let's refactor the code to handle a regular commit (a commit that is
neither a root commit nor a merge commit) into a single function instead
of keeping it inside cmd_replay().

This is good for separation of concerns, and this will help further work
in the future to replay merge commits.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 54 ++++++++++++++++++++++++++++++------------------
 1 file changed, 34 insertions(+), 20 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 16efac11ab..ea12d4c8fe 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -80,6 +80,35 @@ static struct commit *create_commit(struct tree *tree,
 	return (struct commit *)obj;
 }
 
+static struct commit *pick_regular_commit(struct commit *pickme,
+					  struct commit *last_commit,
+					  struct merge_options *merge_opt,
+					  struct merge_result *result)
+{
+	struct commit *base;
+	struct tree *pickme_tree, *base_tree;
+
+	base = pickme->parents->item;
+
+	pickme_tree = get_commit_tree(pickme);
+	base_tree = get_commit_tree(base);
+
+	merge_opt->branch2 = short_commit_name(pickme);
+	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
+
+	merge_incore_nonrecursive(merge_opt,
+				  base_tree,
+				  result->tree,
+				  pickme_tree,
+				  result);
+
+	free((char*)merge_opt->ancestor);
+	merge_opt->ancestor = NULL;
+	if (!result->clean)
+		return NULL;
+	return create_commit(result->tree, pickme, last_commit);
+}
+
 int cmd_replay(int argc, const char **argv, const char *prefix)
 {
 	struct commit *onto;
@@ -91,7 +120,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct rev_info revs;
 	struct commit *commit;
 	struct merge_options merge_opt;
-	struct tree *next_tree, *base_tree, *head_tree;
+	struct tree *head_tree;
 	struct merge_result result;
 	struct strbuf reflog_msg = STRBUF_INIT;
 	struct strbuf branch_name = STRBUF_INIT;
@@ -166,7 +195,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	result.tree = head_tree;
 	last_commit = onto;
 	while ((commit = get_revision(&revs))) {
-		struct commit *base;
+		struct commit *pick;
 
 		fprintf(stderr, "Rebasing %s...\r",
 			oid_to_hex(&commit->object.oid));
@@ -176,26 +205,11 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		if (commit->parents->next)
 			die(_("replaying merge commits is not supported yet!"));
 
-		base = commit->parents->item;
-
-		next_tree = get_commit_tree(commit);
-		base_tree = get_commit_tree(base);
-
-		merge_opt.branch2 = short_commit_name(commit);
-		merge_opt.ancestor = xstrfmt("parent of %s", merge_opt.branch2);
-
-		merge_incore_nonrecursive(&merge_opt,
-					  base_tree,
-					  result.tree,
-					  next_tree,
-					  &result);
-
-		free((char*)merge_opt.ancestor);
-		merge_opt.ancestor = NULL;
-		if (!result.clean)
+		pick = pick_regular_commit(commit, last_commit, &merge_opt, &result);
+		if (!pick)
 			break;
+		last_commit = pick;
 		last_picked_commit = commit;
-		last_commit = create_commit(result.tree, commit, last_commit);
 	}
 
 	merge_switch_to_result(&merge_opt, head_tree, &result, 1, !result.clean);
-- 
2.40.0.228.gb2eb5bb98e


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH 05/14] replay: don't simplify history
  2023-04-07  7:24 [PATCH 00/14] Introduce new `git replay` command Christian Couder
                   ` (3 preceding siblings ...)
  2023-04-07  7:24 ` [PATCH 04/14] replay: introduce pick_regular_commit() Christian Couder
@ 2023-04-07  7:24 ` Christian Couder
  2023-04-07  7:24 ` [PATCH 06/14] replay: add an important FIXME comment about gpg signing Christian Couder
                   ` (12 subsequent siblings)
  17 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-04-07  7:24 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Christian Couder

From: Elijah Newren <newren@gmail.com>

Let's set the rev walking options we need after calling
setup_revisions() instead of before. This makes it clearer which options
we need.

Also we don't want history simplification, as we want to deal with all
the commits in the affected range.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 15 ++++++---------
 1 file changed, 6 insertions(+), 9 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index ea12d4c8fe..59ebbc3b8f 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -164,15 +164,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 
 	repo_init_revisions(the_repository, &revs, prefix);
 
-	revs.verbose_header = 1;
-	revs.max_parents = 1;
-	revs.cherry_mark = 1;
-	revs.limited = 1;
-	revs.reverse = 1;
-	revs.right_only = 1;
-	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
-	revs.topo_order = 1;
-
 	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
 
 	if (setup_revisions(rev_walk_args.nr, rev_walk_args.v, &revs, NULL) > 1) {
@@ -180,6 +171,12 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		goto cleanup;
 	}
 
+	/* requirements/overrides for revs */
+	revs.reverse = 1;
+	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
+	revs.topo_order = 1;
+	revs.simplify_history = 0;
+
 	strvec_clear(&rev_walk_args);
 
 	if (prepare_revision_walk(&revs) < 0) {
-- 
2.40.0.228.gb2eb5bb98e


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH 06/14] replay: add an important FIXME comment about gpg signing
  2023-04-07  7:24 [PATCH 00/14] Introduce new `git replay` command Christian Couder
                   ` (4 preceding siblings ...)
  2023-04-07  7:24 ` [PATCH 05/14] replay: don't simplify history Christian Couder
@ 2023-04-07  7:24 ` Christian Couder
  2023-04-07  7:24 ` [PATCH 07/14] replay: remove progress and info output Christian Couder
                   ` (11 subsequent siblings)
  17 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-04-07  7:24 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Christian Couder

From: Elijah Newren <newren@gmail.com>

We want to be able to handle signed commits in some way in the future,
but we are not ready to do it now. So for the time being let's just add
a FIXME comment to remind us about it.

These are different ways we could handle them:

  - in case of a cli user and if there was an interactive mode, we could
    perhaps ask if the user wants to sign again
  - we could add an option to just fail if there are signed commits

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 59ebbc3b8f..ffd7ce1fde 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -54,7 +54,7 @@ static struct commit *create_commit(struct tree *tree,
 	struct object *obj;
 	struct commit_list *parents = NULL;
 	char *author;
-	char *sign_commit = NULL;
+	char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
 	struct commit_extra_header *extra;
 	struct strbuf msg = STRBUF_INIT;
 	const char *out_enc = get_commit_output_encoding();
-- 
2.40.0.228.gb2eb5bb98e


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH 07/14] replay: remove progress and info output
  2023-04-07  7:24 [PATCH 00/14] Introduce new `git replay` command Christian Couder
                   ` (5 preceding siblings ...)
  2023-04-07  7:24 ` [PATCH 06/14] replay: add an important FIXME comment about gpg signing Christian Couder
@ 2023-04-07  7:24 ` Christian Couder
  2023-04-07  7:24 ` [PATCH 08/14] replay: remove HEAD related sanity check Christian Couder
                   ` (10 subsequent siblings)
  17 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-04-07  7:24 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Christian Couder

From: Elijah Newren <newren@gmail.com>

The replay command will be changed in a follow up commit, so that it
will not update refs directly, but instead it will print on stdout a
list of commands that can be consumed by `git update-ref --stdin`.

We don't want this output to be polluted by its current low value
output, so let's just remove the latter.

In the future, when the command gets an option to update refs by
itself, it will make a lot of sense to display a progress meter, but
we are not there yet.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 7 +------
 1 file changed, 1 insertion(+), 6 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index ffd7ce1fde..0a23ea70dc 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -186,7 +186,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
-	merge_opt.show_rename_progress = 1;
+	merge_opt.show_rename_progress = 0;
 	merge_opt.branch1 = "HEAD";
 	head_tree = get_commit_tree(onto);
 	result.tree = head_tree;
@@ -194,9 +194,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	while ((commit = get_revision(&revs))) {
 		struct commit *pick;
 
-		fprintf(stderr, "Rebasing %s...\r",
-			oid_to_hex(&commit->object.oid));
-
 		if (!commit->parents)
 			die(_("replaying down to root commit is not supported yet!"));
 		if (commit->parents->next)
@@ -215,7 +212,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		exit(128);
 
 	if (result.clean) {
-		fprintf(stderr, "\nDone.\n");
 		strbuf_addf(&reflog_msg, "finish rebase %s onto %s",
 			    oid_to_hex(&last_picked_commit->object.oid),
 			    oid_to_hex(&last_commit->object.oid));
@@ -232,7 +228,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		prime_cache_tree(the_repository, the_repository->index,
 				 result.tree);
 	} else {
-		fprintf(stderr, "\nAborting: Hit a conflict.\n");
 		strbuf_addf(&reflog_msg, "rebase progress up to %s",
 			    oid_to_hex(&last_picked_commit->object.oid));
 		if (update_ref(reflog_msg.buf, "HEAD",
-- 
2.40.0.228.gb2eb5bb98e


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH 08/14] replay: remove HEAD related sanity check
  2023-04-07  7:24 [PATCH 00/14] Introduce new `git replay` command Christian Couder
                   ` (6 preceding siblings ...)
  2023-04-07  7:24 ` [PATCH 07/14] replay: remove progress and info output Christian Couder
@ 2023-04-07  7:24 ` Christian Couder
  2023-04-07  7:24 ` [PATCH 09/14] replay: very coarse worktree updating Christian Couder
                   ` (9 subsequent siblings)
  17 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-04-07  7:24 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Christian Couder

From: Elijah Newren <newren@gmail.com>

We want replay to be a command that can be used on the server side on
any branch, not just the current one, so we are going to stop updating
HEAD in a future commit.

A "sanity check" that makes sure we are replaying the current branch
doesn't make sense anymore. Let's remove it.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 8 +-------
 1 file changed, 1 insertion(+), 7 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 0a23ea70dc..a331887d12 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -114,7 +114,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct commit *onto;
 	const char *onto_name = NULL;
 	struct commit *last_commit = NULL, *last_picked_commit = NULL;
-	struct object_id head;
 	struct lock_file lock = LOCK_INIT;
 	struct strvec rev_walk_args = STRVEC_INIT;
 	struct rev_info revs;
@@ -153,11 +152,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	onto = peel_committish(onto_name);
 	strbuf_addf(&branch_name, "refs/heads/%s", argv[2]);
 
-	/* Sanity check */
-	if (get_oid("HEAD", &head))
-		die(_("Cannot read HEAD"));
-	assert(oideq(&onto->object.oid, &head));
-
 	repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
 	if (repo_read_index(the_repository) < 0)
 		BUG("Could not read index");
@@ -232,7 +226,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			    oid_to_hex(&last_picked_commit->object.oid));
 		if (update_ref(reflog_msg.buf, "HEAD",
 			       &last_commit->object.oid,
-			       &head,
+			       &onto->object.oid,
 			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
 			error(_("could not update %s"), argv[2]);
 			die("Failed to update %s", argv[2]);
-- 
2.40.0.228.gb2eb5bb98e


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH 09/14] replay: very coarse worktree updating
  2023-04-07  7:24 [PATCH 00/14] Introduce new `git replay` command Christian Couder
                   ` (7 preceding siblings ...)
  2023-04-07  7:24 ` [PATCH 08/14] replay: remove HEAD related sanity check Christian Couder
@ 2023-04-07  7:24 ` Christian Couder
  2023-04-07  7:24 ` [PATCH 10/14] replay: make it a minimal server side command Christian Couder
                   ` (8 subsequent siblings)
  17 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-04-07  7:24 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Christian Couder

From: Elijah Newren <newren@gmail.com>

In case of conflict, let's just update the worktree and index. And then
let's just die() as this command doesn't have ways to handle conflicts
yet.

Note that we might want to improve this behavior in the case of a bare
repository in a future commit.

We also have to lock the index only after all the commits have been
picked, so that in case of conflict the index is not already locked.
Note that this locking of the index will be removed in a following
commit as we will not want to modify it anymore.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 35 +++++++++++++++++++++++++++++------
 1 file changed, 29 insertions(+), 6 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index a331887d12..9c795c05a7 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -152,10 +152,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	onto = peel_committish(onto_name);
 	strbuf_addf(&branch_name, "refs/heads/%s", argv[2]);
 
-	repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
-	if (repo_read_index(the_repository) < 0)
-		BUG("Could not read index");
-
 	repo_init_revisions(the_repository, &revs, prefix);
 
 	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
@@ -194,12 +190,39 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			die(_("replaying merge commits is not supported yet!"));
 
 		pick = pick_regular_commit(commit, last_commit, &merge_opt, &result);
-		if (!pick)
-			break;
+		if (!pick) {
+			/* TODO: handle conflicts in sparse worktree instead */
+			struct object_id head;
+			struct tree *head_tree;
+			struct lock_file lock = LOCK_INIT;
+
+			repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
+			if (repo_read_index(the_repository) < 0)
+				BUG("Could not read index");
+
+			get_oid("HEAD", &head);
+			head_tree = parse_tree_indirect(&head);
+			printf("Switching from %s to %s.\n",
+			       oid_to_hex(&head_tree->object.oid),
+			       oid_to_hex(&result.tree->object.oid));
+			merge_switch_to_result(&merge_opt, head_tree, &result,
+					       1, 1);
+			if (write_locked_index(&the_index, &lock,
+					       COMMIT_LOCK | SKIP_IF_UNCHANGED))
+				die(_("unable to write %s"), get_index_file());
+
+			die(_("failure to pick %s; cannot handle conflicts yet"),
+			    oid_to_hex(&commit->object.oid));
+		}
+
 		last_commit = pick;
 		last_picked_commit = commit;
 	}
 
+	repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
+	if (repo_read_index(the_repository) < 0)
+		BUG("Could not read index");
+
 	merge_switch_to_result(&merge_opt, head_tree, &result, 1, !result.clean);
 
 	if (result.clean < 0)
-- 
2.40.0.228.gb2eb5bb98e


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH 10/14] replay: make it a minimal server side command
  2023-04-07  7:24 [PATCH 00/14] Introduce new `git replay` command Christian Couder
                   ` (8 preceding siblings ...)
  2023-04-07  7:24 ` [PATCH 09/14] replay: very coarse worktree updating Christian Couder
@ 2023-04-07  7:24 ` Christian Couder
  2023-04-07  7:24 ` [PATCH 11/14] replay: use standard revision ranges Christian Couder
                   ` (7 subsequent siblings)
  17 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-04-07  7:24 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Christian Couder

From: Elijah Newren <newren@gmail.com>

We want this command to be a minimal command that just does server side
picking of commits, displaying the results on stdout for higher level
scripts to consume.

So let's simplify it:
  * remove the worktree and index reading/writing,
  * remove the ref (and reflog) updating,
  * remove the assumptions tying us to HEAD, since (a) this is not a
    rebase and (b) we want to be able to pick commits in a bare repo,
    i.e. to/from branches that are not checked out and not the main
    branch,
  * remove unneeded includes,
  * handle rebasing multiple branches by printing on stdout the update
    ref commands that should be performed.

The output can be piped into `git update-ref --stdin` for the ref
updates to happen.

In the future to make it easier for users to use this command
directly maybe an option can be added to automatically pipe its output
into `git update-ref`.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c                         | 86 ++++++++----------------
 t/t6429-merge-sequence-rename-caching.sh | 32 ++++++---
 2 files changed, 52 insertions(+), 66 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 9c795c05a7..119cfecfe7 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -6,17 +6,13 @@
 #include "git-compat-util.h"
 
 #include "builtin.h"
-#include "cache-tree.h"
-#include "commit.h"
 #include "hex.h"
 #include "lockfile.h"
 #include "merge-ort.h"
 #include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
-#include "sequencer.h"
 #include "strvec.h"
-#include "tree.h"
 
 static const char *short_commit_name(struct commit *commit)
 {
@@ -93,6 +89,7 @@ static struct commit *pick_regular_commit(struct commit *pickme,
 	pickme_tree = get_commit_tree(pickme);
 	base_tree = get_commit_tree(base);
 
+	merge_opt->branch1 = short_commit_name(last_commit);
 	merge_opt->branch2 = short_commit_name(pickme);
 	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
 
@@ -113,15 +110,12 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 {
 	struct commit *onto;
 	const char *onto_name = NULL;
-	struct commit *last_commit = NULL, *last_picked_commit = NULL;
-	struct lock_file lock = LOCK_INIT;
+	struct commit *last_commit = NULL;
 	struct strvec rev_walk_args = STRVEC_INIT;
 	struct rev_info revs;
 	struct commit *commit;
 	struct merge_options merge_opt;
-	struct tree *head_tree;
 	struct merge_result result;
-	struct strbuf reflog_msg = STRBUF_INIT;
 	struct strbuf branch_name = STRBUF_INIT;
 	int ret = 0;
 
@@ -177,20 +171,19 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
 	merge_opt.show_rename_progress = 0;
-	merge_opt.branch1 = "HEAD";
-	head_tree = get_commit_tree(onto);
-	result.tree = head_tree;
+	result.tree = get_commit_tree(onto);
 	last_commit = onto;
 	while ((commit = get_revision(&revs))) {
-		struct commit *pick;
+		const struct name_decoration *decoration;
 
 		if (!commit->parents)
 			die(_("replaying down to root commit is not supported yet!"));
 		if (commit->parents->next)
 			die(_("replaying merge commits is not supported yet!"));
 
-		pick = pick_regular_commit(commit, last_commit, &merge_opt, &result);
-		if (!pick) {
+		last_commit = pick_regular_commit(commit, last_commit, &merge_opt, &result);
+
+		if (!last_commit) {
 			/* TODO: handle conflicts in sparse worktree instead */
 			struct object_id head;
 			struct tree *head_tree;
@@ -215,54 +208,31 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			    oid_to_hex(&commit->object.oid));
 		}
 
-		last_commit = pick;
-		last_picked_commit = commit;
-	}
-
-	repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
-	if (repo_read_index(the_repository) < 0)
-		BUG("Could not read index");
-
-	merge_switch_to_result(&merge_opt, head_tree, &result, 1, !result.clean);
-
-	if (result.clean < 0)
-		exit(128);
-
-	if (result.clean) {
-		strbuf_addf(&reflog_msg, "finish rebase %s onto %s",
-			    oid_to_hex(&last_picked_commit->object.oid),
-			    oid_to_hex(&last_commit->object.oid));
-		if (update_ref(reflog_msg.buf, branch_name.buf,
-			       &last_commit->object.oid,
-			       &last_picked_commit->object.oid,
-			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
-			error(_("could not update %s"), argv[2]);
-			die("Failed to update %s", argv[2]);
-		}
-		if (create_symref("HEAD", branch_name.buf, reflog_msg.buf) < 0)
-			die(_("unable to update HEAD"));
-
-		prime_cache_tree(the_repository, the_repository->index,
-				 result.tree);
-	} else {
-		strbuf_addf(&reflog_msg, "rebase progress up to %s",
-			    oid_to_hex(&last_picked_commit->object.oid));
-		if (update_ref(reflog_msg.buf, "HEAD",
-			       &last_commit->object.oid,
-			       &onto->object.oid,
-			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
-			error(_("could not update %s"), argv[2]);
-			die("Failed to update %s", argv[2]);
+		decoration = get_name_decoration(&commit->object);
+		if (!decoration)
+			continue;
+
+		while (decoration) {
+			if (decoration->type == DECORATION_REF_LOCAL) {
+				printf("update %s %s %s\n",
+				       decoration->name,
+				       oid_to_hex(&last_commit->object.oid),
+				       oid_to_hex(&commit->object.oid));
+			}
+			decoration = decoration->next;
 		}
 	}
-	if (write_locked_index(&the_index, &lock,
-			       COMMIT_LOCK | SKIP_IF_UNCHANGED))
-		die(_("unable to write %s"), get_index_file());
 
-	ret = (result.clean == 0);
+	/* Cleanup */
+	merge_finalize(&merge_opt, &result);
+	ret = result.clean;
+
 cleanup:
-	strbuf_release(&reflog_msg);
 	strbuf_release(&branch_name);
 	release_revisions(&revs);
-	return ret;
+
+	/* Return */
+	if (ret < 0)
+		exit(128);
+	return ret ? 0 : 1;
 }
diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh
index 40505c9054..bfdf7f30b3 100755
--- a/t/t6429-merge-sequence-rename-caching.sh
+++ b/t/t6429-merge-sequence-rename-caching.sh
@@ -71,7 +71,9 @@ test_expect_success 'caching renames does not preclude finding new ones' '
 
 		git switch upstream &&
 
-		git replay --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		git ls-files >tracked-files &&
 		test_line_count = 2 tracked-files &&
@@ -139,7 +141,9 @@ test_expect_success 'cherry-pick both a commit and its immediate revert' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 1 calls
@@ -197,7 +201,9 @@ test_expect_success 'rename same file identically, then reintroduce it' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		git ls-files >tracked &&
 		test_line_count = 2 tracked &&
@@ -273,7 +279,9 @@ test_expect_success 'rename same file identically, then add file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		git ls-files >tracked &&
 		test_line_count = 4 tracked &&
@@ -450,7 +458,9 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls &&
@@ -515,7 +525,9 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 3 calls &&
@@ -616,7 +628,9 @@ test_expect_success 'caching renames only on upstream side, part 1' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 1 calls &&
@@ -673,7 +687,9 @@ test_expect_success 'caching renames only on upstream side, part 2' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls &&
-- 
2.40.0.228.gb2eb5bb98e


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH 11/14] replay: use standard revision ranges
  2023-04-07  7:24 [PATCH 00/14] Introduce new `git replay` command Christian Couder
                   ` (9 preceding siblings ...)
  2023-04-07  7:24 ` [PATCH 10/14] replay: make it a minimal server side command Christian Couder
@ 2023-04-07  7:24 ` Christian Couder
  2023-04-14 14:09   ` Derrick Stolee
  2023-04-07  7:24 ` [PATCH 12/14] replay: introduce guess_new_base() Christian Couder
                   ` (6 subsequent siblings)
  17 siblings, 1 reply; 208+ messages in thread
From: Christian Couder @ 2023-04-07  7:24 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Christian Couder

From: Elijah Newren <newren@gmail.com>

Instead of the fixed "<oldbase> <branch>" arguments, the replay
command now accepts "<revision-range>..." arguments in a similar
way as many other Git commands. This makes its interface more
standard and more flexible.

Also as the interface of the command is now mostly finalized,
we can add some documentation as well as testcases to make sure
the command will continue to work as designed in the future.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 Documentation/git-replay.txt             | 86 ++++++++++++++++++++++++
 builtin/replay.c                         | 21 ++----
 t/t3650-replay-basics.sh                 | 63 +++++++++++++++++
 t/t6429-merge-sequence-rename-caching.sh | 18 ++---
 4 files changed, 162 insertions(+), 26 deletions(-)
 create mode 100644 Documentation/git-replay.txt
 create mode 100755 t/t3650-replay-basics.sh

diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
new file mode 100644
index 0000000000..7a83f70343
--- /dev/null
+++ b/Documentation/git-replay.txt
@@ -0,0 +1,86 @@
+git-replay(1)
+=============
+
+NAME
+----
+git-replay - Replay commits on a different base, without touching working tree
+
+
+SYNOPSIS
+--------
+[verse]
+'git replay' --onto <newbase> <revision-range>...
+
+DESCRIPTION
+-----------
+
+Takes a range of commits, and replays them onto a new location.  Does
+not touch the working tree or index, and does not update any
+references.  However, the output of this command is meant to be used
+as input to `git update-ref --stdin`, which would update the relevant
+branches.
+
+OPTIONS
+-------
+
+--onto <newbase>::
+	Starting point at which to create the new commits.  May be any
+	valid commit, and not just an existing branch name.
++
+The update-ref commands in the output will update the branch(es)
+in the revision range to point at the new commits (in other
+words, this mimics a rebase operation).
+
+<revision-range>::
+	Range of commits to replay; see "Specifying Ranges" in
+	linkgit:git-rev-parse.
+
+OUTPUT
+------
+
+When there are no conflicts, the output of this command is usable as
+input to `git update-ref --stdin`.  It is basically of the form:
+
+	update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
+	update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
+	update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
+
+where the number of refs updated depend on the arguments passed.
+
+EXIT STATUS
+-----------
+
+For a successful, non-conflicted replay, the exit status is 0.  When
+the replay has conflicts, the exit status is 1.  If the replay is not
+able to complete (or start) due to some kind of error, the exit status
+is something other than 0 or 1.
+
+EXAMPLES
+--------
+
+To simply rebase mybranch onto target:
+
+------------
+$ git replay --onto target origin/main..mybranch
+update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
+------------
+
+When calling `git replay`, one does not need to specify a range of
+commits to replay using the syntax `A..B`; any range expression will
+do:
+
+------------
+$ git replay --onto origin/main ^base branch1 branch2 branch3
+update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
+update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
+update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
+------------
+
+This will simultaneously rebase branch1, branch2, and branch3 -- all
+commits they have since base, playing them on top of origin/main.
+These three branches may have commits on top of base that they have in
+common, but that does not need to be the case.
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/builtin/replay.c b/builtin/replay.c
index 119cfecfe7..63513ea6f1 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -12,7 +12,6 @@
 #include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
-#include "strvec.h"
 
 static const char *short_commit_name(struct commit *commit)
 {
@@ -111,16 +110,14 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct commit *onto;
 	const char *onto_name = NULL;
 	struct commit *last_commit = NULL;
-	struct strvec rev_walk_args = STRVEC_INIT;
 	struct rev_info revs;
 	struct commit *commit;
 	struct merge_options merge_opt;
 	struct merge_result result;
-	struct strbuf branch_name = STRBUF_INIT;
 	int ret = 0;
 
 	const char * const replay_usage[] = {
-		N_("git replay --onto <newbase> <oldbase> <branch>"),
+		N_("git replay --onto <newbase> <revision-range>..."),
 		NULL
 	};
 	struct option replay_options[] = {
@@ -138,20 +135,13 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		usage_with_options(replay_usage, replay_options);
 	}
 
-	if (argc != 3) {
-		error(_("bad number of arguments"));
-		usage_with_options(replay_usage, replay_options);
-	}
-
 	onto = peel_committish(onto_name);
-	strbuf_addf(&branch_name, "refs/heads/%s", argv[2]);
 
 	repo_init_revisions(the_repository, &revs, prefix);
 
-	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
-
-	if (setup_revisions(rev_walk_args.nr, rev_walk_args.v, &revs, NULL) > 1) {
-		ret = error(_("unhandled options"));
+	argc = setup_revisions(argc, argv, &revs, NULL);
+	if (argc > 1) {
+		ret = error(_("unrecognized argument: %s"), argv[1]);
 		goto cleanup;
 	}
 
@@ -161,8 +151,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	revs.topo_order = 1;
 	revs.simplify_history = 0;
 
-	strvec_clear(&rev_walk_args);
-
 	if (prepare_revision_walk(&revs) < 0) {
 		ret = error(_("error preparing revisions"));
 		goto cleanup;
@@ -228,7 +216,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	ret = result.clean;
 
 cleanup:
-	strbuf_release(&branch_name);
 	release_revisions(&revs);
 
 	/* Return */
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
new file mode 100755
index 0000000000..f55b71763a
--- /dev/null
+++ b/t/t3650-replay-basics.sh
@@ -0,0 +1,63 @@
+#!/bin/sh
+
+test_description='basic git replay tests'
+
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
+. ./test-lib.sh
+
+GIT_AUTHOR_NAME=author@name
+GIT_AUTHOR_EMAIL=bogus@email@address
+export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL
+
+test_expect_success 'setup' '
+	test_commit A &&
+	test_commit B &&
+
+	git switch -c topic1 &&
+	test_commit C &&
+	git switch -c topic2 &&
+	test_commit D &&
+	test_commit E &&
+	git switch topic1 &&
+	test_commit F &&
+	git switch -c topic3 &&
+	test_commit G &&
+	test_commit H &&
+	git switch -c topic4 main &&
+	test_commit I &&
+	test_commit J &&
+
+	git switch -c next main &&
+	test_commit K &&
+	git merge -m "Merge topic1" topic1 &&
+	git merge -m "Merge topic2" topic2 &&
+	git merge -m "Merge topic3" topic3 &&
+	>evil &&
+	git add evil &&
+	git commit --amend &&
+	git merge -m "Merge topic4" topic4 &&
+
+	git switch main &&
+	test_commit L &&
+	test_commit M
+'
+
+test_expect_success 'using replay to rebase two branches, one on top of other' '
+	git replay --onto main topic1..topic2 >result &&
+
+	test_line_count = 1 result &&
+
+	git log --format=%s $(cut -f 3 -d " " result) >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/topic2 " >expect &&
+	printf "%s " $(cut -f 3 -d " " result) >>expect &&
+	git rev-parse topic2 >>expect &&
+
+	test_cmp expect result
+'
+
+test_done
diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh
index bfdf7f30b3..8f3c394f0e 100755
--- a/t/t6429-merge-sequence-rename-caching.sh
+++ b/t/t6429-merge-sequence-rename-caching.sh
@@ -71,7 +71,7 @@ test_expect_success 'caching renames does not preclude finding new ones' '
 
 		git switch upstream &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -141,7 +141,7 @@ test_expect_success 'cherry-pick both a commit and its immediate revert' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -201,7 +201,7 @@ test_expect_success 'rename same file identically, then reintroduce it' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -279,7 +279,7 @@ test_expect_success 'rename same file identically, then add file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -357,7 +357,7 @@ test_expect_success 'cached dir rename does not prevent noticing later conflict'
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test_must_fail git replay --onto HEAD upstream~1 topic >output &&
+		test_must_fail git replay --onto HEAD upstream~1..topic >output &&
 
 		grep CONFLICT..rename/rename output &&
 
@@ -458,7 +458,7 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -525,7 +525,7 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -628,7 +628,7 @@ test_expect_success 'caching renames only on upstream side, part 1' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -687,7 +687,7 @@ test_expect_success 'caching renames only on upstream side, part 2' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
-- 
2.40.0.228.gb2eb5bb98e


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH 12/14] replay: introduce guess_new_base()
  2023-04-07  7:24 [PATCH 00/14] Introduce new `git replay` command Christian Couder
                   ` (10 preceding siblings ...)
  2023-04-07  7:24 ` [PATCH 11/14] replay: use standard revision ranges Christian Couder
@ 2023-04-07  7:24 ` Christian Couder
  2023-04-07  7:24 ` [PATCH 13/14] replay: add different modes Christian Couder
                   ` (5 subsequent siblings)
  17 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-04-07  7:24 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Christian Couder

From: Elijah Newren <newren@gmail.com>

In many cases the `--onto` option is not necessary as we can guess the
branch we would like to replay onto.

So let's introduce guess_new_base() for that purpose and make `--onto`
optional.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 Documentation/git-replay.txt |  8 ++++-
 builtin/replay.c             | 61 +++++++++++++++++++++++++++++++-----
 2 files changed, 61 insertions(+), 8 deletions(-)

diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
index 7a83f70343..ce2cafc42e 100644
--- a/Documentation/git-replay.txt
+++ b/Documentation/git-replay.txt
@@ -9,7 +9,7 @@ git-replay - Replay commits on a different base, without touching working tree
 SYNOPSIS
 --------
 [verse]
-'git replay' --onto <newbase> <revision-range>...
+'git replay' [--onto <newbase>] <revision-range>...
 
 DESCRIPTION
 -----------
@@ -20,6 +20,12 @@ references.  However, the output of this command is meant to be used
 as input to `git update-ref --stdin`, which would update the relevant
 branches.
 
+When the `--onto <newbase>` option is not passed, the commits will be
+replayed onto a base guessed from the `<revision-range>`.  For example
+if the `<revision-range>` is `origin/main..mybranch` then `mybranch`
+was probably based on an old version of `origin/main`, so we will
+replay it on the newest version of that branch.
+
 OPTIONS
 -------
 
diff --git a/builtin/replay.c b/builtin/replay.c
index 63513ea6f1..af948af73c 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -75,6 +75,54 @@ static struct commit *create_commit(struct tree *tree,
 	return (struct commit *)obj;
 }
 
+static struct commit *guess_new_base(struct rev_cmdline_info *info)
+{
+	struct commit *new_base = NULL;
+	int i, bottom_commits = 0;
+
+	/*
+	 * When the user specifies e.g.
+	 *   git replay origin/main..mybranch
+	 *   git replay ^origin/next mybranch1 mybranch2
+	 * we want to be able to determine where to replay the commits.  In
+	 * these examples, the branches are probably based on an old version
+	 * of either origin/main or origin/next, so we want to replay on the
+	 * newest version of that branch.  In contrast we would want to error
+	 * out if they ran
+	 *   git replay ^origin/master ^origin/next mybranch
+	 *   git replay mybranch~2..mybranch
+	 * the first of those because there's no unique base to choose, and
+	 * the second because they'd likely just be replaying commits on top
+	 * of the same commit and not making any difference.
+	 */
+	for (i = 0; i < info->nr; i++) {
+		struct rev_cmdline_entry *e = info->rev + i;
+		struct object_id oid;
+		char *fullname = NULL;
+
+		if (!(e->flags & BOTTOM))
+			continue;
+
+		/*
+		 * We need a unique base commit to know where to replay; error
+		 * out if not unique.
+		 *
+		 * Also, we usually don't want to replay commits on the same
+		 * base they started on, so only accept this as the base if
+		 * it uniquely names some ref.
+		 */
+		if (bottom_commits++ ||
+		    dwim_ref(e->name, strlen(e->name), &oid, &fullname, 0) != 1)
+			die(_("cannot determine where to replay commits; please specify --onto"));
+
+		free(fullname);
+		new_base = lookup_commit_reference_gently(the_repository,
+							  &e->item->oid, 1);
+	}
+
+	return new_base;
+}
+
 static struct commit *pick_regular_commit(struct commit *pickme,
 					  struct commit *last_commit,
 					  struct merge_options *merge_opt,
@@ -117,7 +165,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	int ret = 0;
 
 	const char * const replay_usage[] = {
-		N_("git replay --onto <newbase> <revision-range>..."),
+		N_("git replay [--onto <newbase>] <revision-range>..."),
 		NULL
 	};
 	struct option replay_options[] = {
@@ -130,12 +178,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
 			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
 
-	if (!onto_name) {
-		error(_("option --onto is mandatory"));
-		usage_with_options(replay_usage, replay_options);
-	}
-
-	onto = peel_committish(onto_name);
 
 	repo_init_revisions(the_repository, &revs, prefix);
 
@@ -151,6 +193,11 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	revs.topo_order = 1;
 	revs.simplify_history = 0;
 
+	if (onto_name)
+		onto = peel_committish(onto_name);
+	else
+		onto = guess_new_base(&revs.cmdline);
+
 	if (prepare_revision_walk(&revs) < 0) {
 		ret = error(_("error preparing revisions"));
 		goto cleanup;
-- 
2.40.0.228.gb2eb5bb98e


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH 13/14] replay: add different modes
  2023-04-07  7:24 [PATCH 00/14] Introduce new `git replay` command Christian Couder
                   ` (11 preceding siblings ...)
  2023-04-07  7:24 ` [PATCH 12/14] replay: introduce guess_new_base() Christian Couder
@ 2023-04-07  7:24 ` Christian Couder
  2023-04-07  7:24 ` [PATCH 14/14] replay: stop assuming replayed branches do not diverge Christian Couder
                   ` (4 subsequent siblings)
  17 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-04-07  7:24 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Christian Couder

From: Elijah Newren <newren@gmail.com>

There is already a 'rebase' mode with `--onto`. Let's add an 'advance' or
'cherry-pick' mode with `--advance`. This new mode will make the target
branch advance as we replay commits onto it.

While at it, let's also add a `--contained` that can be used along with
`--onto` to rebase all the branches contained in the <revision-range>
argument.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 Documentation/git-replay.txt |  58 +++++++++--
 builtin/replay.c             | 185 +++++++++++++++++++++++++++++------
 t/t3650-replay-basics.sh     |  45 +++++++++
 3 files changed, 247 insertions(+), 41 deletions(-)

diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
index ce2cafc42e..d714c72188 100644
--- a/Documentation/git-replay.txt
+++ b/Documentation/git-replay.txt
@@ -9,7 +9,7 @@ git-replay - Replay commits on a different base, without touching working tree
 SYNOPSIS
 --------
 [verse]
-'git replay' [--onto <newbase>] <revision-range>...
+'git replay' [--contained] [--onto <newbase> | --advance <branch>] <revision-range>...
 
 DESCRIPTION
 -----------
@@ -20,11 +20,12 @@ references.  However, the output of this command is meant to be used
 as input to `git update-ref --stdin`, which would update the relevant
 branches.
 
-When the `--onto <newbase>` option is not passed, the commits will be
-replayed onto a base guessed from the `<revision-range>`.  For example
-if the `<revision-range>` is `origin/main..mybranch` then `mybranch`
-was probably based on an old version of `origin/main`, so we will
-replay it on the newest version of that branch.
+When neither the `--onto <newbase>` option nor the
+`--advance <branch>` option are passed, the commits will be replayed
+onto a base guessed from the `<revision-range>`.  For example if the
+`<revision-range>` is `origin/main..mybranch` then `mybranch` was
+probably based on an old version of `origin/main`, so we will replay
+it on the newest version of that branch.
 
 OPTIONS
 -------
@@ -33,9 +34,17 @@ OPTIONS
 	Starting point at which to create the new commits.  May be any
 	valid commit, and not just an existing branch name.
 +
-The update-ref commands in the output will update the branch(es)
-in the revision range to point at the new commits (in other
-words, this mimics a rebase operation).
+When `--onto` is specified, the update-ref command(s) in the output will
+update the branch(es) in the revision range to point at the new
+commits (in other words, this mimics a rebase operation).
+
+--advance <branch>::
+	Starting point at which to create the new commits; must be a
+	branch name.
++
+When `--advance` is specified, the update-ref command(s) in the output
+will update the branch passed as an argument to `--advance` to point at
+the new commits (in other words, this mimics a cherry-pick operation).
 
 <revision-range>::
 	Range of commits to replay; see "Specifying Ranges" in
@@ -51,7 +60,10 @@ input to `git update-ref --stdin`.  It is basically of the form:
 	update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
 	update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
 
-where the number of refs updated depend on the arguments passed.
+where the number of refs updated depend on the arguments passed.  When
+using `--advance`, the number of refs updated is always one, but for
+`--onto`, it can be one or more (rebasing multiple branches
+simultaneously is supported).
 
 EXIT STATUS
 -----------
@@ -71,6 +83,32 @@ $ git replay --onto target origin/main..mybranch
 update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
 ------------
 
+To cherry-pick the commits from mybranch onto target:
+
+------------
+$ git replay --advance target origin/main..mybranch
+update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}
+------------
+
+Note that the first two examples replay the exact same commits and on
+top of the exact same new base, they only differ in that the first
+provides instructions to make mybranch point at the new commits and
+the second provides instructions to make target point at them.
+
+What if you have a stack of branches, one depending upon another, and
+you'd really like to rebase the whole set?
+
+------------
+$ git replay --contained --onto origin/main origin/main..tipbranch
+update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
+update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
+update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}
+------------
+
+In contrast, trying to do this with rebase would require 3 separate
+rebases, eacho of which involves a different <ONTO> and <UPSTREAM> and
+forces you to first check out each branch in turn.
+
 When calling `git replay`, one does not need to specify a range of
 commits to replay using the syntax `A..B`; any range expression will
 do:
diff --git a/builtin/replay.c b/builtin/replay.c
index af948af73c..63b3ad518e 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -12,6 +12,7 @@
 #include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
+#include "strmap.h"
 
 static const char *short_commit_name(struct commit *commit)
 {
@@ -75,10 +76,24 @@ static struct commit *create_commit(struct tree *tree,
 	return (struct commit *)obj;
 }
 
-static struct commit *guess_new_base(struct rev_cmdline_info *info)
+struct ref_info {
+	struct commit *onto;
+	struct strset positive_refs;
+	struct strset negative_refs;
+	int positive_refexprs;
+	int negative_refexprs;
+};
+
+static void get_ref_information(struct rev_cmdline_info *cmd_info,
+				struct ref_info *ref_info)
 {
-	struct commit *new_base = NULL;
-	int i, bottom_commits = 0;
+	int i;
+
+	ref_info->onto = NULL;
+	strset_init(&ref_info->positive_refs);
+	strset_init(&ref_info->negative_refs);
+	ref_info->positive_refexprs = 0;
+	ref_info->negative_refexprs = 0;
 
 	/*
 	 * When the user specifies e.g.
@@ -95,32 +110,110 @@ static struct commit *guess_new_base(struct rev_cmdline_info *info)
 	 * the second because they'd likely just be replaying commits on top
 	 * of the same commit and not making any difference.
 	 */
-	for (i = 0; i < info->nr; i++) {
-		struct rev_cmdline_entry *e = info->rev + i;
+	for (i = 0; i < cmd_info->nr; i++) {
+		struct rev_cmdline_entry *e = cmd_info->rev + i;
 		struct object_id oid;
+		const char *refexpr = e->name;
 		char *fullname = NULL;
+		int can_uniquely_dwim = 1;
+
+		if (*refexpr == '^')
+			refexpr++;
+		if (dwim_ref(refexpr, strlen(refexpr), &oid, &fullname, 0) != 1)
+			can_uniquely_dwim = 0;
+
+		if (e->flags & BOTTOM) {
+			if (can_uniquely_dwim)
+				strset_add(&ref_info->negative_refs, fullname);
+			if (!ref_info->negative_refexprs)
+				ref_info->onto = lookup_commit_reference_gently(the_repository,
+										&e->item->oid, 1);
+			ref_info->negative_refexprs++;
+		} else {
+			if (can_uniquely_dwim)
+				strset_add(&ref_info->positive_refs, fullname);
+			ref_info->positive_refexprs++;
+		}
 
-		if (!(e->flags & BOTTOM))
-			continue;
+		free(fullname);
+	}
+}
 
+static void determine_replay_mode(struct rev_cmdline_info *cmd_info,
+				  const char *onto_name,
+				  const char **advance_name,
+				  struct commit **onto,
+				  struct strset **update_refs)
+{
+	struct ref_info rinfo;
+
+	get_ref_information(cmd_info, &rinfo);
+	if (!rinfo.positive_refexprs)
+		die(_("need some commits to replay"));
+	if (onto_name && *advance_name)
+		die(_("--onto and --advance are incompatible"));
+	else if (onto_name) {
+		*onto = peel_committish(onto_name);
+		if (rinfo.positive_refexprs <
+		    strset_get_size(&rinfo.positive_refs))
+			die(_("all positive revisions given must be references"));
+	} else if (*advance_name) {
+		struct object_id oid;
+		char *fullname = NULL;
+
+		*onto = peel_committish(*advance_name);
+		if (dwim_ref(*advance_name, strlen(*advance_name),
+			     &oid, &fullname, 0) == 1) {
+			*advance_name = fullname;
+		} else {
+			die(_("argument to --advance must be a reference"));
+		}
+		if (rinfo.positive_refexprs > 1)
+			die(_("cannot advance target with multiple source branches because ordering would be ill-defined"));
+	} else {
+		int positive_refs_complete = (
+			rinfo.positive_refexprs ==
+			strset_get_size(&rinfo.positive_refs));
+		int negative_refs_complete = (
+			rinfo.negative_refexprs ==
+			strset_get_size(&rinfo.negative_refs));
 		/*
-		 * We need a unique base commit to know where to replay; error
-		 * out if not unique.
-		 *
-		 * Also, we usually don't want to replay commits on the same
-		 * base they started on, so only accept this as the base if
-		 * it uniquely names some ref.
+		 * We need either positive_refs_complete or
+		 * negative_refs_complete, but not both.
 		 */
-		if (bottom_commits++ ||
-		    dwim_ref(e->name, strlen(e->name), &oid, &fullname, 0) != 1)
-			die(_("cannot determine where to replay commits; please specify --onto"));
-
-		free(fullname);
-		new_base = lookup_commit_reference_gently(the_repository,
-							  &e->item->oid, 1);
+		if (rinfo.negative_refexprs > 0 &&
+		    positive_refs_complete == negative_refs_complete)
+			die(_("cannot implicitly determine whether this is an --advance or --onto operation"));
+		if (negative_refs_complete) {
+			struct hashmap_iter iter;
+			struct strmap_entry *entry;
+
+			if (rinfo.negative_refexprs == 0)
+				die(_("all positive revisions given must be references"));
+			else if (rinfo.negative_refexprs > 1)
+				die(_("cannot implicitly determine whether this is an --advance or --onto operation"));
+			else if (rinfo.positive_refexprs > 1)
+				die(_("cannot advance target with multiple source branches because ordering would be ill-defined"));
+
+			/* Only one entry, but we have to loop to get it */
+			strset_for_each_entry(&rinfo.negative_refs,
+					      &iter, entry) {
+				*advance_name = entry->key;
+			}
+		} else { /* positive_refs_complete */
+			if (rinfo.negative_refexprs > 1)
+				die(_("cannot implicitly determine correct base for --onto"));
+			if (rinfo.negative_refexprs == 1)
+				*onto = rinfo.onto;
+		}
 	}
-
-	return new_base;
+	if (!*advance_name) {
+		*update_refs = xcalloc(1, sizeof(**update_refs));
+		**update_refs = rinfo.positive_refs;
+		memset(&rinfo.positive_refs, 0, sizeof(**update_refs));
+	}
+	strset_clear(&rinfo.negative_refs);
+	strset_clear(&rinfo.positive_refs);
 }
 
 static struct commit *pick_regular_commit(struct commit *pickme,
@@ -155,29 +248,41 @@ static struct commit *pick_regular_commit(struct commit *pickme,
 
 int cmd_replay(int argc, const char **argv, const char *prefix)
 {
-	struct commit *onto;
+	const char *advance_name = NULL;
+	struct commit *onto = NULL;
 	const char *onto_name = NULL;
-	struct commit *last_commit = NULL;
+	int contained = 0;
+
 	struct rev_info revs;
+	struct commit *last_commit = NULL;
 	struct commit *commit;
 	struct merge_options merge_opt;
 	struct merge_result result;
+	struct strset *update_refs = NULL;
 	int ret = 0;
 
 	const char * const replay_usage[] = {
-		N_("git replay [--onto <newbase>] <revision-range>..."),
+		N_("git replay [--contained] [--onto <newbase> | --advance <branch>] <revision-range>..."),
 		NULL
 	};
 	struct option replay_options[] = {
+		OPT_STRING(0, "advance", &advance_name,
+			   N_("branch"),
+			   N_("make replay advance given branch")),
 		OPT_STRING(0, "onto", &onto_name,
 			   N_("revision"),
 			   N_("replay onto given commit")),
+		OPT_BOOL(0, "contained", &contained,
+			 N_("advance all branches contained in revision-range")),
 		OPT_END()
 	};
 
 	argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
 			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
 
+	if (advance_name && contained)
+		die(_("options '%s' and '%s' cannot be used together"),
+		    "--advance", "--contained");
 
 	repo_init_revisions(the_repository, &revs, prefix);
 
@@ -193,10 +298,11 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	revs.topo_order = 1;
 	revs.simplify_history = 0;
 
-	if (onto_name)
-		onto = peel_committish(onto_name);
-	else
-		onto = guess_new_base(&revs.cmdline);
+	determine_replay_mode(&revs.cmdline, onto_name, &advance_name,
+			      &onto, &update_refs);
+
+	if (!onto) /* FIXME: Should handle replaying down to root commit */
+		die("Replaying down to root commit is not supported yet!");
 
 	if (prepare_revision_walk(&revs) < 0) {
 		ret = error(_("error preparing revisions"));
@@ -206,6 +312,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
 	merge_opt.show_rename_progress = 0;
+
 	result.tree = get_commit_tree(onto);
 	last_commit = onto;
 	while ((commit = get_revision(&revs))) {
@@ -243,12 +350,16 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			    oid_to_hex(&commit->object.oid));
 		}
 
+		/* Update any necessary branches */
+		if (advance_name)
+			continue;
 		decoration = get_name_decoration(&commit->object);
 		if (!decoration)
 			continue;
-
 		while (decoration) {
-			if (decoration->type == DECORATION_REF_LOCAL) {
+			if (decoration->type == DECORATION_REF_LOCAL &&
+			    (contained || strset_contains(update_refs,
+							  decoration->name))) {
 				printf("update %s %s %s\n",
 				       decoration->name,
 				       oid_to_hex(&last_commit->object.oid),
@@ -258,11 +369,23 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		}
 	}
 
+	/* In --advance mode, advance the target ref */
+	if (result.clean == 1 && advance_name) {
+		printf("update %s %s %s\n",
+		       advance_name,
+		       oid_to_hex(&last_commit->object.oid),
+		       oid_to_hex(&onto->object.oid));
+	}
+
 	/* Cleanup */
 	merge_finalize(&merge_opt, &result);
 	ret = result.clean;
 
 cleanup:
+	if (update_refs) {
+		strset_clear(update_refs);
+		free(update_refs);
+	}
 	release_revisions(&revs);
 
 	/* Return */
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index f55b71763a..976032ad18 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -60,4 +60,49 @@ test_expect_success 'using replay to rebase two branches, one on top of other' '
 	test_cmp expect result
 '
 
+test_expect_success 'using replay to perform basic cherry-pick' '
+	# The differences between this test and the last one are:
+	#   --advance vs --onto
+	# 2nd field of result is refs/heads/main vs. refs/heads/topic2
+	# 4th field of result is hash for main instead of hash for topic2
+
+	git replay --advance main topic1..topic2 >result &&
+
+	test_line_count = 1 result &&
+
+	git log --format=%s $(cut -f 3 -d " " result) >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/main " >expect &&
+	printf "%s " $(cut -f 3 -d " " result) >>expect &&
+	git rev-parse main >>expect &&
+
+	test_cmp expect result
+'
+
+test_expect_success 'using replay to also rebase a contained branch' '
+	git replay --contained --onto main main..topic3 >result &&
+
+	test_line_count = 2 result &&
+	cut -f 3 -d " " result >new-branch-tips &&
+
+	git log --format=%s $(head -n 1 new-branch-tips) >actual &&
+	test_write_lines F C M L B A >expect &&
+	test_cmp expect actual &&
+
+	git log --format=%s $(tail -n 1 new-branch-tips) >actual &&
+	test_write_lines H G F C M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/topic1 " >expect &&
+	printf "%s " $(head -n 1 new-branch-tips) >>expect &&
+	git rev-parse topic1 >>expect &&
+	printf "update refs/heads/topic3 " >>expect &&
+	printf "%s " $(tail -n 1 new-branch-tips) >>expect &&
+	git rev-parse topic3 >>expect &&
+
+	test_cmp expect result
+'
+
 test_done
-- 
2.40.0.228.gb2eb5bb98e


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH 14/14] replay: stop assuming replayed branches do not diverge
  2023-04-07  7:24 [PATCH 00/14] Introduce new `git replay` command Christian Couder
                   ` (12 preceding siblings ...)
  2023-04-07  7:24 ` [PATCH 13/14] replay: add different modes Christian Couder
@ 2023-04-07  7:24 ` Christian Couder
  2023-04-14 10:12 ` [PATCH 00/14] Introduce new `git replay` command Phillip Wood
                   ` (3 subsequent siblings)
  17 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-04-07  7:24 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Christian Couder

From: Elijah Newren <newren@gmail.com>

The replay command is able to replay multiple branches but when some of
them are based on other replayed branches, their commit should be
replayed onto already replayed commits.

For this purpose, let's store the replayed commit and its original
commit in a key value store, so that we can easily find and reused a
replayed commit instead of the original one.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c         | 44 ++++++++++++++++++++++++++--------
 t/t3650-replay-basics.sh | 52 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 86 insertions(+), 10 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 63b3ad518e..71815d5ca3 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -216,20 +216,33 @@ static void determine_replay_mode(struct rev_cmdline_info *cmd_info,
 	strset_clear(&rinfo.positive_refs);
 }
 
+static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,
+				    struct commit *commit,
+				    struct commit *fallback)
+{
+	khint_t pos = kh_get_oid_map(replayed_commits, commit->object.oid);
+	if (pos == kh_end(replayed_commits))
+		return fallback;
+	return kh_value(replayed_commits, pos);
+}
+
 static struct commit *pick_regular_commit(struct commit *pickme,
-					  struct commit *last_commit,
+					  kh_oid_map_t *replayed_commits,
+					  struct commit *onto,
 					  struct merge_options *merge_opt,
 					  struct merge_result *result)
 {
-	struct commit *base;
+	struct commit *base, *replayed_base;
 	struct tree *pickme_tree, *base_tree;
 
 	base = pickme->parents->item;
+	replayed_base = mapped_commit(replayed_commits, base, onto);
 
+	result->tree = get_commit_tree(replayed_base);
 	pickme_tree = get_commit_tree(pickme);
 	base_tree = get_commit_tree(base);
 
-	merge_opt->branch1 = short_commit_name(last_commit);
+	merge_opt->branch1 = short_commit_name(replayed_base);
 	merge_opt->branch2 = short_commit_name(pickme);
 	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
 
@@ -243,7 +256,7 @@ static struct commit *pick_regular_commit(struct commit *pickme,
 	merge_opt->ancestor = NULL;
 	if (!result->clean)
 		return NULL;
-	return create_commit(result->tree, pickme, last_commit);
+	return create_commit(result->tree, pickme, replayed_base);
 }
 
 int cmd_replay(int argc, const char **argv, const char *prefix)
@@ -259,6 +272,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct merge_options merge_opt;
 	struct merge_result result;
 	struct strset *update_refs = NULL;
+	kh_oid_map_t *replayed_commits;
 	int ret = 0;
 
 	const char * const replay_usage[] = {
@@ -312,18 +326,20 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
 	merge_opt.show_rename_progress = 0;
-
-	result.tree = get_commit_tree(onto);
 	last_commit = onto;
+	replayed_commits = kh_init_oid_map();
 	while ((commit = get_revision(&revs))) {
 		const struct name_decoration *decoration;
+		khint_t pos;
+		int hr;
 
 		if (!commit->parents)
 			die(_("replaying down to root commit is not supported yet!"));
 		if (commit->parents->next)
 			die(_("replaying merge commits is not supported yet!"));
 
-		last_commit = pick_regular_commit(commit, last_commit, &merge_opt, &result);
+		last_commit = pick_regular_commit(commit, replayed_commits, onto,
+						  &merge_opt, &result);
 
 		if (!last_commit) {
 			/* TODO: handle conflicts in sparse worktree instead */
@@ -350,6 +366,13 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			    oid_to_hex(&commit->object.oid));
 		}
 
+		/* Record commit -> last_commit mapping */
+		pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr);
+		if (hr == 0)
+			BUG("Duplicate rewritten commit: %s\n",
+			    oid_to_hex(&commit->object.oid));
+		kh_value(replayed_commits, pos) = last_commit;
+
 		/* Update any necessary branches */
 		if (advance_name)
 			continue;
@@ -379,13 +402,14 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 
 	/* Cleanup */
 	merge_finalize(&merge_opt, &result);
-	ret = result.clean;
-
-cleanup:
+	kh_destroy_oid_map(replayed_commits);
 	if (update_refs) {
 		strset_clear(update_refs);
 		free(update_refs);
 	}
+	ret = result.clean;
+
+cleanup:
 	release_revisions(&revs);
 
 	/* Return */
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 976032ad18..b13bb7502c 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -105,4 +105,56 @@ test_expect_success 'using replay to also rebase a contained branch' '
 	test_cmp expect result
 '
 
+test_expect_success 'using replay to rebase multiple divergent branches' '
+	git replay --onto main ^topic1 topic2 topic4 >result &&
+
+	test_line_count = 2 result &&
+	cut -f 3 -d " " result >new-branch-tips &&
+
+	git log --format=%s $(head -n 1 new-branch-tips) >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual &&
+
+	git log --format=%s $(tail -n 1 new-branch-tips) >actual &&
+	test_write_lines J I M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/topic2 " >expect &&
+	printf "%s " $(head -n 1 new-branch-tips) >>expect &&
+	git rev-parse topic2 >>expect &&
+	printf "update refs/heads/topic4 " >>expect &&
+	printf "%s " $(tail -n 1 new-branch-tips) >>expect &&
+	git rev-parse topic4 >>expect &&
+
+	test_cmp expect result
+'
+
+test_expect_success 'using replay to rebase multiple divergent branches, including contained ones' '
+	git replay --contained --onto main ^main topic2 topic3 topic4 >result &&
+
+	test_line_count = 4 result &&
+	cut -f 3 -d " " result >new-branch-tips &&
+
+	>expect &&
+	for i in 2 1 3 4
+	do
+		printf "update refs/heads/topic$i " >>expect &&
+		printf "%s " $(grep topic$i result | cut -f 3 -d " ") >>expect &&
+		git rev-parse topic$i >>expect || return 1
+	done &&
+
+	test_cmp expect result &&
+
+	test_write_lines F C M L B A >expect1 &&
+	test_write_lines E D C M L B A >expect2 &&
+	test_write_lines H G F C M L B A >expect3 &&
+	test_write_lines J I M L B A >expect4 &&
+
+	for i in 1 2 3 4
+	do
+		git log --format=%s $(grep topic$i result | cut -f 3 -d " ") >actual &&
+		test_cmp expect$i actual || return 1
+	done
+'
+
 test_done
-- 
2.40.0.228.gb2eb5bb98e


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* Re: [PATCH 00/14] Introduce new `git replay` command
  2023-04-07  7:24 [PATCH 00/14] Introduce new `git replay` command Christian Couder
                   ` (13 preceding siblings ...)
  2023-04-07  7:24 ` [PATCH 14/14] replay: stop assuming replayed branches do not diverge Christian Couder
@ 2023-04-14 10:12 ` Phillip Wood
  2023-04-15 17:18   ` Elijah Newren
  2023-04-14 17:39 ` Felipe Contreras
                   ` (2 subsequent siblings)
  17 siblings, 1 reply; 208+ messages in thread
From: Phillip Wood @ 2023-04-14 10:12 UTC (permalink / raw)
  To: Christian Couder, git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai

Hi Christian and Elijah

On 07/04/2023 08:24, Christian Couder wrote:
> # Intro
> 
> `git replay` has initially been developed entirely by Elijah Newren
> mostly between June and July 2022 at:
> 
> https://github.com/newren/git/commits/replay
> 
> I took over a few months ago to polish and upstream it as GitLab is
> interested in replacing libgit2, and for that purpose needs a command
> to do server side (so without using a worktree) rebases, cherry-picks
> and reverts.
> 
> I reduced the number of commits and features in this first patch
> series, compared to what Elijah already developed. Especially I
> stopped short of replaying merge commits and replaying
> interactively. These and other features might be upstreamed in the
> future after this patch series has graduated.
> 
> Thanks to Elijah, Patrick Steinhardt and Dscho for early reviews and
> discussions.
> 
> Based on ae73b2c8f1 (The seventh batch, 2023-04-04)

Thanks to both of you for working on this it looks very interesting. 
I've had a quick read over the patches and I ended up slightly confused 
as to exactly what the aim of this series is. My main confusion is 
whether "replay" is intended to be a plumbing command or a porcelain 
command. The use case above suggests plumbing and there are patches that 
take it in that direction by removing any diagnostic output and stopping 
it update any refs. But then it is marked as porcelain in 
command-list.txt and there are patches that do things like 
unconditionally updating the index and worktree when there are conflicts 
that stop it working in bare repositories. I've left some comments below

> # Quick Overview (from Elijah)
> 
> `git replay`, at a basic level, can perhaps be thought of as a
> "default-to-dry-run rebase" -- meaning no updates to the working tree,
> or to the index, or to any references.  However, it differs from
> rebase in that it:
> 
>    * Works for branches that aren't checked out
>    * Works in a bare repository
>    * Can replay multiple branches simultaneously (with or without common
>      history in the range being replayed)
>    * Preserves relative topology by default (merges are replayed too)
>    * Focuses on performance
>    * Has several altered defaults as a result of the above
> 
> I sometimes think of `git replay` as "fast-replay", a patch-based
> analogue to the snapshot-based fast-export & fast-import tools.
> 
> # Reasons for diverging from cherry-pick & rebase (from Elijah)
> 
> There are multiple reasons to diverge from the defaults in cherry-pick and
> rebase.
> 
> * Server side needs
> 
>    * Both cherry-pick and rebase, via the sequencer, are heavily tied
>      to updating the working tree, index, some refs, and a lot of
>      control files with every commit replayed, and invoke a mess of
>      hooks[1] that might be hard to avoid for backward compatibility
>      reasons (at least, that's been brought up a few times on the
>      list).
> 
>    * cherry-pick and rebase both fork various subprocesses
>      unnecessarily, but somewhat intrinsically in part to ensure the
>      same hooks are called that old scripted implementations would
>      have called.

To clarify, since 356ee4659bb (sequencer: try to commit without forking 
'git commit', 2017-11-24) cherry-pick and rebase do not fork 
subprocesses other than hooks for the cases covered by this patch series 
(i.e. they do not fork "git commit" for simple picks).

>    * "Dry run" behavior, where there are no updates to worktree, index,
>      or even refs might be important.
> 
>    * Should not assume users only want to operate on HEAD (see next
>      section)
> 
> * Decapitate HEAD-centric assumptions
> 
>    * cherry-pick forces commits to be played on top of HEAD; inflexible.
> 
>    * rebase assumes the range of commits to be replayed is
>      upstream..HEAD by default, though it allows one to replay
>      upstream..otherbranch -- but it still forcibly and needlessly
>      checks out otherbranch before starting to replay things.

I agree it would be nice to be able to restrict the range of commits 
replayed, especially when replaying merges. The comment about checking 
out other branch is out of date since 767a9c417eb (rebase -i: stop 
checking out the tip of the branch to rebase, 2020-01-24)

>    * Assuming HEAD is involved severely limits replaying multiple
>      (possibly divergent) branches.

I'm not sure how true this is anymore, since 89fc0b53fdb (rebase: update 
refs from 'update-ref' commands, 2022-07-19) the sequencer can update 
multiple branches. The issue with divergent branch is with command line 
arguments and the todo list generation rather than the capabilities of 
the sequencer.

>    * Once you stop assuming HEAD has a certain meaning, there's not
>      much reason to have two separate commands anymore (except for the
>      funny extra not-necessarily-compatible options both have gained
>      over time).

I agree having a unified command at the plumbing level certainly makes 
sense.

>    * (Micro issue: Assuming HEAD is involved also makes it harder for
>      new users to learn what rebase means and does; it makes command
>      lines hard to parse.

That's an interesting point, I wonder if operating on branches that are 
not checked out is potentially confusing for new user though.

>  Not sure I want to harp on this too much, as
>      I have a suspicion I might be creating a tool for experts with
>      complicated use cases, but it's a minor quibble.)
> 
> * Performance
> 
>    * jj is slaughtering us on rebase speed[2].  I would like us to become
>      competitive.  (I dropped a few comments in the link at [2] about why
>      git is currently so bad.)
> 
>    * From [3], there was a simple 4-patch series in linux.git that took
>      53 seconds to rebase.  Switching to ort dropped it to 16 seconds.
>      While that sounds great, only 11 *milliseconds* were needed to do
>      the actual merges.  That means almost *all* the time (>99%) was
>      overhead!  Big offenders:
> 
>      * --reapply-cherry-picks should be the default

I agree that can be a performance hit if there are a lot of upstream 
commits, but it is also a usability feature as it means we don't stop 
and ask the user what to do with the commits that have been upstreamed 
which wastes more of their time. I think maybe we want different 
defaults for the server use case than the user replaying commits or 
perhaps default to dropping commits that become empty.

>      * can_fast_forward() should be ripped out, and perhaps other extraneous
>        revision walks

We should look at doing that at least for the merge backend which has 
skip_unnecessary_picks(). I think it is useful to tell the user that the 
branch was not updated by the rebase though.

>      * avoid updating working tree, index, refs, reflogs, and control
>        structures except when needed (e.g. hitting a conflict, or operation
>        finished)

Not writing to disc unless we need to is sensible. Having said that for 
interactive rebases I do find having HEAD's reflog record all the picks 
useful to unpick what went wrong if mess something up.

>    * Other performance ideas:
> 
>      * single-file control structures instead of directory of files

I like the idea as it should make it easier to keep the on disc state 
consistent, but I'm not sure how much of an issue that is in practice as 
we only read/write the files once each time git is run. The bigger slow 
down is writing the author script, commit message, list of rewritten 
commits, todo list and done files with each pick.


>      * avoid forking subprocesses unless explicitly requested (e.g.
>        --exec, --strategy, --run-hooks).  For example, definitely do not
>        invoke `git commit` or `git merge`.

Good, that matches what the sequencer does for non-merge commits when 
we're not editing the commit message.

>      * Sanitize hooks:
> 
>        * dispense with all per-commit hooks for sure (pre-commit,
>          post-commit, post-checkout).

I agree we should not be running those (we don't run the pre-commit hook 
anyway). However we had a bug report when cherry-pick stopped running 
the "prepare-commit-msg" hook (see 
https://lore.kernel.org/git/CAKdAkRQuj1hfKeckjuR2oP+8C1i+ZR36O-+aRYif4ufaS_zs+w@mail.gmail.com/). 
That shouldn't matter for the server but we should bear it in mind when 
it comes to other use cases.

>        * pre-rebase also seems to assume exactly 1 ref is written, and
>          invoking it repeatedly would be stupid.  Plus, it's specific
>          to "rebase".  So...ignore?  (Stolee's --ref-update option for
>          rebase probably broke the pre-rebase assumptions already...)

If replay is a plumbing command then skipping the pre-rebase hook makes 
sense as scripts can call it themselves if they want to. For a porcelain 
command keeping a hook that can prevent it from rewriting commits  that 
are already upstream (which I think is one of the main uses of the 
pre-rebase hook) would be good.

>        * post-rewrite hook might make sense, but fast-import got
>          exempted, and I think of replay like a patch-based analogue
>          to the snapshot-based fast-import.

If we don't call the hook it would be good to have a option that outputs 
that information so scripts can request it if they want. Also we should 
think about if/when we want to update the notes associated with replayed 
commits.

>      * When not running server side, resolve conflicts in a sparse-cone
>        sparse-index worktree to reduce number of files written to a
>        working tree.  (See below as well)
>
>      * [High risk of possible premature optimization] Avoid large
>        numbers of newly created loose objects, when replaying large
>        numbers of commits.  Two possibilities: (1) Consider using
>        tmp-objdir and pack objects from the tmp-objdir at end of
>        exercise, (2) Lift code from git-fast-import to immediately
>        stuff new objects into a pack?
> 
> * Multiple branches and non-checked out branches
> 
>    * The ability to operate on non-checked out branches also implies
>      that we should generally be able to replay when in a dirty working
>      tree (exception being when we expect to update HEAD and any of the
>      dirty files is one that needs to be updated by the replay).
> 
>    * Also, if we are operating locally on a non-checked out branch and
>      hit a conflict, we should have a way to resolve the conflict without
>      messing with the user's work on their current branch.

That sounds tricky to do in a user friendly way.

>      * Idea: new worktree with sparse cone + sparse index checkout,
>        containing only files in the root directory, and whatever is
>        necessary to get the conflicts

If the user has not asked for a sparse checkout then this could be 
surprising. Sometimes I find it helpful to be able to poke about in 
other source files when resolving a conflict. I also often build and 
test after resolving a conflict which requires more than just the 
conflict to be checked out.

>      * Companion to above idea: control structures should be written to
>        $GIT_COMMON_DIR/replay-${worktree}, so users can have multiple
>        replay sessions, and so we know which worktrees are associated
>        with which replay operations.


We certainly want some way of making sure we only update a given ref in 
one replay session, and have checks to for whether the ref is checked 
out anywhere as we do now for rebase --update-refs. That seems to be 
lacking in the patches adding ref updating in this series.

>    - [1] https://lore.kernel.org/git/pull.749.v3.git.git.1586044818132.gitgitgadget@gmail.com/
>    - [2] https://github.com/martinvonz/jj/discussions/49
>    - [3] https://lore.kernel.org/git/CABPp-BE48=97k_3tnNqXPjSEfA163F8hoE+HY0Zvz1SWB2B8EA@mail.gmail.com/
> 
> # Important limitations
> 
> * The code die()s if there are any conflict. No resumability. No nice
>    output. No interactivity.

I can see that on a server you might not want any output, but if I run 
it locally it would be nice to have a message saying which paths have 
conflicts. Maybe we could add a --quiet flag for the server rather than 
removing the existing messages?

> * No replaying merges, nor root commits. Only regular commits.

That is a reasonable place to start.

> * Signed commits are not properly handled. It's not clear what to do
>    to such commits when replaying on the server side.

Yes on the server where you don't have access to the signing key there 
is not much need for replay to have a signing option.

> # Commit overview
> 
> * 1/14 replay: introduce new builtin
> 
>       This creates a minimal `git replay` command by moving the code
>       from the `fast-rebase` test helper from `t/helper/` into
>       `builtin/` and doing some renames and a few other needed changes.
> 
> * - 2/14 replay: start using parse_options API
>    - 3/14 replay: die() instead of failing assert()
>    - 4/14 replay: introduce pick_regular_commit()
>    - 5/14 replay: don't simplify history
>    - 6/14 replay: add an important FIXME comment about gpg signing
>    - 7/14 replay: remove progress and info output
>    - 8/14 replay: remove HEAD related sanity check
> 
>       These slowly change the command to make it behave more like a
>       regular commands and to start cleaning up its output.
> 
> * 9/14 replay: very coarse worktree updating
> 
>       Make it handle conflicts in a very coarse way. This might not
>       work on bare repos, but it allows existing tests to pass and it's
>       nice to help cli users a bit when they get conflicts.
> 
> * 10/14 replay: make it a minimal server side command
> 
>       After the cleaning up in previous ommits, it's now time to
>       radically change the way it works by stopping it to do ref
>       updates, to update the index and worktree, to consider HEAD as
>       special. Instead just make it output commands that should be
>       passed to `git update-ref --stdin`.
> 
> * - 11/14 replay: use standard revision ranges
>    - 12/14 replay: introduce guess_new_base()
>    - 13/14 replay: add different modes
>    - 14/14 replay: stop assuming replayed branches do not diverge
> 
>        These finish the clean up and add new interesting features at
>        the same time, as well as related documentation and tests.
> 
> # Note about tests and documentation
> 
> Note that the `fast-rebase` test helper was used before this series in
> 
> t6429-merge-sequence-rename-caching.sh
> 
> So when `git replay` is created from `fast-rebase` in patch 1/14, this
> test script is also converted to use `git replay`. This ensures that
> `git replay` doesn't break too badly during the first 10 patches in
> this patch series.
> 
> Tests and documentation are introduced specifically for `git replay`
> only in 11/14 and later patches as it doesn't make much sense to
> document and test behavior that we know is going to change soon. So
> it's only when the command is crystalizing towards its final form that
> we start documenting and testing it.
> 
> # Possibly controversial issues
> 
> * bare or not bare: this series works towards a command with the end
>    goal of it being usable and used on bare repos, contrary to existing
>    commands like `git rebase` and `git cherry-pick`, but the tests
>    currently don't check that, and in case of conflicts it won't
>    currently work on bare repos. One reason for that is that existing
>    tests in t6429 should continue to work, and one of these tests
>    requires some output in case of conflict. And it's nice for users to
>    get some help in case of conflict. It's also nice for users if
>    commands that should work on both bare and non bare repos work well
>    on non bare repos first as they are less likely to use them on bare
>    repos. So let's have a command that works well on non-bare repos
>    first, even if its end goal is to work fine on bare repos too. We
>    plan to improve things for bare repos soon after this first patch
>    series graduates.
> 
> * exit status: a successful, non-conflicted replay exits with code
>    0. When the replay has conflicts, the exit status is 1. If the
>    replay is not able to complete (or start) due to some kind of error,
>    the exit status is something other than 0 or 1. It has been
>    suggested in an internal review that conflicts might want to get a
>    more specific error code as an error code of 1 might be quite easy
>    to return by accident. It doesn't seem to me from their docs (which
>    might want to be improved, I didn't look at the code) that other
>    commands like `git merge` and `git rebase` exit with a special error
>    code in case of conflict.

I don't think we've ever had a special "conflict" error code but it 
would be useful for scripts if replay had one. Does replay return a 
different exit code for "merge conflicts" and "cannot merge because it 
would overwrite an untracked file"? Does it have an exit code for "the 
commit becomes empty" or are those patches unconditionally dropped?

> * to guess or not to guess: commit 12/14 introduces the
>    guess_new_base() function which tries to find a base to rebase onto
>    when the --onto option is not provided, making this option actually
>    optional instead of mandatory. Given that it's an heuristic and the
>    command end goal is to be used on server side, we might want to
>    introduce this as an iterative improvement later. I still think it's
>    interesting to have it in for now though, as it shows that --onto
>    and --advance (which is introduced in the following commit) should
>    indeed be options. If --onto was always mandatory in the series,
>    people could argue that it shouldn't be an option and its argument
>    should always be the first (unconditional) argument of the command.

I think it comes down to "what's the aim of this series?" is it focused 
on the bare repository server use case or is it trying to add a general 
purpose cli tool.

> * make worktree and index changes optional: commit 10/14 stops
>    updating the index and worktree, but it might be better especially
>    for cli users to make that optional. The issue is that this would
>    make the command more complex while we are developing a number of
>    important features. It seems to me that this should rather be done
>    in an iterative improvement after the important features have
>    landed.

I'm confused by this as patch 9 seems to start updating the index and 
worktree when there are conflicts but patch 10 stops updating the index 
and worktree if the replay is successful.

> * when and where to add tests and docs: although t6429 has tests that
>    are changed to use the new command instead of the fast-rebase
>    test-tool command as soon as the former is introduced, there is no
>    specific test script and no doc for the new command until commit
>    11/14 when standard revision ranges are used. This is done to avoid
>    churn in tests and docs while the final form of the command hasn't
>    crystalized enough. Adding tests and doc at this point makes this
>    commit quite big and possibly more difficult to review than if they
>    were in separate commits though. On the other hand when tests and
>    docs are added in specific commits some reviewers say it would be
>    better to introduce them when the related changes are made.
> 
> * --advance and --contained: these two advanced options might not
>    belong to this first series and could perhaps be added in a followup
>    series in separate commits. On the other hand the code for
>    --contained seems involved with the code of --advance and it's nice
>    to see soon that git replay can indeed do cherry-picking and rebase
>    many refs at once, and this way fullfil these parts of its promise.

Once I understood what these options did the names made sense, but I 
could not tell from the names what they were going to do. For me 
"--cherry-pick" and "--update-refs" would have been clearer. It might be 
worth splitting this patch so the individual options are added separately.

> * replaying diverging branches: 14/14 the last patch in the series,
>    which allow replaying diverging branches, can be seen as a
>    fundamental fix or alternatively as adding an interesting
>    feature. So it's debatable if it should be in its own patch along
>    with its own tests as in this series, or if it should be merged into
>    a previous patch and which one.

It might make sense to add this in the same patch as you add --contained.

> * only 2 patches: this patch series can be seen from a high level
>    point of view as 1) introducing the new `git replay` command, and 2)
>    using `git replay` to replace, and get rid of, the fast-rebase
>    test-tool command. The fact that not much of the original
>    fast-rebase code and interface is left would agree with that point
>    of view. On the other hand, fast-rebase can also be seen as a first
>    iteration towards `git replay`. So it can also make sense to see how
>    `git replay` evolved from it.

Starting with fast-rebase means one has to checkout the first patch to 
see what code we're adding to replay.c and to make sense of the later 
patches that remove code. It would be interesting to compare this series 
to one that started from scratch but I guess that would be quite a bit 
of work.

Thanks for working on this, I'm interested to see where it goes

Best Wishes

Phillip

> 
> Elijah Newren (14):
>    replay: introduce new builtin
>    replay: start using parse_options API
>    replay: die() instead of failing assert()
>    replay: introduce pick_regular_commit()
>    replay: don't simplify history
>    replay: add an important FIXME comment about gpg signing
>    replay: remove progress and info output
>    replay: remove HEAD related sanity check
>    replay: very coarse worktree updating
>    replay: make it a minimal server side command
>    replay: use standard revision ranges
>    replay: introduce guess_new_base()
>    replay: add different modes
>    replay: stop assuming replayed branches do not diverge
> 
>   .gitignore                               |   1 +
>   Documentation/git-replay.txt             | 130 +++++++
>   Makefile                                 |   2 +-
>   builtin.h                                |   1 +
>   builtin/replay.c                         | 419 +++++++++++++++++++++++
>   command-list.txt                         |   1 +
>   git.c                                    |   1 +
>   t/helper/test-fast-rebase.c              | 233 -------------
>   t/helper/test-tool.c                     |   1 -
>   t/helper/test-tool.h                     |   1 -
>   t/t3650-replay-basics.sh                 | 160 +++++++++
>   t/t6429-merge-sequence-rename-caching.sh |  43 ++-
>   12 files changed, 739 insertions(+), 254 deletions(-)
>   create mode 100644 Documentation/git-replay.txt
>   create mode 100644 builtin/replay.c
>   delete mode 100644 t/helper/test-fast-rebase.c
>   create mode 100755 t/t3650-replay-basics.sh
> 


^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH 11/14] replay: use standard revision ranges
  2023-04-07  7:24 ` [PATCH 11/14] replay: use standard revision ranges Christian Couder
@ 2023-04-14 14:09   ` Derrick Stolee
  2023-04-14 14:23     ` Derrick Stolee
  2023-04-15 18:30     ` Elijah Newren
  0 siblings, 2 replies; 208+ messages in thread
From: Derrick Stolee @ 2023-04-14 14:09 UTC (permalink / raw)
  To: Christian Couder, git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Christian Couder

On 4/7/2023 3:24 AM, Christian Couder wrote:
> From: Elijah Newren <newren@gmail.com>
> 
> Instead of the fixed "<oldbase> <branch>" arguments, the replay
> command now accepts "<revision-range>..." arguments in a similar
> way as many other Git commands. This makes its interface more
> standard and more flexible.

Unfortunately, while doing this, you have broken the --onto
logic:

 $ git replay --onto HEAD~2 HEAD~1 HEAD
 fatal: replaying down to root commit is not supported yet!

The rev-walk you are supplying by this line...

> +	argc = setup_revisions(argc, argv, &revs, NULL);

is taking the remaining arguments and using them as tips to
walk. We need to be able to recognize that --onto A B C means
that A is the new base and our walk is B..C.

I'm not sure if there might be a way to use a callback for
the --onto option and pull out the next three options into
'new-base', 'old-base', 'tip' values or something.

Overall, I don't think being flexible in the CLI is of high
value for this command. Let's be as prescriptive as possible.

Something like:

	'git replay [options] <base> <tip>'
	This mode means to rebase <tip> onto <base>,
	detecting the range of commits to rewrite.

	'git replay [options] <new-base> <old-base> <tip>'
	This mode means to rebase the range <old-base>..<tip>
	onto <new-base>.

We don't even need "--onto" for these positional arguments.

Thanks,
-Stolee

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH 11/14] replay: use standard revision ranges
  2023-04-14 14:09   ` Derrick Stolee
@ 2023-04-14 14:23     ` Derrick Stolee
  2023-04-15 19:07       ` Elijah Newren
  2023-04-18  4:58       ` Elijah Newren
  2023-04-15 18:30     ` Elijah Newren
  1 sibling, 2 replies; 208+ messages in thread
From: Derrick Stolee @ 2023-04-14 14:23 UTC (permalink / raw)
  To: Christian Couder, git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Christian Couder

On 4/14/2023 10:09 AM, Derrick Stolee wrote:
> On 4/7/2023 3:24 AM, Christian Couder wrote:
>> From: Elijah Newren <newren@gmail.com>
>>
>> Instead of the fixed "<oldbase> <branch>" arguments, the replay
>> command now accepts "<revision-range>..." arguments in a similar
>> way as many other Git commands. This makes its interface more
>> standard and more flexible.
> 
> Unfortunately, while doing this, you have broken the --onto
> logic:
> 
>  $ git replay --onto HEAD~2 HEAD~1 HEAD
>  fatal: replaying down to root commit is not supported yet!
> 
> The rev-walk you are supplying by this line...
> 
>> +	argc = setup_revisions(argc, argv, &revs, NULL);
> 
> is taking the remaining arguments and using them as tips to
> walk. We need to be able to recognize that --onto A B C means
> that A is the new base and our walk is B..C.

I'm realizing after hitting "send" that this change is
intentional (based on your test updates). I don't agree with
it, though.

Sending arbitrary command-line arguments to setup_revisions()
creates an opportunity for behavior you are not expecting.

For instance, can users pass multiple ranges? Can users
supply --first-parent? What happens if they add an --author
filter?

(I was able to get a segfault by rebasing this series with
--author=stolee because the commit list became empty. Something
to watch for.)
 
> Something like:
> 
> 	'git replay [options] <base> <tip>'
> 	This mode means to rebase <tip> onto <base>,
> 	detecting the range of commits to rewrite.
> 
> 	'git replay [options] <new-base> <old-base> <tip>'
> 	This mode means to rebase the range <old-base>..<tip>
> 	onto <new-base>.

For that reason, I think we should be using explicit argument
parsing in the builtin and only transform arguments we
understand into the setup_revisions() (by building a strvec).

Thanks,
-Stolee

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH 00/14] Introduce new `git replay` command
  2023-04-07  7:24 [PATCH 00/14] Introduce new `git replay` command Christian Couder
                   ` (14 preceding siblings ...)
  2023-04-14 10:12 ` [PATCH 00/14] Introduce new `git replay` command Phillip Wood
@ 2023-04-14 17:39 ` Felipe Contreras
  2023-04-15  6:44 ` Elijah Newren
  2023-05-09 17:53 ` [PATCH v2 00/15] " Christian Couder
  17 siblings, 0 replies; 208+ messages in thread
From: Felipe Contreras @ 2023-04-14 17:39 UTC (permalink / raw)
  To: Christian Couder, git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Christian Couder

Christian Couder wrote:
> # Quick Overview (from Elijah)
> 
> `git replay`, at a basic level, can perhaps be thought of as a
> "default-to-dry-run rebase" -- meaning no updates to the working tree,
> or to the index, or to any references.

Interesting, I just ran into this problem trying to cleanup my personal git
branches.

Simply checking which branches can be cleanly rebased on top of master takes a
significant amount of time without any tricks, and using `git merge-tree` still
takes some time.

But the biggest offender is checking which patches have not yet been merged
into master, which takes 52 seconds on my machine which is by no means old.

> # Reasons for diverging from cherry-pick & rebase (from Elijah)
> 
> * Server side needs

I personally don't care about the server side, but...

>   * Both cherry-pick and rebase, via the sequencer, are heavily tied
>     to updating the working tree, index, some refs, and a lot of
>     control files with every commit replayed, and invoke a mess of
>     hooks[1] that might be hard to avoid for backward compatibility
>     reasons (at least, that's been brought up a few times on the
>     list).

This is important as an end user as well.

Since day 1 one of the important selling points of git was that operations that
could be done in milliseconds did take milliseconds.

If it can be done faster, why wouldn't I want it to be done faster?

> * Decapitate HEAD-centric assumptions

That's good, but not particularly important at the moment IMO.

> * Performance
> 
>   * jj is slaughtering us on rebase speed[2].  I would like us to become
>     competitive.  (I dropped a few comments in the link at [2] about why
>     git is currently so bad.)

Indeed.

>   * From [3], there was a simple 4-patch series in linux.git that took
>     53 seconds to rebase.

I did participate in that discussion, but Uwe Kleine-König never responded back.

In [1] he clearly noticed the problem was *before* attempting to apply any
patch. Other people mentioned the fork-point detection, but I don't think that
was the issue, my guess was that checking for the possibility of a fast-forward
was the issue.

The code was clearly doing the wrong thing for that case, but I believe it
should have been fixed by d42c9ffa0f (rebase: factor out branch_base
calculation, 2022-10-17).

It would be interesting to see if this issue can be reproduced somehow.

>     Switching to ort dropped it to 16 seconds.

No, it dropped to 16 seconds it for Elijah, not Uwe. Uwe (who had the real
repository) noticed a big reduction of around 70%, but the discrepancy of using
--onto versus not always remained.

>     While that sounds great, only 11 *milliseconds* were needed to do
>     the actual merges.  That means almost *all* the time (>99%) was
>     overhead!  Big offenders:
> 
>     * --reapply-cherry-picks should be the default
> 
>     * can_fast_forward() should be ripped out, and perhaps other extraneous
>       revision walks

Doesn't d42c9ffa0f (rebase: factor out branch_base calculation, 2022-10-17)
deal with that?

---

I think something like this is defeinitely needed, when I rewrote `git rebase`
to use `git cherry-pick` I noticed many areas of improvement, and I'm of the
opinion that `git rebase` should be rewritten from scratch.

But precisely because git focuses too much on backwards compatibility (and
often in the wrong areas), I think `git replay` should be thoroughly discussed
before accepting something we could quickly realize can be substantially
improved.

Cheers.

[1] https://lore.kernel.org/git/20210528214024.vw4huojcklrm6d27@pengutronix.de/

-- 
Felipe Contreras

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH 00/14] Introduce new `git replay` command
  2023-04-07  7:24 [PATCH 00/14] Introduce new `git replay` command Christian Couder
                   ` (15 preceding siblings ...)
  2023-04-14 17:39 ` Felipe Contreras
@ 2023-04-15  6:44 ` Elijah Newren
  2023-05-09 17:53 ` [PATCH v2 00/15] " Christian Couder
  17 siblings, 0 replies; 208+ messages in thread
From: Elijah Newren @ 2023-04-15  6:44 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	John Cai

On Fri, Apr 7, 2023 at 12:24 AM Christian Couder
<christian.couder@gmail.com> wrote:
>
> # Intro
>
> `git replay` has initially been developed entirely by Elijah Newren
> mostly between June and July 2022 at:

(Sidenote: actually, there was a good chunk in Jan & Feb 2022 as well,
and various design and idea work preceding that over a long time, some
in the form of the fast-rebase test-tool...)

> https://github.com/newren/git/commits/replay
>
> I took over a few months ago to polish and upstream it as GitLab is
> interested in replacing libgit2, and for that purpose needs a command
> to do server side (so without using a worktree) rebases, cherry-picks
> and reverts.
>
> I reduced the number of commits and features in this first patch
> series, compared to what Elijah already developed. Especially I
> stopped short of replaying merge commits and replaying
> interactively. These and other features might be upstreamed in the
> future after this patch series has graduated.

...and also cleaned up my commits which were in a WIP state.  Thanks!  :-)

> Thanks to Elijah, Patrick Steinhardt and Dscho for early reviews and
> discussions.
>
> Based on ae73b2c8f1 (The seventh batch, 2023-04-04)
>
> # Quick Overview (from Elijah)

In particular, this comes from the replay-design-notes.txt file on the
replay branch, up until the footnote links.

<snip>

My replay branch involved a whole bunch of aspirational ideas, work in
progress, and some things that worked.  As noted above, you've taken
this portion of the cover letter from my replay-design-notes.txt file
on that branch, but that file involved my in-progress thought
processes on all these ideas.  For this series, we should probably
just focus on the server-side usecases since other aspects just
weren't complete enough for use.  (I also thought the server-side
aspects of git-replay weren't good enough for use either because I
thought we'd need some conflict-related handling similar to what
git-merge-tree does, but both you and Dscho have said you aren't
worried about that, and are fine with just a simple non-zero exit
status.)

Anyway, deleting the forward-looking stuff and concentrating on the
server-side replaying of commits would mean that we can at least
delete the portion of this cover letter starting from here...

>   * Other performance ideas:
>
>     * single-file control structures instead of directory of files
>
>     * avoid forking subprocesses unless explicitly requested (e.g.
>       --exec, --strategy, --run-hooks).  For example, definitely do not
>       invoke `git commit` or `git merge`.
>
>     * Sanitize hooks:
>
>       * dispense with all per-commit hooks for sure (pre-commit,
>         post-commit, post-checkout).
>
>       * pre-rebase also seems to assume exactly 1 ref is written, and
>         invoking it repeatedly would be stupid.  Plus, it's specific
>         to "rebase".  So...ignore?  (Stolee's --ref-update option for
>         rebase probably broke the pre-rebase assumptions already...)
>
>       * post-rewrite hook might make sense, but fast-import got
>         exempted, and I think of replay like a patch-based analogue
>         to the snapshot-based fast-import.
>
>     * When not running server side, resolve conflicts in a sparse-cone
>       sparse-index worktree to reduce number of files written to a
>       working tree.  (See below as well)
>
>     * [High risk of possible premature optimization] Avoid large
>       numbers of newly created loose objects, when replaying large
>       numbers of commits.  Two possibilities: (1) Consider using
>       tmp-objdir and pack objects from the tmp-objdir at end of
>       exercise, (2) Lift code from git-fast-import to immediately
>       stuff new objects into a pack?
>
> * Multiple branches and non-checked out branches
>
>   * The ability to operate on non-checked out branches also implies
>     that we should generally be able to replay when in a dirty working
>     tree (exception being when we expect to update HEAD and any of the
>     dirty files is one that needs to be updated by the replay).
>
>   * Also, if we are operating locally on a non-checked out branch and
>     hit a conflict, we should have a way to resolve the conflict without
>     messing with the user's work on their current branch.
>
>     * Idea: new worktree with sparse cone + sparse index checkout,
>       containing only files in the root directory, and whatever is
>       necessary to get the conflicts
>
>     * Companion to above idea: control structures should be written to
>       $GIT_COMMON_DIR/replay-${worktree}, so users can have multiple
>       replay sessions, and so we know which worktrees are associated
>       with which replay operations.

...up to here.  Some of the other stuff could perhaps be trimmed as
well, though I suspect at least some of it is useful from the
perspective of letting others know of additional usecases we'd like to
support (so that design suggestions don't curtail those additional
future usecases).


>   - [1] https://lore.kernel.org/git/pull.749.v3.git.git.1586044818132.gitgitgadget@gmail.com/
>   - [2] https://github.com/martinvonz/jj/discussions/49
>   - [3] https://lore.kernel.org/git/CABPp-BE48=97k_3tnNqXPjSEfA163F8hoE+HY0Zvz1SWB2B8EA@mail.gmail.com/

This appears to be the end of the part you copied from replay-design-notes.txt

<snip>

> * 9/14 replay: very coarse worktree updating
>
>      Make it handle conflicts in a very coarse way. This might not
>      work on bare repos, but it allows existing tests to pass and it's
>      nice to help cli users a bit when they get conflicts.

I had the coarse working updating in git-replay mostly because it came
from fast-rebase.  I did also use it to poke and prod early ideas,
even though I knew that its current implementation might well be
incompatible with some of the other ideas I had.

I don't think this helps the server-side use though, and since it
potentially conflicts with some of the other goals, I'd be inclined to
say we should just drop this patch (and/or squash the next patch into
this one.)

> * 10/14 replay: make it a minimal server side command
>
>      After the cleaning up in previous ommits, it's now time to
>      radically change the way it works by stopping it to do ref
>      updates, to update the index and worktree, to consider HEAD as
>      special. Instead just make it output commands that should be
>      passed to `git update-ref --stdin`.

A squashed 9 & 10 would thus be similar to patch 8 in the sense that
its purpose was to get rid of something that made sense for
fast-rebase but which doesn't align with the current goals of this
command.

<snip>

> # Possibly controversial issues
>
> * bare or not bare: this series works towards a command with the end
>   goal of it being usable and used on bare repos, contrary to existing
>   commands like `git rebase` and `git cherry-pick`, but the tests
>   currently don't check that, and in case of conflicts it won't
>   currently work on bare repos. One reason for that is that existing
>   tests in t6429 should continue to work, and one of these tests
>   requires some output in case of conflict. And it's nice for users to
>   get some help in case of conflict. It's also nice for users if
>   commands that should work on both bare and non bare repos work well
>   on non bare repos first as they are less likely to use them on bare
>   repos. So let's have a command that works well on non-bare repos
>   first, even if its end goal is to work fine on bare repos too. We
>   plan to improve things for bare repos soon after this first patch
>   series graduates.

I think there's a lot of work to do for conflict handling, and if we
want to submit this for inclusion then we should just exclude conflict
handling entirely and adjust the tests accordingly.

> * exit status: a successful, non-conflicted replay exits with code
>   0. When the replay has conflicts, the exit status is 1. If the
>   replay is not able to complete (or start) due to some kind of error,
>   the exit status is something other than 0 or 1. It has been
>   suggested in an internal review that conflicts might want to get a
>   more specific error code as an error code of 1 might be quite easy
>   to return by accident. It doesn't seem to me from their docs (which
>   might want to be improved, I didn't look at the code) that other
>   commands like `git merge` and `git rebase` exit with a special error
>   code in case of conflict.

The merge backend does not provide any granularity finer than "had
conflicts" for its return code.  This is somewhat cemented by the
git-merge API as well, where it mandates that the return code of merge
backends be 1 for conflicts:

    /*
     * The backend exits with 1 when conflicts are
     * left to be resolved, with 2 when it does not
     * handle the given merge at all.
     */

Because of this, all of git-merge-resolve, git-merge-octopus,
git-merge-recursive, and git-merge-ort return 1 for conflicts.

Of course, we do not have to use the return code of the merge backend
as the exit status for `git replay`.  We could inspect the conflicts,
somewhat like git-merge-tree does, except that instead of printing
information about those conflicts, we could also let it guide us to
choose a new exit status.  I'm not sure we'd want to do that, but we
could.

Since an exit status of 1 is pretty thoroughly baked in elsewhere for
everything merge related, including `git-merge-tree`, I'd be inclined
to leave this series as-is and have an exit status of 1 for conflicts.

> * to guess or not to guess: commit 12/14 introduces the
>   guess_new_base() function which tries to find a base to rebase onto
>   when the --onto option is not provided, making this option actually
>   optional instead of mandatory. Given that it's an heuristic and the
>   command end goal is to be used on server side, we might want to
>   introduce this as an iterative improvement later. I still think it's
>   interesting to have it in for now though, as it shows that --onto
>   and --advance (which is introduced in the following commit) should
>   indeed be options. If --onto was always mandatory in the series,
>   people could argue that it shouldn't be an option and its argument
>   should always be the first (unconditional) argument of the command.

I do not want a positional argument for <onto>; rebase's use of
positional arguments is an example to avoid, IMO.

I also don't want people thinking this is just rebase.  Rebase is
perhaps used more than cherry-pick and as such a lot of the
documentation and design goals talk about it more, but I designed this
command very deliberately to be about handling both cherry-pick and
rebase functionality in the same command.

All that said, I remember spending a fair amount of time on the
heuristic, but don't remember if I felt I had gotten it good enough or
not.  If folks want to leave it out for a future series, that'd be
fine, so long as it doesn't lead people to suggestions that'd conflict
with the above points.

> * make worktree and index changes optional: commit 10/14 stops
>   updating the index and worktree, but it might be better especially
>   for cli users to make that optional. The issue is that this would
>   make the command more complex while we are developing a number of
>   important features. It seems to me that this should rather be done
>   in an iterative improvement after the important features have
>   landed.

I don't think worktree and index changes should be included in this
series, even as an option.  The related code as currently written is
incompatible with some other ideas expressed in the cover letter.
Granted, those ideas might change, even dramatically, but I don't want
us limiting the design space and I don't think the worktree or index
updates in this series are ready for users to use yet.  (And there
aren't any patches available, to my knowledge, that make them ready.
Those still need to be written.)

> * when and where to add tests and docs: although t6429 has tests that
>   are changed to use the new command instead of the fast-rebase
>   test-tool command as soon as the former is introduced, there is no
>   specific test script and no doc for the new command until commit
>   11/14 when standard revision ranges are used. This is done to avoid
>   churn in tests and docs while the final form of the command hasn't
>   crystalized enough. Adding tests and doc at this point makes this
>   commit quite big and possibly more difficult to review than if they
>   were in separate commits though. On the other hand when tests and
>   docs are added in specific commits some reviewers say it would be
>   better to introduce them when the related changes are made.

I don't have opinions on this one.  Maybe I will after reading through
this cleaned up series more carefully.

> * --advance and --contained: these two advanced options might not
>   belong to this first series and could perhaps be added in a followup
>   series in separate commits. On the other hand the code for
>   --contained seems involved with the code of --advance and it's nice
>   to see soon that git replay can indeed do cherry-picking and rebase
>   many refs at once, and this way fullfil these parts of its promise.

--advance is the one option I thought I had a chance of getting my
company to use server-side in the near term...

Also, without --advance, I worry reviewers will make rebase-centric
assumptions that preclude handling cherry-picks, so I'm really leery
of deferring it based on that.

I understand that --contained isn't as likely to be useful
server-side, but it was the one piece that I really enjoyed using
cli-side (especially since it pre-dated rebase --update-refs).  But
yeah, it's utility is somewhat limited until there's some kind of
conflict resolution mechanism provided.

> * replaying diverging branches: 14/14 the last patch in the series,
>   which allow replaying diverging branches, can be seen as a
>   fundamental fix or alternatively as adding an interesting
>   feature. So it's debatable if it should be in its own patch along
>   with its own tests as in this series, or if it should be merged into
>   a previous patch and which one.

I don't have opinions on this either.  Maybe I will after reading
through the series?

> * only 2 patches: this patch series can be seen from a high level
>   point of view as 1) introducing the new `git replay` command, and 2)
>   using `git replay` to replace, and get rid of, the fast-rebase
>   test-tool command. The fact that not much of the original
>   fast-rebase code and interface is left would agree with that point
>   of view. On the other hand, fast-rebase can also be seen as a first
>   iteration towards `git replay`. So it can also make sense to see how
>   `git replay` evolved from it.

I preferred the angle of starting from fast-rebase, but I don't feel
too strongly about it.  Mostly I just thought it'd make it easier for
me, and for the few other people who already reviewed fast-rebase (and
yes, I got review comments on it from a few different people back when
it was proposed).  If dividing up the patches some other way makes it
easier for others to reason about, and you want to switch it around,
go for it.

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH 00/14] Introduce new `git replay` command
  2023-04-14 10:12 ` [PATCH 00/14] Introduce new `git replay` command Phillip Wood
@ 2023-04-15 17:18   ` Elijah Newren
  0 siblings, 0 replies; 208+ messages in thread
From: Elijah Newren @ 2023-04-15 17:18 UTC (permalink / raw)
  To: phillip.wood
  Cc: Christian Couder, git, Junio C Hamano, Patrick Steinhardt,
	Johannes Schindelin, John Cai

Hi Phillip,

On Fri, Apr 14, 2023 at 3:13 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
>
> Hi Christian and Elijah
>
> On 07/04/2023 08:24, Christian Couder wrote:
> > # Intro
> >
> > `git replay` has initially been developed entirely by Elijah Newren
> > mostly between June and July 2022 at:
> >
> > https://github.com/newren/git/commits/replay
> >
> > I took over a few months ago to polish and upstream it as GitLab is
> > interested in replacing libgit2, and for that purpose needs a command
> > to do server side (so without using a worktree) rebases, cherry-picks
> > and reverts.
> >
> > I reduced the number of commits and features in this first patch
> > series, compared to what Elijah already developed. Especially I
> > stopped short of replaying merge commits and replaying
> > interactively. These and other features might be upstreamed in the
> > future after this patch series has graduated.
> >
> > Thanks to Elijah, Patrick Steinhardt and Dscho for early reviews and
> > discussions.
> >
> > Based on ae73b2c8f1 (The seventh batch, 2023-04-04)
>
> Thanks to both of you for working on this it looks very interesting.
> I've had a quick read over the patches and I ended up slightly confused
> as to exactly what the aim of this series is. My main confusion is
> whether "replay" is intended to be a plumbing command or a porcelain
> command. The use case above suggests plumbing and there are patches that
> take it in that direction by removing any diagnostic output and stopping
> it update any refs. But then it is marked as porcelain in
> command-list.txt and there are patches that do things like
> unconditionally updating the index and worktree when there are conflicts
> that stop it working in bare repositories. I've left some comments below

Yeah, that's fair.  It was totally unclear in my work-in-progress
patches, as I had ideas taking it in both directions.  Then Christian
and Dscho came along and said they wanted to use it for server-side
rebasing & cherry-picking.  Christian did some good cleanup of my
patches, but I think your comment here is just reflecting that there's
still a bit more to do.

I'd say for now that we should expunge the worktree & index updating,
any conflict handling beyond exit status, mark it as plumbing, and
focus strictly on server-side use for now.

> > # Quick Overview (from Elijah)
> >
> > `git replay`, at a basic level, can perhaps be thought of as a
> > "default-to-dry-run rebase" -- meaning no updates to the working tree,
> > or to the index, or to any references.  However, it differs from
> > rebase in that it:
> >
> >    * Works for branches that aren't checked out
> >    * Works in a bare repository
> >    * Can replay multiple branches simultaneously (with or without common
> >      history in the range being replayed)
> >    * Preserves relative topology by default (merges are replayed too)
> >    * Focuses on performance
> >    * Has several altered defaults as a result of the above
> >
> > I sometimes think of `git replay` as "fast-replay", a patch-based
> > analogue to the snapshot-based fast-export & fast-import tools.
> >
> > # Reasons for diverging from cherry-pick & rebase (from Elijah)
> >
> > There are multiple reasons to diverge from the defaults in cherry-pick and
> > rebase.
> >
> > * Server side needs
> >
> >    * Both cherry-pick and rebase, via the sequencer, are heavily tied
> >      to updating the working tree, index, some refs, and a lot of
> >      control files with every commit replayed, and invoke a mess of
> >      hooks[1] that might be hard to avoid for backward compatibility
> >      reasons (at least, that's been brought up a few times on the
> >      list).
> >
> >    * cherry-pick and rebase both fork various subprocesses
> >      unnecessarily, but somewhat intrinsically in part to ensure the
> >      same hooks are called that old scripted implementations would
> >      have called.
>
> To clarify, since 356ee4659bb (sequencer: try to commit without forking
> 'git commit', 2017-11-24) cherry-pick and rebase do not fork
> subprocesses other than hooks for the cases covered by this patch series
> (i.e. they do not fork "git commit" for simple picks).

Thanks for the clarification.

Let me also add a clarification about this text you're responding to:
the document that Christian copied into this portion of the cover
letter wasn't talking about this particular series he submitted, but
about the broader design goals I was working on.  I don't want a
run_git_commit() function within replay.c, even for commit message
editing, for picking merge commits, or for handling staged changes
after resuming from an interactive rebase.  I'd even say I don't want
it for failure handling, either, though that one is less important.

> >    * "Dry run" behavior, where there are no updates to worktree, index,
> >      or even refs might be important.
> >
> >    * Should not assume users only want to operate on HEAD (see next
> >      section)
> >
> > * Decapitate HEAD-centric assumptions
> >
> >    * cherry-pick forces commits to be played on top of HEAD; inflexible.
> >
> >    * rebase assumes the range of commits to be replayed is
> >      upstream..HEAD by default, though it allows one to replay
> >      upstream..otherbranch -- but it still forcibly and needlessly
> >      checks out otherbranch before starting to replay things.
>
> I agree it would be nice to be able to restrict the range of commits
> replayed, especially when replaying merges. The comment about checking
> out other branch is out of date since 767a9c417eb (rebase -i: stop
> checking out the tip of the branch to rebase, 2020-01-24)

Guess how many years I've been gathering my ideas for git-replay.  ;-)

One thing to note here, though, is the checkout handling still isn't
fully fixed, and it may be impossible to fix at this point.  In
particular, `git rebase --abort` doesn't return people to the original
branch when otherbranch is specified, which is problematic in
conjunction with other options (see e.g.
https://lore.kernel.org/git/xmqqft5icsd9.fsf@gitster.c.googlers.com/).

> >    * Assuming HEAD is involved severely limits replaying multiple
> >      (possibly divergent) branches.
>
> I'm not sure how true this is anymore, since 89fc0b53fdb (rebase: update
> refs from 'update-ref' commands, 2022-07-19) the sequencer can update
> multiple branches. The issue with divergent branch is with command line
> arguments and the todo list generation rather than the capabilities of
> the sequencer.

Yeah, this particular paragraph I suspect is from about 18 months ago.
(See also where I responded to the update-refs series about this
https://lore.kernel.org/git/CABPp-BEOV53oBoBp4YjiRfksZMmAADanZUUemhxwn7Wor=m-nA@mail.gmail.com/).

However, I'd still say the statement is true, it just needs to be
focused on the divergent branches.  The use of HEAD (or more generally
exactly 3 commitish-es), as done in rebase, is going to make it really
awkward to introduce any kind of command line UI for handling rebasing
of multiple divergent branches.  I'm not sure it even makes sense.
git-replay has done it for over a year, and I think it makes sense
specifically because it got rid of the problematic assumptions
hard-wired into the command line UI.

> >    * Once you stop assuming HEAD has a certain meaning, there's not
> >      much reason to have two separate commands anymore (except for the
> >      funny extra not-necessarily-compatible options both have gained
> >      over time).
>
> I agree having a unified command at the plumbing level certainly makes
> sense.

:-)

> >    * (Micro issue: Assuming HEAD is involved also makes it harder for
> >      new users to learn what rebase means and does; it makes command
> >      lines hard to parse.
>
> That's an interesting point, I wonder if operating on branches that are
> not checked out is potentially confusing for new user though.

Yes, absolutely.  Especially since I'm very focused on having
operations be O(changes), not O(size-of-repo).  The amount of work
people go through to avoid backporting because checking out other
branches is too expensive is impressive.  But designing things for the
user that needs scaling to big repos isn't always the friendliest to
new users.  And I admittedly am focused on performance above other
aspects, and in some cases that might leave new users behind.

> >  Not sure I want to harp on this too much, as
> >      I have a suspicion I might be creating a tool for experts with
> >      complicated use cases, but it's a minor quibble.)

Oh, heh.  I guess I knew at the time I wrote this that people might
point out ways I was not being newbie friendly.  :-)

> > * Performance
> >
> >    * jj is slaughtering us on rebase speed[2].  I would like us to become
> >      competitive.  (I dropped a few comments in the link at [2] about why
> >      git is currently so bad.)
> >
> >    * From [3], there was a simple 4-patch series in linux.git that took
> >      53 seconds to rebase.  Switching to ort dropped it to 16 seconds.
> >      While that sounds great, only 11 *milliseconds* were needed to do
> >      the actual merges.  That means almost *all* the time (>99%) was
> >      overhead!  Big offenders:
> >
> >      * --reapply-cherry-picks should be the default
>
> I agree that can be a performance hit if there are a lot of upstream
> commits, but it is also a usability feature as it means we don't stop
> and ask the user what to do with the commits that have been upstreamed
> which wastes more of their time. I think maybe we want different
> defaults for the server use case than the user replaying commits or
> perhaps default to dropping commits that become empty.

Oh, sure, there are people who will like different options and
configurations.  Non-performant features can certainly be options, I
was mostly harping on the defaults for git-replay.  It should default
to fast, even if/when it moves beyond server-side only.

> >      * can_fast_forward() should be ripped out, and perhaps other extraneous
> >        revision walks
>
> We should look at doing that at least for the merge backend which has
> skip_unnecessary_picks(). I think it is useful to tell the user that the
> branch was not updated by the rebase though.

Which could be done as a very simple post-operation check: do pre- and
post- hashes match?  No need for a bunch of logic to check what's
going to happen that'll result in slow startup in large repositories.
Default should be fast and scale to large repos.

> >      * avoid updating working tree, index, refs, reflogs, and control
> >        structures except when needed (e.g. hitting a conflict, or operation
> >        finished)
>
> Not writing to disc unless we need to is sensible. Having said that for
> interactive rebases I do find having HEAD's reflog record all the picks
> useful to unpick what went wrong if mess something up.

Adding an option for writing HEAD's reflog at every step along the way
might be reasonable.  But I specifically was designing a new command
with speed as priority number 1.  Updating some external file with
every commit thus should not be the default for this new command.  I
know that's incompatible with how git-rebase works, but this isn't
rebase.

Actually, there's a second problem as well.  git-replay shouldn't
update _anything_ (other than adding new un-referenced
blobs/trees/commits), until it is entirely done.  In fact, even when
it's done, it doesn't update any refs by default; it simply prints out
some "update-ref" instructions for `git update-ref --stdin` (though I
was thinking of adding a flag that would make git-replay also do the
ref updates, as the very final thing the program did).

And there's a third problem.  HEAD isn't being updated in many cases,
so why should its reflog be?

> >    * Other performance ideas:
> >
> >      * single-file control structures instead of directory of files
>
> I like the idea as it should make it easier to keep the on disc state
> consistent, but I'm not sure how much of an issue that is in practice as
> we only read/write the files once each time git is run. The bigger slow
> down is writing the author script, commit message, list of rewritten
> commits, todo list and done files with each pick.

The author script, commit message, list of rewritten commits, todo
list and done files are all control structures that are part of that
directory of files I was complaining about.  As far as I can tell, you
said it's not an issue, but then immediately said the opposite.  What
did you take the "directory of files" control structures to mean?
...and is there some other way I could word this that would be better?

Also, if we speed up the other stuff enough, the writing of the
control files becomes much more important.  See the example elsewhere
where I mentioned that for a cherry-pick of a single commit that took
a minute under git-rebase, that actually creating the new toplevel
tree only required like 11 milliseconds.  If we can speed things up
dramatically enough, then all the stuff that used to just not matter,
suddenly starts mattering a lot.  (This happened a lot when I was
rewriting the merge machinery and optimizing it.)  And, I think that
writing one file is going to be faster than writing N files
(especially on spinny disks, and yes my laptop has a spinny disk).

Also, single file control structures are easier to make safe against
an ill-timed Ctrl-C, though I suspect that's so rarely a problem that
it may not be worth worrying much about.

> >      * avoid forking subprocesses unless explicitly requested (e.g.
> >        --exec, --strategy, --run-hooks).  For example, definitely do not
> >        invoke `git commit` or `git merge`.
>
> Good, that matches what the sequencer does for non-merge commits when
> we're not editing the commit message.

...and when not committing staged changes after a stopped interactive
rebase, and when not trying to ensure identical error messages when an
issue is hit.  :-)

This is good; sequencer is always improving.  If it could just stop
calling git commit in these four extra cases, we'd be rid of the
git-commit forking for good.

> >      * Sanitize hooks:
> >
> >        * dispense with all per-commit hooks for sure (pre-commit,
> >          post-commit, post-checkout).
>
> I agree we should not be running those (we don't run the pre-commit hook
> anyway).

...unless picking a merge commit or editing the commit message or...  :-)

> However we had a bug report when cherry-pick stopped running
> the "prepare-commit-msg" hook (see
> https://lore.kernel.org/git/CAKdAkRQuj1hfKeckjuR2oP+8C1i+ZR36O-+aRYif4ufaS_zs+w@mail.gmail.com/).
> That shouldn't matter for the server but we should bear it in mind when
> it comes to other use cases.

git-fast-export and git-fast-import do not run hooks, in part for
speed.  I don't think git-fast-patch, i.e. git-replay should either,
at least not by default.  I'm well aware that it's impossible to make
that kind of change to git-rebase, but I'm not trying to design
another rebase.

Another thing that might horrify you that I don't think I wrote down
is that when interactivity is added, I don't want replay to pre-parse
and validate the script, much like fast-import doesn't pre-parse and
validate its input.  Not validating would be anathema to how rebase
operates (because rebase muddies things as it goes -- the worktree and
the index AND refs AND reflogs, and it's slow, so you don't want to
start down that path until you're confident that people didn't make
simple mistakes).  As such, this is a change that I'd never even
consider suggesting for rebase, but it's exactly the kind of thing I
want for replay.

> >        * pre-rebase also seems to assume exactly 1 ref is written, and
> >          invoking it repeatedly would be stupid.  Plus, it's specific
> >          to "rebase".  So...ignore?  (Stolee's --ref-update option for
> >          rebase probably broke the pre-rebase assumptions already...)
>
> If replay is a plumbing command then skipping the pre-rebase hook makes
> sense as scripts can call it themselves if they want to. For a porcelain
> command keeping a hook that can prevent it from rewriting commits  that
> are already upstream (which I think is one of the main uses of the
> pre-rebase hook) would be good.

pre-rebase has a broken design; it already doesn't work correctly with
Stolee's --ref-update option.  So, even if we wanted to support this
kind of functionality, it certainly wouldn't be that script.

Also, the name is pre-rebase, not pre-cherry-pick, and doesn't apply
to both commands.  That means there's a second reason the design is
broken.  So, even if we wanted to support this kind of functionality,
it'd have to be a differently named script with a different design.

But, perhaps more importantly than either of the above, is that replay
doesn't change any refs, so why do we need a hook to prevent it from
changing refs?

> >        * post-rewrite hook might make sense, but fast-import got
> >          exempted, and I think of replay like a patch-based analogue
> >          to the snapshot-based fast-import.
>
> If we don't call the hook it would be good to have a option that outputs
> that information so scripts can request it if they want.

Good point, although the fact that replay doesn't update refs probably
means I didn't even need to mention these hooks.

> Also we should
> think about if/when we want to update the notes associated with replayed
> commits.

_Very_ good point.  There should definitely be an option for that,
maybe even a config option.

> >      * When not running server side, resolve conflicts in a sparse-cone
> >        sparse-index worktree to reduce number of files written to a
> >        working tree.  (See below as well)
> >
> >      * [High risk of possible premature optimization] Avoid large
> >        numbers of newly created loose objects, when replaying large
> >        numbers of commits.  Two possibilities: (1) Consider using
> >        tmp-objdir and pack objects from the tmp-objdir at end of
> >        exercise, (2) Lift code from git-fast-import to immediately
> >        stuff new objects into a pack?
> >
> > * Multiple branches and non-checked out branches
> >
> >    * The ability to operate on non-checked out branches also implies
> >      that we should generally be able to replay when in a dirty working
> >      tree (exception being when we expect to update HEAD and any of the
> >      dirty files is one that needs to be updated by the replay).
> >
> >    * Also, if we are operating locally on a non-checked out branch and
> >      hit a conflict, we should have a way to resolve the conflict without
> >      messing with the user's work on their current branch.

This stuff really shouldn't have been in the cover letter for this
series, but since it is...

> That sounds tricky to do in a user friendly way.

Yep, totally agreed.

But, I really, really want a command that operates in O(changes)
rather than O(repo size), and which can operate on non-checked-out
branches, and even if the user has a dirty working tree.

There are other choices too -- e.g. if replay is fast enough, just
fail if the user has a dirty working tree and tell them to re-issue
the command after first cleaning it up, allowing us to use their
working tree for conflict resolution.  However, the level of IDE
thrashing that happens when people check out other branches (and the
subsequent loss of productivity they experience after resolving the
simple conflict and switching back) means I'm not willing to have the
only option be resolving conflicts in the current working tree.  I
think something very different than how git-rebase and git-cherry-pick
behave currently are needed, at least as an option.

> >      * Idea: new worktree with sparse cone + sparse index checkout,
> >        containing only files in the root directory, and whatever is
> >        necessary to get the conflicts
>
> If the user has not asked for a sparse checkout then this could be
> surprising. Sometimes I find it helpful to be able to poke about in
> other source files when resolving a conflict. I also often build and
> test after resolving a conflict which requires more than just the
> conflict to be checked out.

Sure, some people may need an option to have conflicts be resolved in
the current working tree, or perhaps even in a separate full tree.
But both are such performance killers that an O(changes) option that
doesn't mess with the current working tree is very much needed for
some usecases.

But again, that's **way** outside the scope of this series.

> >      * Companion to above idea: control structures should be written to
> >        $GIT_COMMON_DIR/replay-${worktree}, so users can have multiple
> >        replay sessions, and so we know which worktrees are associated
> >        with which replay operations.
>
> We certainly want some way of making sure we only update a given ref in
> one replay session, and have checks to for whether the ref is checked
> out anywhere as we do now for rebase --update-refs. That seems to be
> lacking in the patches adding ref updating in this series.

Yeah, that was in patch 9.  It should be dropped or have patch 10
squashed into it.

At that point, this series won't contain any ref updating, but will
only have instructions that can be passed to `git update-ref --stdin`.
At that point, no such checks will be necessary for this series (as it
all belongs in `git update-ref --stdin` instead).

> > * The code die()s if there are any conflict. No resumability. No nice
> >    output. No interactivity.
>
> I can see that on a server you might not want any output, but if I run
> it locally it would be nice to have a message saying which paths have
> conflicts. Maybe we could add a --quiet flag for the server rather than
> removing the existing messages?

I think the choice of what to do for conflicts becomes really complex,
really quick.  Just look at git-merge-tree, which is a much simpler
case, and those kinds of questions blocked the series from progressing
for like half a year last year.

I don't think we're ready to answer these questions.  In fact, that's
precisely why I never submitted this series, even though I've had the
bits that Christian is pushing in a working state for about 9 months
now.

I think we should defer all conflict handling other than exit status
until later (or continue deferring this series until conflict handling
is ready, but that may be a long time).

> > * Signed commits are not properly handled. It's not clear what to do
> >    to such commits when replaying on the server side.
>
> Yes on the server where you don't have access to the signing key there
> is not much need for replay to have a signing option.

And on the client side, it's the one place where I'm almost certainly
going to be forced to accept forking a subprocess, though rest assured
that I'll grumble about it incessantly.  :-)

> > * exit status: a successful, non-conflicted replay exits with code
> >    0. When the replay has conflicts, the exit status is 1. If the
> >    replay is not able to complete (or start) due to some kind of error,
> >    the exit status is something other than 0 or 1. It has been
> >    suggested in an internal review that conflicts might want to get a
> >    more specific error code as an error code of 1 might be quite easy
> >    to return by accident. It doesn't seem to me from their docs (which
> >    might want to be improved, I didn't look at the code) that other
> >    commands like `git merge` and `git rebase` exit with a special error
> >    code in case of conflict.
>
> I don't think we've ever had a special "conflict" error code but it
> would be useful for scripts if replay had one. Does replay return a
> different exit code for "merge conflicts" and "cannot merge because it
> would overwrite an untracked file"? Does it have an exit code for "the
> commit becomes empty" or are those patches unconditionally dropped?

I'm not sure what Christian meant here, but as you say there's
certainly no different special codes in the merge machinery.

> > * --advance and --contained: these two advanced options might not
> >    belong to this first series and could perhaps be added in a followup
> >    series in separate commits. On the other hand the code for
> >    --contained seems involved with the code of --advance and it's nice
> >    to see soon that git replay can indeed do cherry-picking and rebase
> >    many refs at once, and this way fullfil these parts of its promise.
>
> Once I understood what these options did the names made sense, but I
> could not tell from the names what they were going to do. For me
> "--cherry-pick" and "--update-refs" would have been clearer. It might be
> worth splitting this patch so the individual options are added separately.

The output of `git replay`, with or without these options, is meant to
be passed as input to `git update-refs --stdin`.  As such, naming an
option --update-refs for this command just seems inherently confusing.

I also feel like --update-refs is a name coded more around
implementation details than end-user description of behavior (and as
far as implementation details are concerned, this options is not tied
to an interactive backend the way the --update-refs options in rebase
is).


In contrast, --cherry-pick might be reasonable.  But I worry it'll be
read backwards by people.  I want:

   git replay --OPTION branch upstream..commit

to make sense and I worry that OPTION==cherry-pick will cause users to
think `branch` is being cherry-picked rather than `upstream..commit`.
What this command does is advance `branch` by cherry-picking
`upstream..commit` onto it.

> > * only 2 patches: this patch series can be seen from a high level
> >    point of view as 1) introducing the new `git replay` command, and 2)
> >    using `git replay` to replace, and get rid of, the fast-rebase
> >    test-tool command. The fact that not much of the original
> >    fast-rebase code and interface is left would agree with that point
> >    of view. On the other hand, fast-rebase can also be seen as a first
> >    iteration towards `git replay`. So it can also make sense to see how
> >    `git replay` evolved from it.
>
> Starting with fast-rebase means one has to checkout the first patch to
> see what code we're adding to replay.c and to make sense of the later
> patches that remove code. It would be interesting to compare this series
> to one that started from scratch but I guess that would be quite a bit
> of work.

Yeah, I was probably wrong to suggest to Christian to use fast-rebase
as the starting point.  That is how I started my series, and multiple
people had reviewed it and it seemed like it'd be useful to leverage
that past review, but the reviews of these series so far suggest I may
have made the wrong recommendation to Christian.

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH 11/14] replay: use standard revision ranges
  2023-04-14 14:09   ` Derrick Stolee
  2023-04-14 14:23     ` Derrick Stolee
@ 2023-04-15 18:30     ` Elijah Newren
  1 sibling, 0 replies; 208+ messages in thread
From: Elijah Newren @ 2023-04-15 18:30 UTC (permalink / raw)
  To: Derrick Stolee
  Cc: Christian Couder, git, Junio C Hamano, Patrick Steinhardt,
	Johannes Schindelin, John Cai, Christian Couder

On Fri, Apr 14, 2023 at 7:09 AM Derrick Stolee <derrickstolee@github.com> wrote:
>
> On 4/7/2023 3:24 AM, Christian Couder wrote:
> > From: Elijah Newren <newren@gmail.com>
> >
> > Instead of the fixed "<oldbase> <branch>" arguments, the replay
> > command now accepts "<revision-range>..." arguments in a similar
> > way as many other Git commands. This makes its interface more
> > standard and more flexible.
>
> Unfortunately, while doing this, you have broken the --onto
> logic:
>
>  $ git replay --onto HEAD~2 HEAD~1 HEAD
>  fatal: replaying down to root commit is not supported yet!

One of the things Christian asked/proposed a while ago was instead of
modifying fast-rebase into git-replay, just build git-replay from the
ground up.  I argued for using fast-rebase as a starting point, but I
think you've perhaps given an example for why I may have been wrong to
do so.  It has caused confusion, because fast-rebase used a very rigid
syntax and specification (because it was a simple test script designed
to handle exactly one simple setup) that is completely against what we
want here.

In particular, you are probably thinking of
    $ git replay --onto HEAD~2 HEAD~1 HEAD
as meaning replay the commits in the range HEAD~1..HEAD (i.e. just
HEAD) with the new base of HEAD~2.  That's the inflexible way
fast-rebase worked.  We are dispensing with that here, though; your
command means replay the commits in the history of either HEAD~1 or
HEAD (all the way to the root since you had no negative references) on
top of HEAD~2.  If you had instead said:
    $ git replay --onto HEAD~2 HEAD~1..HEAD
then I think `git replay` handles it just fine.  Christian did cover
this in the commit message, but it's perhaps subtle and easily missed.

Anyway, at no point in this series does `git replay` support rebasing
commits back to the root, so the error message is what I'd expect.
The problem was we weren't clear enough about a different syntax being
expected.

> The rev-walk you are supplying by this line...
>
> > +     argc = setup_revisions(argc, argv, &revs, NULL);
>
> is taking the remaining arguments and using them as tips to
> walk. We need to be able to recognize that --onto A B C means
> that A is the new base and our walk is B..C.
>
> I'm not sure if there might be a way to use a callback for
> the --onto option and pull out the next three options into
> 'new-base', 'old-base', 'tip' values or something.
>
> Overall, I don't think being flexible in the CLI is of high
> value for this command. Let's be as prescriptive as possible.
>
> Something like:
>
>         'git replay [options] <base> <tip>'
>         This mode means to rebase <tip> onto <base>,
>         detecting the range of commits to rewrite.
>
>         'git replay [options] <new-base> <old-base> <tip>'
>         This mode means to rebase the range <old-base>..<tip>
>         onto <new-base>.
>
> We don't even need "--onto" for these positional arguments.

So, from my view, the problem with this alternative design is that it
inflexibly hardcodes a linear range of commits, in a way that likely
precludes future extension.  In particular it:

  * precludes handling multiple divergent branches, which I think was
a core design requirement
  * seems problematic for extending this to handle replaying of merges
(where being able to select what to replay needs more control)
  * more generally from the above two, this precludes the opportunity
to specify both multiple positive and negative refs
  * precludes things like using `--ancestry-path=<commit>` which was
specifically designed for use with git-replay[1]
  * may not work well with --first-parent, which I think should be
allowed (in fact, --first-parent in some instances is a specialization
of --ancestry-path=<commit>)
  * seems to presume that rebasing is the only thing we want to do,
possibly precluding supporting cherry-pick-like commands

[1] See https://lore.kernel.org/git/CABPp-BHmj+QCBFDrH77iNfEU41V=UDu7nhBYkAbCsbXhshJzzw@mail.gmail.com/,
look for "Here's my usecase"

And on a minor note, it also copies the positional argument UI design
from rebase that I've always considered to be an annoying UI design
flaw.  (Not sure if others agree with me on that, but it's always
bothered me.)

Since you followed up on this, I'll add more detail in response to
your other email.

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH 11/14] replay: use standard revision ranges
  2023-04-14 14:23     ` Derrick Stolee
@ 2023-04-15 19:07       ` Elijah Newren
  2023-04-16  5:28         ` Elijah Newren
                           ` (2 more replies)
  2023-04-18  4:58       ` Elijah Newren
  1 sibling, 3 replies; 208+ messages in thread
From: Elijah Newren @ 2023-04-15 19:07 UTC (permalink / raw)
  To: Derrick Stolee
  Cc: Christian Couder, git, Junio C Hamano, Patrick Steinhardt,
	Johannes Schindelin, John Cai, Christian Couder

On Fri, Apr 14, 2023 at 7:23 AM Derrick Stolee <derrickstolee@github.com> wrote:
>
> On 4/14/2023 10:09 AM, Derrick Stolee wrote:
> > On 4/7/2023 3:24 AM, Christian Couder wrote:
> >> From: Elijah Newren <newren@gmail.com>
> >>
> >> Instead of the fixed "<oldbase> <branch>" arguments, the replay
> >> command now accepts "<revision-range>..." arguments in a similar
> >> way as many other Git commands. This makes its interface more
> >> standard and more flexible.
> >
> > Unfortunately, while doing this, you have broken the --onto
> > logic:
> >
> >  $ git replay --onto HEAD~2 HEAD~1 HEAD
> >  fatal: replaying down to root commit is not supported yet!
> >
> > The rev-walk you are supplying by this line...
> >
> >> +    argc = setup_revisions(argc, argv, &revs, NULL);
> >
> > is taking the remaining arguments and using them as tips to
> > walk. We need to be able to recognize that --onto A B C means
> > that A is the new base and our walk is B..C.
>
> I'm realizing after hitting "send" that this change is
> intentional (based on your test updates). I don't agree with
> it, though.
>
> Sending arbitrary command-line arguments to setup_revisions()
> creates an opportunity for behavior you are not expecting.

Note that fast-export has always accepted the full range of
setup_revisions() flags, even though many options make no sense.
Perhaps we could tighten up the parsing for fast-export, but limiting
fast-export to three commits in order to avoid other nonsensical
combinations would destroy its utility.

You may think I've gone on a tangent, but I think it's an important
comparison point here.  I think limiting replay to three commits does
the same: destroys its intended utility.  replay is a patch-based
analogue to fast-export, and we need a wide range of the revision
ranges for it to work.

(There are other interesting comparisons here too.  For example, if
someone specifies commits rather than branches to fast-export, then
things break down: new commits may be written but nothing takes
advantage of them so they merely become garbage to be gc'ed later.
Likely not what the user wanted.  The exact same issue existed for
replay.  However, in replay's case I tried to add some sanity checking
for it -- perhaps that sanity checking would be useful to copy to
fast-export?)

> For instance, can users pass multiple ranges?

There's no such thing as multiple ranges for most commands (see
f302c1e4aa0 ("revisions(7): clarify that most commands take a single
revision range", 2021-05-18))

However, users should absolutely be allowed to specify something like

  $ git replay --onto new-base master..my-topic some-base..other-topic

which is one revision range, and I'd expect replay to take commits in
the history of either my-topic or other-topic, which are not in the
history of either master or some-base, and replay them.  It'd be
equivalent, in most cases, to the separate commands:

   $ git replay --onto new-base ^master ^some-base my-topic
   $ git replay --onto new-base ^master ^some-base other-topic

The first of the two commands above, for example, would replay the
commits in my-topic back to nearer of (master, some-base) and do the
replaying on top of new-base.  The second command is similar, but with
my-topic replaced by other-topic.

Notice, though, that I said most cases.  There's an important case
where the single command is different from the two commands: when
my-topic and other-topic share common commits (which is not also
shared by master and some-base), we want the replayed common history
to remain common.  If you do two separate commands, the replayed
common history is no longer common (those commits will instead be
duplicated with separate committer dates).  So, supporting the single
command form is an intrinsic design consideration for git-replay.

> Can users supply --first-parent?

Absolutely, and it might be a useful replacement for or supplement to
--[no-]rebase-cousins.

Also, we definitely want to allow --ancestry-path and
--ancestry-path=<commit>, the latter of which was specifically written
for use in git-replay.

In fact, --first-parent sometimes might be equivalent to a particular
specialization of --ancestry-path=<commit>, so yeah, I definitely want
to allow it.

> What happens if they add an --author filter?

git fast-export accepts full revision ranges, including ones that make
no sense like this.  We could add patches to exclude such options, to
both replay and fast-export.  Or just document that they make no
sense.

To answer the question as asked, though, let me first provide a little
background: for a commit A with parent B, git-replay checks whether
the parent of the current commit (i.e. for A that'd be B) has been
replayed (as B').  If so, it will make the parent of A' be B'.  If
there is no B' (e.g. because B was part of "upstream" and thus wasn't
replayed), it makes the parent of A' be whatever is passed to --onto.
This allows git-replay to not only rebase/replay a linear set of
patches, but a partially or even fully divergent set of branches.

Now, with that background, if someone passes --author, there will be a
lot of parent commits that aren't replayed, resulting in each new
"first" commit being rebased on top of --onto, and only the final
one(s) being referenced by the updated ref commands.  In other words,
it'll leave lots of orphaned commits behind and only keep the "newest"
contiguous ones belonging to the author.  In simpler terms, passing
--author to git-replay would "make a really big mess of things" from
the user perspective, but an implementationally well-defined mess.
This, again, is similar to passing --author to fast-export -- that
tool has very different primitives involved causing it to make a
completely different big mess of things (and perhaps even bigger mess
of things), but the mess is at least implementationally well-defined
to those that understand the data structures and implementation.  (And
yes, I thought of this exact case while developing the tool, for what
it's worth.)

Does that mean I want to allow people to pass --author to fast-export
or replay?  I'd be perfectly happy with patches that prohibit it in
both tools.  But I'm also not worried about people off-roading and
trying it and getting non-sense.  A documentation patch stating that
using options that "disconnect" history make no sense with either
replay or fast-export might be the easiest (and most future-proof)
solution.

> [unstated: and what about other similar options?]

I'd really rather not have an allowlist of which revision options
should be allowed for use by git-replay.  A denylist might be okay, if
also implemented for fast-export, but I'm more of the opinion that we
just document that both commands only make sense with "contiguous"
history.

Most importantly, though, at a minimum I do absolutely want to allow
several negative revisions to be specified, several positive revisions
to be specified, and special flags like --ancestry-path or
--first-parent to be specified.  There may well be additional flags,
both current and future, that should be allowed too.

In short, I don't want another limited rebase; I want a more generic tool.


Hope that helps,
Elijah

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH 11/14] replay: use standard revision ranges
  2023-04-15 19:07       ` Elijah Newren
@ 2023-04-16  5:28         ` Elijah Newren
  2023-04-17 14:05         ` Derrick Stolee
  2023-04-17 15:45         ` Junio C Hamano
  2 siblings, 0 replies; 208+ messages in thread
From: Elijah Newren @ 2023-04-16  5:28 UTC (permalink / raw)
  To: Derrick Stolee
  Cc: Christian Couder, git, Junio C Hamano, Patrick Steinhardt,
	Johannes Schindelin, John Cai, Christian Couder

On Sat, Apr 15, 2023 at 12:07 PM Elijah Newren <newren@gmail.com> wrote:
<snip>
> > What happens if they add an --author filter?
>
> git fast-export accepts full revision ranges, including ones that make
> no sense like this.  We could add patches to exclude such options, to
> both replay and fast-export.  Or just document that they make no
> sense.
>
> To answer the question as asked, though, let me first provide a little
> background: for a commit A with parent B, git-replay checks whether
> the parent of the current commit (i.e. for A that'd be B) has been
> replayed (as B').  If so, it will make the parent of A' be B'.  If
> there is no B' (e.g. because B was part of "upstream" and thus wasn't
> replayed), it makes the parent of A' be whatever is passed to --onto.
> This allows git-replay to not only rebase/replay a linear set of
> patches, but a partially or even fully divergent set of branches.

A small clarification, which only matters to future work and not this
particular series: the above description where ONTO is the fallback
for a lack of B', is for first parent only.  For a second (or later)
parent of A, which we'll refer to as C, if C has not been replayed as
part of this operation, then we simply use the unmodified C as the
second (or later) parent of A'.

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH 11/14] replay: use standard revision ranges
  2023-04-15 19:07       ` Elijah Newren
  2023-04-16  5:28         ` Elijah Newren
@ 2023-04-17 14:05         ` Derrick Stolee
  2023-04-18  5:54           ` Elijah Newren
  2023-04-17 15:45         ` Junio C Hamano
  2 siblings, 1 reply; 208+ messages in thread
From: Derrick Stolee @ 2023-04-17 14:05 UTC (permalink / raw)
  To: Elijah Newren
  Cc: Christian Couder, git, Junio C Hamano, Patrick Steinhardt,
	Johannes Schindelin, John Cai, Christian Couder

On 4/15/2023 3:07 PM, Elijah Newren wrote:
> On Fri, Apr 14, 2023 at 7:23 AM Derrick Stolee <derrickstolee@github.com> wrote:

>> Sending arbitrary command-line arguments to setup_revisions()
>> creates an opportunity for behavior you are not expecting.
 
>> [unstated: and what about other similar options?]
> 
> I'd really rather not have an allowlist of which revision options
> should be allowed for use by git-replay.  A denylist might be okay, if
> also implemented for fast-export, but I'm more of the opinion that we
> just document that both commands only make sense with "contiguous"
> history.
> 
> Most importantly, though, at a minimum I do absolutely want to allow
> several negative revisions to be specified, several positive revisions
> to be specified, and special flags like --ancestry-path or
> --first-parent to be specified.  There may well be additional flags,
> both current and future, that should be allowed too.
> 
> In short, I don't want another limited rebase; I want a more generic tool.

The primary value of git-replay (to me) is that we can do "simple" rebases
without a worktree or user interaction. To me, simple rebases mean apply a
linear sequence of commits from a single branch onto another branch (and
specifying an "old base" is simple enough to include here). It also means
that we abort completely if there is a conflict (and handle conflict
resolution in later changes).

The sooner we can deliver that, the better we can deliver on its potential
(and expand its functionality to be more generic).

And this is generally my preference, but we shouldn't get functionality
"by accident" but instead do it with full intention and strong testing.

I will always think that certain shortcuts should not be used. This
includes passing arguments directly to setup_revisions() and using
pathspec parsing when a path string will work just fine.

If we have a need for a feature, then we should add it explicitly and
carefully. The surface area of setup_revisions() is far too large to have
confidence that we are not exposing bugs for users to trip on later.

Thanks,
-Stolee

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH 11/14] replay: use standard revision ranges
  2023-04-15 19:07       ` Elijah Newren
  2023-04-16  5:28         ` Elijah Newren
  2023-04-17 14:05         ` Derrick Stolee
@ 2023-04-17 15:45         ` Junio C Hamano
  2023-04-18  5:58           ` Elijah Newren
  2 siblings, 1 reply; 208+ messages in thread
From: Junio C Hamano @ 2023-04-17 15:45 UTC (permalink / raw)
  To: Elijah Newren
  Cc: Derrick Stolee, Christian Couder, git, Patrick Steinhardt,
	Johannes Schindelin, John Cai, Christian Couder

Elijah Newren <newren@gmail.com> writes:

>> For instance, can users pass multiple ranges?
>
> There's no such thing as multiple ranges for most commands (see
> f302c1e4aa0 ("revisions(7): clarify that most commands take a single
> revision range", 2021-05-18))
>
> However, users should absolutely be allowed to specify something like
>
>   $ git replay --onto new-base master..my-topic some-base..other-topic
>
> which is one revision range, and I'd expect replay to take commits in
> the history of either my-topic or other-topic, which are not in the
> history of either master or some-base, and replay them.

It is one revision range "^master ^some-base my-topic other-topic"
if we let traditional revision parser to work on them, and in many
topologies, it would mean something unexpected for users who thought
that the user gave two ranges.  E.g. some-base may be an ancestor of
master, in which case commits in the "some-base..master" range is
not included in the result.  Or some-base may be able to reach
my-topic, in which case no commits from master..my-topic range
appears in the result, etc.

But should we be forever limited by the "A..B is always equivalent
to ^A B"?

Shouldn't replaying master..my-topic and some-base..other-topic,
when some-base is an ancestor of master, replay two overlapping
range, possibly recreating some commits twice (when some-base falls
in between master..my-topic, for example), if the user is willing to
accept the consequences?

We can still keep the "so called 'range' is just commits that are
reachable from a positive end and are not reachable from any
negative end, and by definition there is no such thing as multiple
ranges" as an option for advanced users and trigger the semantics
when one negative end is written explicitly with ^A notation, but in
applications like cherry-pick where it is convenient to work on
multiple ranges, we may want to extend our worldview to allow A..B
C..D (especially when these two are distinct ranges---imagine the
case where B is an ancestor of C) to mean "we have two ranges, and
we mean the union of the commits in these two ranges".

We'd need to design carefully what should happen for trickier cases
like A..B C (does it mean the traditional "a single range of commits
that are reachable from either B or C, but never reachable from A",
or does it mean "the union of commits A..B that are reachable from B
but not from A and commits C that are reachable from C all the way
down to root"?), though.

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH 11/14] replay: use standard revision ranges
  2023-04-14 14:23     ` Derrick Stolee
  2023-04-15 19:07       ` Elijah Newren
@ 2023-04-18  4:58       ` Elijah Newren
  1 sibling, 0 replies; 208+ messages in thread
From: Elijah Newren @ 2023-04-18  4:58 UTC (permalink / raw)
  To: Derrick Stolee
  Cc: Christian Couder, git, Junio C Hamano, Patrick Steinhardt,
	Johannes Schindelin, John Cai, Christian Couder

On Fri, Apr 14, 2023 at 7:23 AM Derrick Stolee <derrickstolee@github.com> wrote:
>
> (I was able to get a segfault by rebasing this series with
> --author=stolee because the commit list became empty. Something
> to watch for.)
>
> > Something like:
> >
> >       'git replay [options] <base> <tip>'
> >       This mode means to rebase <tip> onto <base>,
> >       detecting the range of commits to rewrite.
> >
> >       'git replay [options] <new-base> <old-base> <tip>'
> >       This mode means to rebase the range <old-base>..<tip>
> >       onto <new-base>.
>
> For that reason, I think we should be using explicit argument
> parsing in the builtin and only transform arguments we
> understand into the setup_revisions() (by building a strvec).

So, it turns out that this suggested solution wouldn't have helped
prevent the segfault you found.  If someone merely passed <old-base>
== <tip>, they'd see the same segfault.  In fact, I think you found a
latent bug in merge-ort.  In particular, cmd_replay() has

    struct merge_options merge_opt;
    struct merge_result result;

    init_merge_options(&merge_opt, the_repository);
    memset(&result, 0, sizeof(result));

    <do N merges, for some value of N>

    merge_finalize(&merge_opt, &result);

This code segfaults if N is 0, because merge_finalize() doesn't return
early when result->priv is NULL.

We never triggered this before, because this is the very first code
example we've had where someone has tried to call merge_finalize()
without first performing a merge.  Anyway, nice find and thanks for
reporting!

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH 11/14] replay: use standard revision ranges
  2023-04-17 14:05         ` Derrick Stolee
@ 2023-04-18  5:54           ` Elijah Newren
  2023-04-18 13:10             ` Derrick Stolee
  0 siblings, 1 reply; 208+ messages in thread
From: Elijah Newren @ 2023-04-18  5:54 UTC (permalink / raw)
  To: Derrick Stolee
  Cc: Christian Couder, git, Junio C Hamano, Patrick Steinhardt,
	Johannes Schindelin, John Cai, Christian Couder

On Mon, Apr 17, 2023 at 7:05 AM Derrick Stolee <derrickstolee@github.com> wrote:
>
> On 4/15/2023 3:07 PM, Elijah Newren wrote:
> > On Fri, Apr 14, 2023 at 7:23 AM Derrick Stolee <derrickstolee@github.com> wrote:
>
> >> Sending arbitrary command-line arguments to setup_revisions()
> >> creates an opportunity for behavior you are not expecting.
>
> >> [unstated: and what about other similar options?]
> >
> > I'd really rather not have an allowlist of which revision options
> > should be allowed for use by git-replay.  A denylist might be okay, if
> > also implemented for fast-export, but I'm more of the opinion that we
> > just document that both commands only make sense with "contiguous"
> > history.
> >
> > Most importantly, though, at a minimum I do absolutely want to allow
> > several negative revisions to be specified, several positive revisions
> > to be specified, and special flags like --ancestry-path or
> > --first-parent to be specified.  There may well be additional flags,
> > both current and future, that should be allowed too.
> >
> > In short, I don't want another limited rebase; I want a more generic tool.
>
> The primary value of git-replay (to me) is that we can do "simple" rebases
> without a worktree or user interaction. To me, simple rebases mean apply a
> linear sequence of commits from a single branch onto another branch (and
> specifying an "old base" is simple enough to include here). It also means
> that we abort completely if there is a conflict (and handle conflict
> resolution in later changes).
>
> The sooner we can deliver that, the better we can deliver on its potential
> (and expand its functionality to be more generic).

Limiting the initial scope of this tool to a smaller set of features
is okay (and, in fact, Christian has already done that), but we really
need to avoid painting ourselves into a corner if we change the design
as part of that limiting.  As I stated previously[1], I'm worried this
is happening.

[1] https://lore.kernel.org/git/CABPp-BH7ZPBtV5Hu_-_yVdqVmiydW7_s_LYAtwbPuYSbRQiuQw@mail.gmail.com/,
under "from my view"

> And this is generally my preference, but we shouldn't get functionality
> "by accident"

All of the specific flags and cases you brought up so far were ones I
already carefully considered over a year ago, so the "by accident"
comment seems a little unfair.

> but instead do it with full intention

The intention is listed in the subject of the commit message of this
patch.  I've also explicitly stated my desire on this list to make a
tool which replays based on a general range expression multiple
times[2][3][4][5].  And there are tests for general range expressions
in this series being reviewed.  I don't understand why you might think
I didn't intend to use general range expressions.

[2] https://lore.kernel.org/git/CABPp-BEAqP7maTVw82Qr8mn-sxPzXmHnE_mTKf2pg6hVYAJSUw@mail.gmail.com/
[3] https://lore.kernel.org/git/CABPp-BHmj+QCBFDrH77iNfEU41V=UDu7nhBYkAbCsbXhshJzzw@mail.gmail.com/
[4] https://lore.kernel.org/git/CABPp-BHARfYcsEM7Daeb7+vYxeB9Awo8=qbrOMXG6BQ0gX1RiA@mail.gmail.com/
[5] https://lore.kernel.org/git/CABPp-BEOV53oBoBp4YjiRfksZMmAADanZUUemhxwn7Wor=m-nA@mail.gmail.com/

> and strong testing.

The series as it stands already has some good testing for multiple
divergent branches, which already takes it beyond the simplistic cases
you were focusing on.

Yes, there should obviously be tests for even more cases, and yes I
did leave this series in a work-in-progress state nearly a year ago.
My Git time has been limited, and so I have tended to only work on
things that needed short bursts of attention, like responding to the
list, or the cache.h refactoring...

But I'm kind of at a loss trying to understand this paragraph.  Am I
misinterpreting what you are saying?

> I will always think that certain shortcuts should not be used. This
> includes passing arguments directly to setup_revisions() and using
> pathspec parsing when a path string will work just fine.
>
> If we have a need for a feature, then we should add it explicitly and
> carefully.  The surface area of setup_revisions() is far too large to have
> confidence that we are not exposing bugs for users to trip on later.

My original plan was to just extend rebase rather than create a new
command.  While it'd be an extraordinary amount of work to change
rebase to work without touching the working tree or index, it didn't
look completely insurmountable.  The first thing I found that did look
completely insurmountable was extending it to handle rebasing several
divergent branches possibly sharing some common history.  Since
handling possibly-divergent branches was one of my primary motivating
goals, I'm really rather unwilling to accept the suggestion that we
should strip the tool and copy rebase's broken design, since that
would prevent the tool from ever being extended to do what I want and
what I *already* implemented.  There may be some kind of middle ground
we can find, but the suggestion elsewhere in this thread (to make
git-replay take three commits as positional arguments, with one an
assumed negative ref) is copying that exact inflexible design flaw.

If you want to move git-replay away from setup_revisions(), at a
minimum I think we need a proposal that can be extended to the cases I
highlighted previously:
    * allow specifying several negative revisions (or maybe even zero of them)
    * allow specifying several positive revisions
    * allow standard basic range syntax, i.e. A..B
    * allow --first-parent
    * allow --ancestry-path[=<commit>]
I think it should also be able to eventually support
    * --branches[=<pattern>]
    * --not
    * --tags[=<pattern>]
    * --remotes[=<pattern>]
    * --glob=<pattern>
    * --exclude=<glob-pattern>
    * --all
There may well be others I missed in glancing over the list.

That's a long list of things to parse, which setup_revisions() happens
to handle nicely.  You are right that setup_revisions() also has lots
of options for generating non-contiguous history that may make little
or no sense for replay (and makes no sense for fast-export either).
So, I am willing to consider other solutions if anyone has one, but
only alternative solutions which can eventually handle the above
requirements.  In particular, "three commits as positional arguments"
(with one implicitly being considered a negative ref) conflicts with
the first three bullet points above.

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH 11/14] replay: use standard revision ranges
  2023-04-17 15:45         ` Junio C Hamano
@ 2023-04-18  5:58           ` Elijah Newren
  0 siblings, 0 replies; 208+ messages in thread
From: Elijah Newren @ 2023-04-18  5:58 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Derrick Stolee, Christian Couder, git, Patrick Steinhardt,
	Johannes Schindelin, John Cai, Christian Couder

On Mon, Apr 17, 2023 at 8:45 AM Junio C Hamano <gitster@pobox.com> wrote:
>
> Elijah Newren <newren@gmail.com> writes:
>
> >> For instance, can users pass multiple ranges?
> >
> > There's no such thing as multiple ranges for most commands (see
> > f302c1e4aa0 ("revisions(7): clarify that most commands take a single
> > revision range", 2021-05-18))
> >
> > However, users should absolutely be allowed to specify something like
> >
> >   $ git replay --onto new-base master..my-topic some-base..other-topic
> >
> > which is one revision range, and I'd expect replay to take commits in
> > the history of either my-topic or other-topic, which are not in the
> > history of either master or some-base, and replay them.
>
> It is one revision range "^master ^some-base my-topic other-topic"
> if we let traditional revision parser to work on them, and in many
> topologies, it would mean something unexpected for users who thought
> that the user gave two ranges.  E.g. some-base may be an ancestor of
> master, in which case commits in the "some-base..master" range is
> not included in the result.  Or some-base may be able to reach
> my-topic, in which case no commits from master..my-topic range
> appears in the result, etc.
>
> But should we be forever limited by the "A..B is always equivalent
> to ^A B"?
>
> Shouldn't replaying master..my-topic and some-base..other-topic,
> when some-base is an ancestor of master, replay two overlapping
> range, possibly recreating some commits twice (when some-base falls
> in between master..my-topic, for example), if the user is willing to
> accept the consequences?
>
> We can still keep the "so called 'range' is just commits that are
> reachable from a positive end and are not reachable from any
> negative end, and by definition there is no such thing as multiple
> ranges" as an option for advanced users and trigger the semantics
> when one negative end is written explicitly with ^A notation, but in
> applications like cherry-pick where it is convenient to work on
> multiple ranges, we may want to extend our worldview to allow A..B
> C..D (especially when these two are distinct ranges---imagine the
> case where B is an ancestor of C) to mean "we have two ranges, and
> we mean the union of the commits in these two ranges".

Ooh, interesting.

That could also be of use in rebase-like operations (i.e. it'd serve
as a way for the user to rebase while dropping the commits between B
and C).

One thing to be careful about is that I think we would also need
parent rewriting or some other hint that B was an ancestor of C for
this to correctly work.  Otherwise, this construction would just be
mistaken for separate branches that are both being replayed.  That may
even matter for the cherry-pick case, since cherry-picking merge
commits is part of the intended scope, preventing us from just
assuming a fully linear range of commits to handle.

> We'd need to design carefully what should happen for trickier cases
> like A..B C (does it mean the traditional "a single range of commits
> that are reachable from either B or C, but never reachable from A",
> or does it mean "the union of commits A..B that are reachable from B
> but not from A and commits C that are reachable from C all the way
> down to root"?), though.

Yes, and in things like "^D A..B C", we'd have to not only worry about
whether A limits C, but also whether ^D limits A..B.

Thanks for the interesting food for thought.

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH 11/14] replay: use standard revision ranges
  2023-04-18  5:54           ` Elijah Newren
@ 2023-04-18 13:10             ` Derrick Stolee
  2023-04-20  4:53               ` Elijah Newren
  0 siblings, 1 reply; 208+ messages in thread
From: Derrick Stolee @ 2023-04-18 13:10 UTC (permalink / raw)
  To: Elijah Newren
  Cc: Christian Couder, git, Junio C Hamano, Patrick Steinhardt,
	Johannes Schindelin, John Cai, Christian Couder

On 4/18/2023 1:54 AM, Elijah Newren wrote:
> On Mon, Apr 17, 2023 at 7:05 AM Derrick Stolee <derrickstolee@github.com> wrote:
>>
>> On 4/15/2023 3:07 PM, Elijah Newren wrote:

>>> In short, I don't want another limited rebase; I want a more generic tool.
>>
>> The primary value of git-replay (to me) is that we can do "simple" rebases
>> without a worktree or user interaction. To me, simple rebases mean apply a
>> linear sequence of commits from a single branch onto another branch (and
>> specifying an "old base" is simple enough to include here). It also means
>> that we abort completely if there is a conflict (and handle conflict
>> resolution in later changes).
>>
>> The sooner we can deliver that, the better we can deliver on its potential
>> (and expand its functionality to be more generic).
> 
> Limiting the initial scope of this tool to a smaller set of features
> is okay (and, in fact, Christian has already done that), but we really
> need to avoid painting ourselves into a corner if we change the design
> as part of that limiting.  As I stated previously[1], I'm worried this
> is happening.
> 
> [1] https://lore.kernel.org/git/CABPp-BH7ZPBtV5Hu_-_yVdqVmiydW7_s_LYAtwbPuYSbRQiuQw@mail.gmail.com/,
> under "from my view"
> 
>> And this is generally my preference, but we shouldn't get functionality
>> "by accident"
> 
> All of the specific flags and cases you brought up so far were ones I
> already carefully considered over a year ago, so the "by accident"
> comment seems a little unfair.
> 
>> but instead do it with full intention
> 
> The intention is listed in the subject of the commit message of this
> patch.  I've also explicitly stated my desire on this list to make a
> tool which replays based on a general range expression multiple
> times[2][3][4][5].  And there are tests for general range expressions
> in this series being reviewed.  I don't understand why you might think
> I didn't intend to use general range expressions.

It's one thing to describe commit ranges, and another to include every
possible rev-list option.

> If you want to move git-replay away from setup_revisions(), at a
> minimum I think we need a proposal that can be extended to the cases I
> highlighted previously:
>     * allow specifying several negative revisions (or maybe even zero of them)
>     * allow specifying several positive revisions
>     * allow standard basic range syntax, i.e. A..B

I think supporting these descriptive ranges is nice, but doesn't _need_
to be in v1 of the tool. If we need to bake them into the CLI from v1
in order to ensure compatibility, then I understand that.

>     * allow --first-parent
>     * allow --ancestry-path[=<commit>]
> I think it should also be able to eventually support
>     * --branches[=<pattern>]
>     * --not
>     * --tags[=<pattern>]
>     * --remotes[=<pattern>]
>     * --glob=<pattern>
>     * --exclude=<glob-pattern>
>     * --all

However, I think very few of these should be generally supported, and
if there are reasons to include some then they should be motivated by
a specific use case and tested directly.

As it is, these tags do something in this v1, but we can't really be
sure that they work because we're not testing them. That is my
primary complaint. And testing each individually isn't enough,
because they can combine in all sorts of ways.

> That's a long list of things to parse, which setup_revisions() happens
> to handle nicely.  You are right that setup_revisions() also has lots
> of options for generating non-contiguous history that may make little
> or no sense for replay (and makes no sense for fast-export either).
> So, I am willing to consider other solutions if anyone has one, but
> only alternative solutions which can eventually handle the above
> requirements.  In particular, "three commits as positional arguments"
> (with one implicitly being considered a negative ref) conflicts with
> the first three bullet points above.

The only way I can see using setup_revisions() safely is if you make
sure to reject any arguments that start with "--" (or perhaps "-"
because some of those options may have single-character versions).
This could be extended to an allowlist if we find a motivation to
include other options (I see "--not" as being a likely candidate)
in patches that test that interaction.

Or, could we extract the portion of setup_revisions() that parses
just the revision ranges in to a new setup_revision_ranges() method?
It could reject any options that are not directly about revision
ranges. This option makes less sense if we are going the allowlist
approach.

Thanks,
-Stolee

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH 11/14] replay: use standard revision ranges
  2023-04-18 13:10             ` Derrick Stolee
@ 2023-04-20  4:53               ` Elijah Newren
  2023-04-20 13:44                 ` Derrick Stolee
  0 siblings, 1 reply; 208+ messages in thread
From: Elijah Newren @ 2023-04-20  4:53 UTC (permalink / raw)
  To: Derrick Stolee
  Cc: Christian Couder, git, Junio C Hamano, Patrick Steinhardt,
	Johannes Schindelin, John Cai, Christian Couder

On Tue, Apr 18, 2023 at 6:10 AM Derrick Stolee <derrickstolee@github.com> wrote:
>
> On 4/18/2023 1:54 AM, Elijah Newren wrote:
> > On Mon, Apr 17, 2023 at 7:05 AM Derrick Stolee <derrickstolee@github.com> wrote:
> >>
> >> On 4/15/2023 3:07 PM, Elijah Newren wrote:
>
> >>> In short, I don't want another limited rebase; I want a more generic tool.
> >>
> >> The primary value of git-replay (to me) is that we can do "simple" rebases
> >> without a worktree or user interaction. To me, simple rebases mean apply a
> >> linear sequence of commits from a single branch onto another branch (and
> >> specifying an "old base" is simple enough to include here). It also means
> >> that we abort completely if there is a conflict (and handle conflict
> >> resolution in later changes).
> >>
> >> The sooner we can deliver that, the better we can deliver on its potential
> >> (and expand its functionality to be more generic).
> >
> > Limiting the initial scope of this tool to a smaller set of features
> > is okay (and, in fact, Christian has already done that), but we really
> > need to avoid painting ourselves into a corner if we change the design
> > as part of that limiting.  As I stated previously[1], I'm worried this
> > is happening.
> >
> > [1] https://lore.kernel.org/git/CABPp-BH7ZPBtV5Hu_-_yVdqVmiydW7_s_LYAtwbPuYSbRQiuQw@mail.gmail.com/,
> > under "from my view"
> >
> >> And this is generally my preference, but we shouldn't get functionality
> >> "by accident"
> >
> > All of the specific flags and cases you brought up so far were ones I
> > already carefully considered over a year ago, so the "by accident"
> > comment seems a little unfair.
> >
> >> but instead do it with full intention
> >
> > The intention is listed in the subject of the commit message of this
> > patch.  I've also explicitly stated my desire on this list to make a
> > tool which replays based on a general range expression multiple
> > times[2][3][4][5].  And there are tests for general range expressions
> > in this series being reviewed.  I don't understand why you might think
> > I didn't intend to use general range expressions.
>
> It's one thing to describe commit ranges, and another to include every
> possible rev-list option.

Yes, agreed.  So clearly there is room for some middle ground.  :-)

> > If you want to move git-replay away from setup_revisions(), at a
> > minimum I think we need a proposal that can be extended to the cases I
> > highlighted previously:
> >     * allow specifying several negative revisions (or maybe even zero of them)
> >     * allow specifying several positive revisions
> >     * allow standard basic range syntax, i.e. A..B
>
> I think supporting these descriptive ranges is nice, but doesn't _need_
> to be in v1 of the tool. If we need to bake them into the CLI from v1
> in order to ensure compatibility, then I understand that.

Wahoo, we're moving towards middle ground.

I personally think baking them into the CLI in v1 is the easiest way
to ensure compatibility.  They're part of the long term goal anyway,
and they are already implemented and tested.  (Well, at least the 2nd
and 3rd items.  We also have tests with a negative revision, but could
add ones with more than one.  Having zero negative revisions means
replaying a root commit onto something else, which the code does not
yet support.)

> >     * allow --first-parent
> >     * allow --ancestry-path[=<commit>]
> > I think it should also be able to eventually support
> >     * --branches[=<pattern>]
> >     * --not
> >     * --tags[=<pattern>]
> >     * --remotes[=<pattern>]
> >     * --glob=<pattern>
> >     * --exclude=<glob-pattern>
> >     * --all
>
> However, I think very few of these should be generally supported, and
> if there are reasons to include some then they should be motivated by
> a specific use case and tested directly.

None of these need to be in v1.  Some aren't even useful yet without
other changes that Christian excluded in this initial version.

But can I take a step back and ask if you are saying few of these
should be generally supported _in v1_ (which I'm fine with) or _ever_
(which puts us at total loggerheads)?  It reads to me like you're
saying the latter, but I can't fathom why.  While I totally understand
the apprehension with "every possible rev-list option", this is far
from that extreme and I don't see why flags selecting contiguous
revision ranges should be of any concern.  They all have usecases, and
I've even pointed out multiple already.  Going slightly out of order:

* --ancestry-path=<commit>:

I don't see why this option is getting pushback at all.  It was
invented specifically and almost exclusively for use in git-replay.
I've pointed that out already and linked to the detailed usecase
explanation for using this option in git-replay twice so far in this
thread, but there's another interesting twist that may be relevant
here: to my knowledge, there are still no known usecases for this
option outside of git-replay; Junio and I at least could not imagine
any others[6].

Granted, this option is not of much use until we also support
replaying of merges, so leaving it out of v1 is no big deal.

[6] See the quoted comment of Junio including "solution in search of a
problem", found right before I give my usecase in
https://lore.kernel.org/git/CABPp-BHmj+QCBFDrH77iNfEU41V=UDu7nhBYkAbCsbXhshJzzw@mail.gmail.com/

* --first-parent:

This option was already referenced with a usecase in the link "[5]"
from my last email.  I also listed two other usecases in my
replay-design-notes file[7].

Granted, this option isn't yet useful since Christian has removed the
pick_merge_commit() function for v1.  But considering that it was part
of my original design, I've listed multiple usecases, and I have
already written code for git-replay specifically designed to be used
with both --ancestry-path[=<commit>] and --first-parent (just not
included in this v1), it is also one where I'm struggling to
understand pushback for eventual support.

[7] https://github.com/newren/git/blob/2a621020863c0b867293e020fec0954b43818789/replay-design-notes.txt#L113-L124

* all the others (which can be summarized as shorthands options for
various sets of multiple positive refs or multiple negative refs):

I think that supporting multiple positive and negative revisions
likely leads to users wanting these shorthand versions.  For example,
say a user has a dozen different branches in their repository
(possibly intertwined), all based on various topics in main and next.
They go on vacation (or work on some other project) for a few weeks,
during which time everything in next has graduated to main (and next
has not been rewound), and the user wants to replay all of their
branches on an updated main.  They could do so via:

   $ git fetch origin
   $ git checkout --detach HEAD   # Detach HEAD so it doesn't prevent
any ref updates
   $ git replay --onto origin/main --branches --not --remotes=origin |
git update-ref --stdin

That's something I would like to have available for my use.  (And,
again, N separate rebases/replays does not achieve the same effect
since the commits in those branches being replayed may be partially or
fully contained in some of the other branches being replayed.)

> As it is, these tags do something in this v1, but we can't really be
> sure that they work because we're not testing them. That is my
> primary complaint.

Lack of testing for some of these options is a totally fair complaint.

I'm fine with these options being left out of v1, so long as there
isn't a design change that will impose backward compatibility
constraints that precludes them from being added in the future.  That
is my primary complaint.

> And testing each individually isn't enough, because they can combine in all sorts of ways.

That's kind of the point, though, right?  Letting people select a
range of (contiguous) revisions using capabilities they are familiar
with and which they have used elsewhere in Git (and which have been
tested elsewhere in Git).

> > That's a long list of things to parse, which setup_revisions() happens
> > to handle nicely.  You are right that setup_revisions() also has lots
> > of options for generating non-contiguous history that may make little
> > or no sense for replay (and makes no sense for fast-export either).
> > So, I am willing to consider other solutions if anyone has one, but
> > only alternative solutions which can eventually handle the above
> > requirements.  In particular, "three commits as positional arguments"
> > (with one implicitly being considered a negative ref) conflicts with
> > the first three bullet points above.
>
> The only way I can see using setup_revisions() safely is if you make
> sure to reject any arguments that start with "--" (or perhaps "-"
> because some of those options may have single-character versions).

Ooh, single dash.  I wonder if people might find doing an interactive
replay with e.g. `--keep-base -5 ${OTHER_BRANCH}` handy.

Granted, that's basically the same as
`${OTHER_BRANCH}~5..${OTHER_BRANCH}` in most cases, and I'm not sure
if I'd personally use it, but it might be a nice touch to allow folks who
have long branch names to get by with less typing.

Anyway, I could see employing this reject-dashed-arguments scheme for
v1, since v1 doesn't need any of the dashed options, but only if
accompanied with a code comment that it's a hack not intended to be
kept in the future.

> This could be extended to an allowlist if we find a motivation to
> include other options (I see "--not" as being a likely candidate)
> in patches that test that interaction.

"if we find a motivation"??

I find this is a bit frustrating.  Several things in the merge-ort
machinery were designed with these kinds of capabilities in mind.
git-merge-tree, on my end, was designed, written, submitted, iterated,
and improved (taking 9+ months) almost solely as a feedback gathering
mechanism for this tool (it has much simpler design and conflict
handling needed).  I've been working on the usecases for this tool for
years, and have put quite a bit of time into this tool already, with
specific usecases in mind driving how the tool is being written.  And
I'm specifically motivated by things that rebase cannot do.

I've linked to several of those usecases multiple times in this thread
already.  Many were also in the cover letter in this series.  (And
more are in my replay-design-notes.txt file on my replay branch.)

I feel like you're brushing aside those repeated attempts to point out
those usecases as though they don't exist or don't matter, with some
separate new motivation needed for these options to be allowed?  And
perhaps saying those usecases should all be *ruled out* apriori,
because you're concerned the implementation *might* also allow other
unintended uses?

Something isn't working here.

> Or, could we extract the portion of setup_revisions() that parses
> just the revision ranges in to a new setup_revision_ranges() method?
> It could reject any options that are not directly about revision
> ranges. This option makes less sense if we are going the allowlist
> approach.

Ooh, that's interesting.  However, would the name lead people to think
that e.g. --reflog, --walk-reflogs, --author, --committer, --grep,
--min-parents, --max-parents, --cherry-pick are relevant?  Should we
perhaps use a name like setup_contiguous_revision_ranges() so it's
clear these flags are disallowed, while things like A, ^B, C..D,
--first-parent, --ancestry-path, --branches, --remotes, are allowed?

Is it worth refactoring setup_revision() into more divisible chunks,
so that callers can make use of just the relevant parts of it?  We
have numerous other tools looking for revision ranges, which currently
accept full rev-list options and pass along user-supplied command-line
arguments to them.  Beyond rev-list/log/shortlog, these seem to
include at least:
  * fast-export
  * blame
  * cherry-pick
  * revert
  * format-patch
  * bisect (at least the skip subcommand)
  * stash (at least the show subcommand)
  * filter-branch (actually, as a horrible shell script, it'll
continue accepting all rev-list options anyway)

Some of these could probably get away with non-contiguous revision
ranges, but most wouldn't find any benefit from:
  * tree/blob/packing options (e.g. --objects*, --unpacked, --filter*,
--verify-objects, --in-commit-order)
  * anything dealing with reflogs (e.g. --reflog, --walk-reflogs)
  * various forms of history simplification (e.g. --simplify-merges,
--simplify-by-decoration, --sparse, --show-pulls)
  * flags related to display (e.g. --pretty, --graph,
--show-signature, --early-output, --disk-usage, --abbrev-commit,
--relative-date, --log-size, --left-right, --cherry-mark)
  * flags related to file contents (e.g. <pathspecs>, --merge,
--follow, --full-diff, --remove-empty)

Here's a fun and valid (and innocuous) command.  Guess which flags are
ignored and which aren't:

  $ git stash show --graph --relative-date --min-parents=3
--simplify-merges --cherry --show-pulls --unpacked -v -t -8
--format=oneline --abbrev=12 --pretty=fuller --show-notes
--encode-email-headers --always --branches --indexed-objects stash@{0}


That all said, while I like the idea of someone dividing
setup_revisions() into divisible chunks so tools can just use the bits
that are relevant, and doing so sounds like it'd avoid weird surprises
from use of unintended flags, I'm surprised that the solution to the
"setup_revisions() is scary" problem isn't to provide easy-to-use API
that allows selecting the relevant subsets for each command, but to
instead expect every one that comes along to do some heavy lifting and
special whitelisting.  We have half a dozen users in the tree already
facing these problems, so shouldn't these be fixed and the API made to
be easy to use more safely so that this problem is solved more
generally rather than just putting it on each future functionality
implementor who comes along to work around it in their particular
situation?

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH 11/14] replay: use standard revision ranges
  2023-04-20  4:53               ` Elijah Newren
@ 2023-04-20 13:44                 ` Derrick Stolee
  2023-04-23  1:18                   ` Elijah Newren
  0 siblings, 1 reply; 208+ messages in thread
From: Derrick Stolee @ 2023-04-20 13:44 UTC (permalink / raw)
  To: Elijah Newren
  Cc: Christian Couder, git, Junio C Hamano, Patrick Steinhardt,
	Johannes Schindelin, John Cai, Christian Couder

On 4/20/2023 12:53 AM, Elijah Newren wrote:
> On Tue, Apr 18, 2023 at 6:10 AM Derrick Stolee <derrickstolee@github.com> wrote:
>>
>> On 4/18/2023 1:54 AM, Elijah Newren wrote:
>>> The intention is listed in the subject of the commit message of this
>>> patch.  I've also explicitly stated my desire on this list to make a
>>> tool which replays based on a general range expression multiple
>>> times[2][3][4][5].  And there are tests for general range expressions
>>> in this series being reviewed.  I don't understand why you might think
>>> I didn't intend to use general range expressions.
>>
>> It's one thing to describe commit ranges, and another to include every
>> possible rev-list option.
> 
> Yes, agreed.  So clearly there is room for some middle ground.  :-)
> 
>>> If you want to move git-replay away from setup_revisions(), at a
>>> minimum I think we need a proposal that can be extended to the cases I
>>> highlighted previously:
>>>     * allow specifying several negative revisions (or maybe even zero of them)
>>>     * allow specifying several positive revisions
>>>     * allow standard basic range syntax, i.e. A..B
>>
>> I think supporting these descriptive ranges is nice, but doesn't _need_
>> to be in v1 of the tool. If we need to bake them into the CLI from v1
>> in order to ensure compatibility, then I understand that.
> 
> Wahoo, we're moving towards middle ground.
> 
> I personally think baking them into the CLI in v1 is the easiest way
> to ensure compatibility.  They're part of the long term goal anyway,
> and they are already implemented and tested.  (Well, at least the 2nd
> and 3rd items.  We also have tests with a negative revision, but could
> add ones with more than one.  Having zero negative revisions means
> replaying a root commit onto something else, which the code does not
> yet support.)
> 
>>>     * allow --first-parent
>>>     * allow --ancestry-path[=<commit>]
>>> I think it should also be able to eventually support
>>>     * --branches[=<pattern>]
>>>     * --not
>>>     * --tags[=<pattern>]
>>>     * --remotes[=<pattern>]
>>>     * --glob=<pattern>
>>>     * --exclude=<glob-pattern>
>>>     * --all
>>
>> However, I think very few of these should be generally supported, and
>> if there are reasons to include some then they should be motivated by
>> a specific use case and tested directly.
> 
> None of these need to be in v1.  Some aren't even useful yet without
> other changes that Christian excluded in this initial version.
> 
> But can I take a step back and ask if you are saying few of these
> should be generally supported _in v1_ (which I'm fine with) or _ever_
> (which puts us at total loggerheads)?  It reads to me like you're
> saying the latter, but I can't fathom why.  While I totally understand
> the apprehension with "every possible rev-list option", this is far
> from that extreme and I don't see why flags selecting contiguous
> revision ranges should be of any concern.  They all have usecases, and
> I've even pointed out multiple already.  Going slightly out of order:

(going more out of order)

> * --first-parent:
> 
> This option was already referenced with a usecase in the link "[5]"
> from my last email.  I also listed two other usecases in my
> replay-design-notes file[7].

I agree on this one.

> * --ancestry-path=<commit>:
> 
> I don't see why this option is getting pushback at all.  It was
> invented specifically and almost exclusively for use in git-replay.

(Edited out your explanation.)

I'm still not super convinced that this solves a common user problem,
but you've documented your use case well (in other places). The
problem I see is that the current patch brings it in without having
any of that context.

> * all the others (which can be summarized as shorthands options for
> various sets of multiple positive refs or multiple negative refs):

I think you're over-simplifying here, because...

>> Or, could we extract the portion of setup_revisions() that parses
>> just the revision ranges in to a new setup_revision_ranges() method?
>> It could reject any options that are not directly about revision
>> ranges. This option makes less sense if we are going the allowlist
>> approach.
> 
> However, would the name lead people to think
> that e.g. --reflog, --walk-reflogs, --author, --committer, --grep,
> --min-parents, --max-parents, --cherry-pick are relevant?  Should we
> perhaps use a name like setup_contiguous_revision_ranges() so it's
> clear these flags are disallowed, while things like A, ^B, C..D,
> --first-parent, --ancestry-path, --branches, --remotes, are allowed?

I think one thing that might help bridge the divide here is a
different split when I think of "revision range" and "rev-list options".

We have several categories of rev-list options, and we need to find
the right set to care about and the ones that aren't useful for git-replay:

 1. (What I call revision ranges) a collection of starting revisions,
    each marked as an "include" or "exclude" (A..B includes B,
    excludes A). The --not option helps with defining these starting
    points.

 2. (Walk options) Modifications to how we walk commits, such as
    --first-parent, --ancestry-path. These are so far the kind of
    options you have motivated with use cases.

 3. (Ordering options) Modifications to how those commits are ordered,
    such as --topo-order, --date-order, and --reverse. These seem to
    be overridden by git-replay (although, --reverse probably causes
    some confusion right now).

 4. (Filtering options) A post-walk filter on a per-commit basis.
    This includes --(max|min)-parents, --author, --grep.

note: at this point I'm not sure into which of these categories we
should put time-based options like --since.

My main objection to the generic rev-list options come from allowing
categories (3) and (4), since this is more likely to cause user
confusion rather than actually be of any use to the feature.

While I was not considering (2) to be included in setup_revision_ranges(),
I could see it being valid to include both (1) and (2) in those options.

I would like to see tests for options in category (2) to demonstrate
these use cases and solidify them in our supported scenarios.

> "if we find a motivation"??
> 
> I find this is a bit frustrating.  Several things in the merge-ort
> machinery were designed with these kinds of capabilities in mind.
> git-merge-tree, on my end, was designed, written, submitted, iterated,
> and improved (taking 9+ months) almost solely as a feedback gathering
> mechanism for this tool (it has much simpler design and conflict
> handling needed).  I've been working on the usecases for this tool for
> years, and have put quite a bit of time into this tool already, with
> specific usecases in mind driving how the tool is being written.  And
> I'm specifically motivated by things that rebase cannot do.
> 
> I've linked to several of those usecases multiple times in this thread
> already.  Many were also in the cover letter in this series.  (And
> more are in my replay-design-notes.txt file on my replay branch.)
> 
> I feel like you're brushing aside those repeated attempts to point out
> those usecases as though they don't exist or don't matter, with some
> separate new motivation needed for these options to be allowed?  And
> perhaps saying those usecases should all be *ruled out* apriori,
> because you're concerned the implementation *might* also allow other
> unintended uses?
> 
> Something isn't working here.

I'm trying to read the patches and make sense of what is written there.

The current patch especially is far too lightly documented for what
it is actually implementing.

Even its documentation states this:

+<revision-range>::
+	Range of commits to replay; see "Specifying Ranges" in
+	linkgit:git-rev-parse.

This "Specifying Ranges" section describes exactly category (1) of
what I was talking about, but really the patch enables everything
in "Commit Limiting" from git-rev-list.

Based on what I see in the patch, I can't help but think that the
extra options are an accident.

And _even with the extra context linked elsewhere_ I will still hold
that using something as generic as setup_revisions() isn't a good
practice for software development. It adds too may things that all
at once, some of which I don't think match the purpose of git-replay.
You've convinced me to expand my understanding of what fits in that
category, but I still think we need to test this more. Tests can
demonstrate use cases much better than anything else.

> Is it worth refactoring setup_revision() into more divisible chunks,
> so that callers can make use of just the relevant parts of it?  We
> have numerous other tools looking for revision ranges, which currently
> accept full rev-list options and pass along user-supplied command-line
> arguments to them.  Beyond rev-list/log/shortlog, these seem to
> include at least:
>   * fast-export
>   * blame
>   * cherry-pick
>   * revert
>   * format-patch
>   * bisect (at least the skip subcommand)
>   * stash (at least the show subcommand)
>   * filter-branch (actually, as a horrible shell script, it'll
> continue accepting all rev-list options anyway)
> 
> Some of these could probably get away with non-contiguous revision
> ranges, but most wouldn't find any benefit from:
>   * tree/blob/packing options (e.g. --objects*, --unpacked, --filter*,
> --verify-objects, --in-commit-order)
>   * anything dealing with reflogs (e.g. --reflog, --walk-reflogs)
>   * various forms of history simplification (e.g. --simplify-merges,
> --simplify-by-decoration, --sparse, --show-pulls)
>   * flags related to display (e.g. --pretty, --graph,
> --show-signature, --early-output, --disk-usage, --abbrev-commit,
> --relative-date, --log-size, --left-right, --cherry-mark)
>   * flags related to file contents (e.g. <pathspecs>, --merge,
> --follow, --full-diff, --remove-empty)
> 
> Here's a fun and valid (and innocuous) command.  Guess which flags are
> ignored and which aren't:
> 
>   $ git stash show --graph --relative-date --min-parents=3
> --simplify-merges --cherry --show-pulls --unpacked -v -t -8
> --format=oneline --abbrev=12 --pretty=fuller --show-notes
> --encode-email-headers --always --branches --indexed-objects stash@{0}

This investigation is exactly why I'm concerned about using the
generic setup_revisions(). I've already noticed its use elsewhere
and been disappointed. But I wasn't around when those were written,
so you get the short straw and become the reason I bring it up.

> That all said, while I like the idea of someone dividing
> setup_revisions() into divisible chunks so tools can just use the bits
> that are relevant, and doing so sounds like it'd avoid weird surprises
> from use of unintended flags, I'm surprised that the solution to the
> "setup_revisions() is scary" problem isn't to provide easy-to-use API
> that allows selecting the relevant subsets for each command, but to
> instead expect every one that comes along to do some heavy lifting and
> special whitelisting.  We have half a dozen users in the tree already
> facing these problems, so shouldn't these be fixed and the API made to
> be easy to use more safely so that this problem is solved more
> generally rather than just putting it on each future functionality
> implementor who comes along to work around it in their particular
> situation?

I think a better API is a great idea! Splitting into multiple methods
or providing option flags for "categories" of options allowed would
also work.

But back to my original suggestion: you can also do something simpler
for v1 of git-replay (say, very limited revision parsing such as one A..B
range) so your progress here isn't blocked on refactoring the revisions
API.

Thanks,
-Stolee

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH 11/14] replay: use standard revision ranges
  2023-04-20 13:44                 ` Derrick Stolee
@ 2023-04-23  1:18                   ` Elijah Newren
  2023-04-24 15:23                     ` Derrick Stolee
  0 siblings, 1 reply; 208+ messages in thread
From: Elijah Newren @ 2023-04-23  1:18 UTC (permalink / raw)
  To: Derrick Stolee
  Cc: Christian Couder, git, Junio C Hamano, Patrick Steinhardt,
	Johannes Schindelin, John Cai, Christian Couder

On Thu, Apr 20, 2023 at 6:44 AM Derrick Stolee <derrickstolee@github.com> wrote:
>
> On 4/20/2023 12:53 AM, Elijah Newren wrote:
> > On Tue, Apr 18, 2023 at 6:10 AM Derrick Stolee <derrickstolee@github.com> wrote:
> >>
> >> On 4/18/2023 1:54 AM, Elijah Newren wrote:
[...]
> >>>     * allow --first-parent
> >>>     * allow --ancestry-path[=<commit>]
> >>> I think it should also be able to eventually support
> >>>     * --branches[=<pattern>]
> >>>     * --not
> >>>     * --tags[=<pattern>]
> >>>     * --remotes[=<pattern>]
> >>>     * --glob=<pattern>
> >>>     * --exclude=<glob-pattern>
> >>>     * --all
> >>
> >> However, I think very few of these should be generally supported, and
> >> if there are reasons to include some then they should be motivated by
> >> a specific use case and tested directly.
> >
> > None of these need to be in v1.  Some aren't even useful yet without
> > other changes that Christian excluded in this initial version.
> >
> > But can I take a step back and ask if you are saying few of these
> > should be generally supported _in v1_ (which I'm fine with) or _ever_
> > (which puts us at total loggerheads)?  It reads to me like you're
> > saying the latter, but I can't fathom why.  While I totally understand
> > the apprehension with "every possible rev-list option", this is far
> > from that extreme and I don't see why flags selecting contiguous
> > revision ranges should be of any concern.  They all have usecases, and
> > I've even pointed out multiple already.  Going slightly out of order:
>
> (going more out of order)
>
> > * --first-parent:
> >
> > This option was already referenced with a usecase in the link "[5]"
> > from my last email.  I also listed two other usecases in my
> > replay-design-notes file[7].
>
> I agree on this one.

:-)

> > * --ancestry-path=<commit>:
> >
> > I don't see why this option is getting pushback at all.  It was
> > invented specifically and almost exclusively for use in git-replay.
>
> (Edited out your explanation.)
>
> I'm still not super convinced that this solves a common user problem,

Why does it have to be common?  I said I wanted to be able to do
things that I cannot do with existing tooling, which usually implies
less common usecases.


> > * all the others (which can be summarized as shorthand options for
> > various sets of multiple positive refs or multiple negative refs):
>
> I think you're over-simplifying here, because...

I don't think so.  Was the context lost?  I suspect it was, because
right after this you started talking about completely unrelated
options (though in a very useful manner, so it's all good in the end.)

Since I was responding to "few of these should be generally
supported", and the first two of those had been discussed above, "all
the others" here refers to exactly these 7 flags:
    * --branches[=<pattern>]
    * --not
    * --tags[=<pattern>]
    * --remotes[=<pattern>]
    * --glob=<pattern>
    * --exclude=<glob-pattern>
    * --all

All I did was read our own documentation for these options and
categorize exactly what they share in common.  That yields my
statement that these "can be summarized as shorthand options for
various sets of multiple positive refs or multiple negative refs".

> >> Or, could we extract the portion of setup_revisions() that parses
> >> just the revision ranges in to a new setup_revision_ranges() method?
> >> It could reject any options that are not directly about revision
> >> ranges. This option makes less sense if we are going the allowlist
> >> approach.
> >
> > However, would the name lead people to think
> > that e.g. --reflog, --walk-reflogs, --author, --committer, --grep,
> > --min-parents, --max-parents, --cherry-pick are relevant?  Should we
> > perhaps use a name like setup_contiguous_revision_ranges() so it's
> > clear these flags are disallowed, while things like A, ^B, C..D,
> > --first-parent, --ancestry-path, --branches, --remotes, are allowed?
>
> I think one thing that might help bridge the divide here is a
> different split when I think of "revision range" and "rev-list options".
>
> We have several categories of rev-list options, and we need to find
> the right set to care about and the ones that aren't useful for git-replay:
>
>  1. (What I call revision ranges) a collection of starting revisions,
>     each marked as an "include" or "exclude" (A..B includes B,
>     excludes A). The --not option helps with defining these starting
>     points.

There are also shorthands for building up these collections of
starting revisions, so that instead of saying e.g. `maint main next
seen mytopic`, one can just use `--branches`.  Thus, options like
--branches[=<pattern>], --tags[=<pattern>], --exclude=<pattern>, etc.
also belong in this category.

>  2. (Walk options) Modifications to how we walk commits, such as
>     --first-parent, --ancestry-path. These are so far the kind of
>     options you have motivated with use cases.

I think I also motivated category 1 with use cases.  Since you
acquiesced on including that category first, I think you may have
meant this last sentence to refer to both of these categories?

>  3. (Ordering options) Modifications to how those commits are ordered,
>     such as --topo-order, --date-order, and --reverse. These seem to
>     be overridden by git-replay (although, --reverse probably causes
>     some confusion right now).

Yep, intentionally overridden.

Could you elaborate on what you mean by --reverse causing confusion?

>  4. (Filtering options) A post-walk filter on a per-commit basis.
>     This includes --(max|min)-parents, --author, --grep.

I think pathspecs are an additional example that is worth specifically
calling out as belonging in category 4.

This is a reasonable splitting of categories, if a bit incomplete.
(However, other categories I can think of matter less to git-replay
than these four.)

> note: at this point I'm not sure into which of these categories we
> should put time-based options like --since.

Fair enough, there were also some options I was unsure of.  And there
are some that look like a can of worms.  (Which I'd rather not get
into, but I'll obliquely note that I would place `--since` and
`--until` in separate categories.)

> My main objection to the generic rev-list options come from allowing
> categories (3) and (4), since this is more likely to cause user
> confusion rather than actually be of any use to the feature.

That is a reasonable objection; I wish what you had stated earlier
looked more like this.

I might disagree with you on the severity of the issue, or the
relative necessity/importance of cleaning up the widely-used existing
code as a pre-requisite to using it in another place, but it's a
totally fair review comment to bring up.  Further, I agree with you
that the options in these two categories would not be of any use to
git-replay.

> While I was not considering (2) to be included in setup_revision_ranges(),
> I could see it being valid to include both (1) and (2) in those options.

Awesome, I think this provides me with what I need.  Note that
categories (1) and (2) include every single option in my previous "at
a minimum" list:

    * several negative revisions (or maybe even zero of them)
    * several positive revisions
    * standard basic range syntax, i.e. A..B
    * --first-parent
    * --ancestry-path[=<commit>]
    * --branches[=<pattern>]
    * --not
    * --tags[=<pattern>]
    * --remotes[=<pattern>]
    * --glob=<pattern>
    * --exclude=<glob-pattern>
    * --all

There may also be additional options that fit within categories (1) and (2).

> I would like to see tests for options in category (2) to demonstrate
> these use cases and solidify them in our supported scenarios.

Yup, totally fair.  Probably not as part of this initial patchset,
though, since Christian and Dscho are only interested in a small
subset of usecases and are the ones pushing these patches early.
(Whereas I'm focused on answering questions, and making sure that
whatever happens doesn't make my goals for the tool impossible.)

> > "if we find a motivation"??
> >
> > I find this is a bit frustrating.  Several things in the merge-ort
> > machinery were designed with these kinds of capabilities in mind.
> > git-merge-tree, on my end, was designed, written, submitted, iterated,
> > and improved (taking 9+ months) almost solely as a feedback gathering
> > mechanism for this tool (it has much simpler design and conflict
> > handling needed).  I've been working on the usecases for this tool for
> > years, and have put quite a bit of time into this tool already, with
> > specific usecases in mind driving how the tool is being written.  And
> > I'm specifically motivated by things that rebase cannot do.
> >
> > I've linked to several of those usecases multiple times in this thread
> > already.  Many were also in the cover letter in this series.  (And
> > more are in my replay-design-notes.txt file on my replay branch.)
> >
> > I feel like you're brushing aside those repeated attempts to point out
> > those usecases as though they don't exist or don't matter, with some
> > separate new motivation needed for these options to be allowed?  And
> > perhaps saying those usecases should all be *ruled out* apriori,
> > because you're concerned the implementation *might* also allow other
> > unintended uses?
> >
> > Something isn't working here.
>
> I'm trying to read the patches and make sense of what is written there.
>
> The current patch especially is far too lightly documented for what
> it is actually implementing.
>
> Even its documentation states this:
>
> +<revision-range>::
> +       Range of commits to replay; see "Specifying Ranges" in
> +       linkgit:git-rev-parse.
>
> This "Specifying Ranges" section describes exactly category (1) of
> what I was talking about, but really the patch enables everything
> in "Commit Limiting" from git-rev-list.
>
> Based on what I see in the patch, I can't help but think that the
> extra options are an accident.

This all would be a fine review comment, if you had said that.
Instead, you stated that setup_revisions() shouldn't be used (which
might have been fine as an initial comment), and when I pointed out
exact options and flags that were needed at a minimum you still said
they shouldn't generally be supported, and after pointing out
usecases, multiple times, you responded with "if we can find a
motivation".

There are two reasons I found that problematic:

Sometimes it makes sense to say that a usecase shouldn't be supported.
But, when there's months of effort so far put into implementing those
usecases, I would expect strong statements about ruling out usecases
to be accompanied with really good explanations of why those exact
cases are deemed "bad" and the best possible alternative solutions
that should instead be implemented to solve something close to the
proposer's stated desires.  I didn't see that.

Similarly, there are times when a reviewer should say that code should
not be implemented a certain way.  But when the person responds with
some alternatives and their _minimum_ needs trying to find some common
ground, and has put months of efforts into this wider goal, I would
expect that ruling out certain methods and their follow up
alternatives to be accompanied with alternative solutions of your own
that can solve the proposer's goals; not merely attempting to veto
suggestions immediately with seemingly no path forward.

It was an unfortunate set of examples that seemed far below your
typically excellent reviews.  In contrast, this latest email of yours
is another high quality response in line with your emails prior to this thread.

> And _even with the extra context linked elsewhere_ I will still hold
> that using something as generic as setup_revisions() isn't a good
> practice for software development. It adds too may things that all
> at once, some of which I don't think match the purpose of git-replay.
> You've convinced me to expand my understanding of what fits in that
> category, but I still think we need to test this more. Tests can
> demonstrate use cases much better than anything else.

Yaay on the convincing bit.  :-)

The rest is all fair, but I'd like to point out that there are a few
problems here:

  * Tests should generally be passing before submitting upstream, so
all the code to implement them needs to be sent too
  * Submitted patch series have to be digestible sizes; not everything
can be submitted at once
  * Christian and Dscho wanted some of what I had implemented despite
other parts not being ready

Importantly, the first two issues in particular mean that when the
first series comes:

  * If you insist on (or even just suggest) certain changes that
happen to break capabilities in the pipeline, especially in a fashion
that cannot backward-compatibly be fixed, then I think the only path
forward (other than dropping your suggestions) is to engage in
discussions about those usecases & design *without* the testcases and
code being available yet.

> > Is it worth refactoring setup_revision() into more divisible chunks,
> > so that callers can make use of just the relevant parts of it?  We
> > have numerous other tools looking for revision ranges, which currently
> > accept full rev-list options and pass along user-supplied command-line
> > arguments to them.  Beyond rev-list/log/shortlog, these seem to
> > include at least:
> >   * fast-export
> >   * blame
> >   * cherry-pick
> >   * revert
> >   * format-patch
> >   * bisect (at least the skip subcommand)
> >   * stash (at least the show subcommand)
> >   * filter-branch (actually, as a horrible shell script, it'll
> > continue accepting all rev-list options anyway)
> >
> > Some of these could probably get away with non-contiguous revision
> > ranges, but most wouldn't find any benefit from:
> >   * tree/blob/packing options (e.g. --objects*, --unpacked, --filter*,
> > --verify-objects, --in-commit-order)
> >   * anything dealing with reflogs (e.g. --reflog, --walk-reflogs)
> >   * various forms of history simplification (e.g. --simplify-merges,
> > --simplify-by-decoration, --sparse, --show-pulls)
> >   * flags related to display (e.g. --pretty, --graph,
> > --show-signature, --early-output, --disk-usage, --abbrev-commit,
> > --relative-date, --log-size, --left-right, --cherry-mark)
> >   * flags related to file contents (e.g. <pathspecs>, --merge,
> > --follow, --full-diff, --remove-empty)
> >
> > Here's a fun and valid (and innocuous) command.  Guess which flags are
> > ignored and which aren't:
> >
> >   $ git stash show --graph --relative-date --min-parents=3
> > --simplify-merges --cherry --show-pulls --unpacked -v -t -8
> > --format=oneline --abbrev=12 --pretty=fuller --show-notes
> > --encode-email-headers --always --branches --indexed-objects stash@{0}
>
> This investigation is exactly why I'm concerned about using the
> generic setup_revisions(). I've already noticed its use elsewhere
> and been disappointed. But I wasn't around when those were written,
> so you get the short straw and become the reason I bring it up.
>
> > That all said, while I like the idea of someone dividing
> > setup_revisions() into divisible chunks so tools can just use the bits
> > that are relevant, and doing so sounds like it'd avoid weird surprises
> > from use of unintended flags, I'm surprised that the solution to the
> > "setup_revisions() is scary" problem isn't to provide easy-to-use API
> > that allows selecting the relevant subsets for each command, but to
> > instead expect every one that comes along to do some heavy lifting and
> > special whitelisting.  We have half a dozen users in the tree already
> > facing these problems, so shouldn't these be fixed and the API made to
> > be easy to use more safely so that this problem is solved more
> > generally rather than just putting it on each future functionality
> > implementor who comes along to work around it in their particular
> > situation?
>
> I think a better API is a great idea! Splitting into multiple methods
> or providing option flags for "categories" of options allowed would
> also work.
>
> But back to my original suggestion: you can also do something simpler
> for v1 of git-replay

That's workable.

> (say, very limited revision parsing such as one A..B range)

I'm not fine with that, though.  We already have tests of multiple
positive refs, so there's no reason to exclude those.  In fact,
keeping them in is especially important as a means of avoiding having
other reviewers make suggestions to copy rebase's broken design
(namely, being limited to a couple commits passed as positional
arguments with one implicitly being an exclude).

> so your progress here isn't blocked on refactoring the revisions
> API.

Is that a positive or a negative?  That question may surprise you, so
let me explain.

I have a conflict of interest of sorts.  When Christian and Dscho
(separately) approached me earlier this year and pointed out that
git-replay had the functionality they wanted, I was happy that others
liked it, and gave some pointers here and there, but I was _very_
worried that upstreaming it would result in something getting
backward-incompatibly locked into place that prevents the pieces that
are  still in progress from getting implemented.  And I was concerned
that my plans to create a tool people could experiment with, and that
we could get usability feedback on (as I suggested at the Git
contributors summit last year) might be in jeopardy as pieces of its
functionality get locked into place before it's even ready for
usability testing.  I was silently hoping they'd lose steam on
rebasing and cleaning up my patches or choose to just delay until I
could get real time to work on it with them.  (Since then, the one
person who had ever advocated for my Git work at $DAYJOB, the best
manager I had ever worked under, became not-a-manager.  I was
blindsided by that in February.  Also, I have been transferred to a
different team and am spinning up there.  And so, to my dismay, I'm
worried my little sliver of Git time at work may evaporate entirely
rather than return to previous healthier levels.)  Anyway, I've been
fretting about things getting "locked-in" for a few months.

They didn't lose steam, as I found out when these patches were
submitted.  But, uh, I'm silently torn because I want to help
Christian and Dscho get what they want, but having them be blocked on
progress would reduce my stress.  A lot.

Is there any chance people would be willing to accept a "NO BACKWARD
COMPATIBILITY GUARANTEES" disclaimer on this command for a (long?)
while, like Victoria suggested at Review club?  That might be an
alternate solution that would lower my worries.

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH 11/14] replay: use standard revision ranges
  2023-04-23  1:18                   ` Elijah Newren
@ 2023-04-24 15:23                     ` Derrick Stolee
  2023-04-30  6:45                       ` Elijah Newren
  0 siblings, 1 reply; 208+ messages in thread
From: Derrick Stolee @ 2023-04-24 15:23 UTC (permalink / raw)
  To: Elijah Newren
  Cc: Christian Couder, git, Junio C Hamano, Patrick Steinhardt,
	Johannes Schindelin, John Cai, Christian Couder

On 4/22/2023 9:18 PM, Elijah Newren wrote:
> On Thu, Apr 20, 2023 at 6:44 AM Derrick Stolee <derrickstolee@github.com> wrote:
>>
>> On 4/20/2023 12:53 AM, Elijah Newren wrote:
>>> On Tue, Apr 18, 2023 at 6:10 AM Derrick Stolee <derrickstolee@github.com> wrote:

>>  3. (Ordering options) Modifications to how those commits are ordered,
>>     such as --topo-order, --date-order, and --reverse. These seem to
>>     be overridden by git-replay (although, --reverse probably causes
>>     some confusion right now).
> 
> Yep, intentionally overridden.
> 
> Could you elaborate on what you mean by --reverse causing confusion?

It's very unlikely that a list of patches will successfully apply
when applied in the reverse order. If we allow the argument, then
someone will try it thinking they can flip their commits. Then they
might be surprised when there are a bunch of conflicts on every
commit.

Basically, I'm not super thrilled about exposing options that are
unlikely to be valuable to users and instead are more likely to cause
confusion due to changes that won't successfully apply.

>>> I feel like you're brushing aside those repeated attempts to point out
>>> those usecases as though they don't exist or don't matter, with some
>>> separate new motivation needed for these options to be allowed?  And
>>> perhaps saying those usecases should all be *ruled out* apriori,
>>> because you're concerned the implementation *might* also allow other
>>> unintended uses?
>>>
>>> Something isn't working here.
>>
>> I'm trying to read the patches and make sense of what is written there.
>>
>> The current patch especially is far too lightly documented for what
>> it is actually implementing.
>>
>> Even its documentation states this:
>>
>> +<revision-range>::
>> +       Range of commits to replay; see "Specifying Ranges" in
>> +       linkgit:git-rev-parse.
>>
>> This "Specifying Ranges" section describes exactly category (1) of
>> what I was talking about, but really the patch enables everything
>> in "Commit Limiting" from git-rev-list.
>>
>> Based on what I see in the patch, I can't help but think that the
>> extra options are an accident.
> 
> This all would be a fine review comment, if you had said that.
> Instead, you stated that setup_revisions() shouldn't be used (which
> might have been fine as an initial comment), and when I pointed out
> exact options and flags that were needed at a minimum you still said
> they shouldn't generally be supported, and after pointing out
> usecases, multiple times, you responded with "if we can find a
> motivation".
> 
> There are two reasons I found that problematic:
> 
> Sometimes it makes sense to say that a usecase shouldn't be supported.
> But, when there's months of effort so far put into implementing those
> usecases, I would expect strong statements about ruling out usecases
> to be accompanied with really good explanations of why those exact
> cases are deemed "bad" and the best possible alternative solutions
> that should instead be implemented to solve something close to the
> proposer's stated desires.  I didn't see that.
> 
> Similarly, there are times when a reviewer should say that code should
> not be implemented a certain way.  But when the person responds with
> some alternatives and their _minimum_ needs trying to find some common
> ground, and has put months of efforts into this wider goal, I would
> expect that ruling out certain methods and their follow up
> alternatives to be accompanied with alternative solutions of your own
> that can solve the proposer's goals; not merely attempting to veto
> suggestions immediately with seemingly no path forward.
> 
> It was an unfortunate set of examples that seemed far below your
> typically excellent reviews.  In contrast, this latest email of yours
> is another high quality response in line with your emails prior to this thread.

I'm sorry I didn't read the prior work linked in the cover letter.
Since cover letters and their external links are not recorded in our
commit history, I have a habit of ignoring them other than getting
some basic context of the series.

I also was less specific about my complaints because I thought that
saying "things like --author" was enough to point out that
setup_revisions() is too generic. Didn't seem like context that was
required until you showed it was required.

> The rest is all fair, but I'd like to point out that there are a few
> problems here:
> 
>   * Tests should generally be passing before submitting upstream, so
> all the code to implement them needs to be sent too
>   * Submitted patch series have to be digestible sizes; not everything
> can be submitted at once
>   * Christian and Dscho wanted some of what I had implemented despite
> other parts not being ready

Outside of my first response (using positional arguments, recommended
before you provided the extra context I was missing) I have not
suggested implementing something that can't be forward compatible. Note
that I've used A..B notation in those replies. I'm suggesting that we
focus on a critical core of functionality that can be carefully tested
while the rest of the system is being worked out. Then, those more
advanced bits can be added carefully with test cases.

When we get to that point, there are creative ways to prepare tests in
advance of a code change such that they make reviewable patches, instead
of creating test debt to be left for later.

> Importantly, the first two issues in particular mean that when the
> first series comes:
> 
>   * If you insist on (or even just suggest) certain changes that
> happen to break capabilities in the pipeline, especially in a fashion
> that cannot backward-compatibly be fixed, then I think the only path
> forward (other than dropping your suggestions) is to engage in
> discussions about those usecases & design *without* the testcases and
> code being available yet.
This sounds a lot like your arguments are focused on reducing the
amount of rework you'd need to do on your pipeline of work that hasn't
been submitted for review. I find that to be an expected risk of working
so far ahead of reviewed patches, and am familiar with that pain. I
don't find that to be a convincing argument.

>> But back to my original suggestion: you can also do something simpler
>> for v1 of git-replay
> 
> That's workable.
> 
>> (say, very limited revision parsing such as one A..B range)
> 
> I'm not fine with that, though.  We already have tests of multiple
> positive refs, so there's no reason to exclude those.  In fact,
> keeping them in is especially important as a means of avoiding having
> other reviewers make suggestions to copy rebase's broken design
> (namely, being limited to a couple commits passed as positional
> arguments with one implicitly being an exclude).

I'm just making one suggestion about where to limit the parsing. If
multiple ranges are doable before refactoring setup_revisions(), then
by all means go ahead. And whatever parsing is accepted in this v1,
we can make sure it works with those future plans.

Focusing on the revisions as described by 'git rev-parse' pointed to
in the 'git replay' docs in this patch would be a great place to start.
 
>> so your progress here isn't blocked on refactoring the revisions
>> API.
> 
> Is that a positive or a negative?  That question may surprise you, so
> let me explain.
> 
> I have a conflict of interest of sorts.  When Christian and Dscho
> (separately) approached me earlier this year and pointed out that
> git-replay had the functionality they wanted, I was happy that others
> liked it, and gave some pointers here and there, but I was _very_
> worried that upstreaming it would result in something getting
> backward-incompatibly locked into place that prevents the pieces that
> are  still in progress from getting implemented.  And I was concerned
> that my plans to create a tool people could experiment with, and that
> we could get usability feedback on (as I suggested at the Git
> contributors summit last year) might be in jeopardy as pieces of its
> functionality get locked into place before it's even ready for
> usability testing.  I was silently hoping they'd lose steam on
> rebasing and cleaning up my patches or choose to just delay until I
> could get real time to work on it with them.  (Since then, the one
> person who had ever advocated for my Git work at $DAYJOB, the best
> manager I had ever worked under, became not-a-manager.  I was
> blindsided by that in February.  Also, I have been transferred to a
> different team and am spinning up there.  And so, to my dismay, I'm
> worried my little sliver of Git time at work may evaporate entirely
> rather than return to previous healthier levels.)  Anyway, I've been
> fretting about things getting "locked-in" for a few months.

I'm also upset that you have been disrupted like this.

> They didn't lose steam, as I found out when these patches were
> submitted.  But, uh, I'm silently torn because I want to help
> Christian and Dscho get what they want, but having them be blocked on
> progress would reduce my stress.  A lot.

From my perspective, git-replay's most important use is being able
to generate rebases without a worktree or an interactive user. For
now, I don't even care if that includes conflict resolution. That's
enough of a lift that has enough unknowns that adding a complex CLI
at this early stage seems like a hasty decision to me. I'm voicing
my opinion that we should avoid backwards-compatibility issues by
implementing only the essentials.

That said, I also want to make sure that you eventually get the
super-flexible command that you want, but only when the framework
is ready for that kind of flexibility.

> Is there any chance people would be willing to accept a "NO BACKWARD
> COMPATIBILITY GUARANTEES" disclaimer on this command for a (long?)
> while, like Victoria suggested at Review club?  That might be an
> alternate solution that would lower my worries.

I'm not crazy about this idea, especially when it is easy to do
something simpler and avoid the need for it. But I'm just one voice
and one opinion.

Thanks,
-Stolee

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH 11/14] replay: use standard revision ranges
  2023-04-24 15:23                     ` Derrick Stolee
@ 2023-04-30  6:45                       ` Elijah Newren
  2023-09-03 15:47                         ` Johannes Schindelin
  0 siblings, 1 reply; 208+ messages in thread
From: Elijah Newren @ 2023-04-30  6:45 UTC (permalink / raw)
  To: Derrick Stolee
  Cc: Christian Couder, git, Junio C Hamano, Patrick Steinhardt,
	Johannes Schindelin, John Cai, Christian Couder

On Mon, Apr 24, 2023 at 8:23 AM Derrick Stolee <derrickstolee@github.com> wrote:
>
> On 4/22/2023 9:18 PM, Elijah Newren wrote:
> > On Thu, Apr 20, 2023 at 6:44 AM Derrick Stolee <derrickstolee@github.com> wrote:
> >>
> >> On 4/20/2023 12:53 AM, Elijah Newren wrote:
> >>> On Tue, Apr 18, 2023 at 6:10 AM Derrick Stolee <derrickstolee@github.com> wrote:
>
> >>  3. (Ordering options) Modifications to how those commits are ordered,
> >>     such as --topo-order, --date-order, and --reverse. These seem to
> >>     be overridden by git-replay (although, --reverse probably causes
> >>     some confusion right now).
> >
> > Yep, intentionally overridden.
> >
> > Could you elaborate on what you mean by --reverse causing confusion?
>
> It's very unlikely that a list of patches will successfully apply
> when applied in the reverse order. If we allow the argument, then
> someone will try it thinking they can flip their commits. Then they
> might be surprised when there are a bunch of conflicts on every
> commit.
>
> Basically, I'm not super thrilled about exposing options that are
> unlikely to be valuable to users and instead are more likely to cause
> confusion due to changes that won't successfully apply.

Oh, I got thrown by the "right now" portion of your comment; I
couldn't see how time or future changes would affect anything to make
it less (or more) confusing for users.

Quick clarification, though: while you correctly point out the type of
confusion the user would experience without my overriding, my
overriding of rev.reverse (after setup_revisions() returns, not before
it is called) precludes that experience.  The override means none of
the above happens, and they would instead just wonder why their option
is being ignored.

[...]
> > The rest is all fair, but I'd like to point out that there are a few
> > problems here:
> >
> >   * Tests should generally be passing before submitting upstream, so
> > all the code to implement them needs to be sent too
> >   * Submitted patch series have to be digestible sizes; not everything
> > can be submitted at once
> >   * Christian and Dscho wanted some of what I had implemented despite
> > other parts not being ready
>
> Outside of my first response (using positional arguments, recommended
> before you provided the extra context I was missing) I have not
> suggested implementing something that can't be forward compatible.

True, and maybe I was misunderstanding, but I thought you were trying
to make it forward compatible to only slight deviations while ruling
out most potential future changes, including ones I was specifically
highlighting as minimum eventual requirements.  While it was clear
that _some_ of the limitations you were trying to impose were only on
v1, it sounded like you were also trying to rule out a wide swath of
things permanently.  That's where things really concerned me, and why
I specifically asked which you were trying to do.

To be honest, I'm still unsure what your original intent was.  But it
probably doesn't matter now, as it sounds like you've shifted.  I
_think_ an accurate summary of where we are now at the end of
this thread is:
  * I am predominantly concerned about flexibility/potential for
git-replay in the future (what goes in v1 doesn't matter *if* it
doesn't affect the future, but that's a huge "if").
  * You are predominantly concerned about v1 and only including the
portions of functionality that have associated tests

I think there's enough wiggle room to mostly satisfy both viewpoints.

You helped with my concern by stating that categories (1) and (2)
should be fine for git-replay in the future.  (It at least handles the
forms of potential lock-in that I already know about, though I'm
worried there are others.)

I believe we can give you what you want via a tweak to the hack you
previously suggested for v1 only: rule out passing any options with a
leading dash to setup_revisions(), *and* throw an error if
revs->prune_data.nr is nonzero after calling setup_revisions().  But
I'd want us to include a comment pointing out that it's a hack, and
include in that note that in the future we want to allow all options
in categories (1) and (2) to be passed to setup_revisions() (or passed
to a suitable refactoring thereof).

[...]
> >> so your progress here isn't blocked on refactoring the revisions
> >> API.
> >
> > Is that a positive or a negative?  That question may surprise you, so
> > let me explain.
> >
> > I have a conflict of interest of sorts.  When Christian and Dscho
> > (separately) approached me earlier this year and pointed out that
> > git-replay had the functionality they wanted, I was happy that others
> > liked it, and gave some pointers here and there, but I was _very_
> > worried that upstreaming it would result in something getting
> > backward-incompatibly locked into place that prevents the pieces that
> > are  still in progress from getting implemented.  And I was concerned
> > that my plans to create a tool people could experiment with, and that
> > we could get usability feedback on (as I suggested at the Git
> > contributors summit last year) might be in jeopardy as pieces of its
> > functionality get locked into place before it's even ready for
> > usability testing.  I was silently hoping they'd lose steam on
> > rebasing and cleaning up my patches or choose to just delay until I
> > could get real time to work on it with them.  (Since then, the one
> > person who had ever advocated for my Git work at $DAYJOB, the best
> > manager I had ever worked under, became not-a-manager.  I was
> > blindsided by that in February.  Also, I have been transferred to a
> > different team and am spinning up there.  And so, to my dismay, I'm
> > worried my little sliver of Git time at work may evaporate entirely
> > rather than return to previous healthier levels.)  Anyway, I've been
> > fretting about things getting "locked-in" for a few months.
>
> I'm also upset that you have been disrupted like this.

Thanks.

> > They didn't lose steam, as I found out when these patches were
> > submitted.  But, uh, I'm silently torn because I want to help
> > Christian and Dscho get what they want, but having them be blocked on
> > progress would reduce my stress.  A lot.
>
> From my perspective, git-replay's most important use is being able
> to generate rebases without a worktree or an interactive user. For
> now, I don't even care if that includes conflict resolution. That's
> enough of a lift that has enough unknowns that adding a complex CLI
> at this early stage seems like a hasty decision to me.  I'm voicing
> my opinion that we should avoid backwards-compatibility issues by
> implementing only the essentials.

That comment is mostly reasonable, but I'd like to nit-pick the last
sentence: while limiting the series further for now is okay, I don't
believe that only implementing the essentials is any kind of guarantee
against backwards-compatibility issues.  More on that below.

> That said, I also want to make sure that you eventually get the
> super-flexible command that you want, but only when the framework
> is ready for that kind of flexibility.

Yaay!!!

> > Is there any chance people would be willing to accept a "NO BACKWARD
> > COMPATIBILITY GUARANTEES" disclaimer on this command for a (long?)
> > while, like Victoria suggested at Review club?  That might be an
> > alternate solution that would lower my worries.
>
> I'm not crazy about this idea, especially when it is easy to do
> something simpler and avoid the need for it. But I'm just one voice
> and one opinion.

Sorry I wasn't clear.  I wasn't suggesting this as a way of avoiding
something simpler; I was suggesting it as an addition to doing
something simpler, because it's not clear to me that doing something
simpler is sufficient.

Actually, I think it goes a bit further than that.  Most of my
objections to various forms of simplifying are due to the fact that I
think that simplifying greatly increases the risks of accidental
backward compatibility issues.

Part of the reason why I feel that way is that simplifying has to go
through future rounds of review and people may well respond with, "oh,
that'd be easier if you just did <X> instead of <Y>" with an implicit
assumption that there's no difference between them if <simplification
Z> is being presented as all we need, since <X> and <Y> handle that
simplification equally well.  It's kind of hard to guess in advance
all the forms that <X>, <Y>, and <Z> combined can take, and I also
have no idea if I'll have time to respond to upcoming reviews in a
timely fashion given current conditions, especially as I'm not the one
submitting the patches, so simplifying is a *big* risk in my book.
And this clearly isn't just theoretical, as this thread started
precisely with a simplification suggestion that would have broken
everything.  But even more importantly, I didn't start git-replay
until I found too many backward compatibility issues in rebase that
just couldn't be resolved, and yet I know that when folks review
patches they will make suggestions in line with what they are familiar
with, likely meaning rebase-like behavior.  So, "limiting to the
essentials" sounds like "increase the risk of future problems" to
me...unless we include a
backward-compatibility-explicitly-not-guaranteed-right-now disclaimer
from the beginning.

^ permalink raw reply	[flat|nested] 208+ messages in thread

* [PATCH v2 00/15] Introduce new `git replay` command
  2023-04-07  7:24 [PATCH 00/14] Introduce new `git replay` command Christian Couder
                   ` (16 preceding siblings ...)
  2023-04-15  6:44 ` Elijah Newren
@ 2023-05-09 17:53 ` Christian Couder
  2023-05-09 17:53   ` [PATCH v2 01/15] t6429: remove switching aspects of fast-rebase Christian Couder
                     ` (17 more replies)
  17 siblings, 18 replies; 208+ messages in thread
From: Christian Couder @ 2023-05-09 17:53 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

# Intro

`git replay` has initially been developed entirely by Elijah Newren
mostly last year (2022) at:

https://github.com/newren/git/commits/replay

I took over a few months ago to polish and upstream it as GitLab is
interested in replacing libgit2, and for that purpose needs a command
to do server side (so without using a worktree) rebases, cherry-picks
and reverts.

I reduced the number of commits and features in this first patch
series, compared to what Elijah already developed. Especially I
stopped short of replaying merge commits and replaying
interactively. These and other features might be upstreamed in the
future after this patch series has graduated.

The focus in this series is to make it a good plumbing command that
can already be used server side and that replaces the "fast-rebase"
test-tool command. So things to make it easier to use on the command
line, and more advanced features (like replaying merges) are left out.

# Content of this cover letter

The "Quick Overview" and "Reasons for diverging from cherry-pick &
rebase" sections just below are describing the purpose of the new
command in the big scheme of things. They are taken from Elijah's
design notes
(https://github.com/newren/git/blob/replay/replay-design-notes.txt)
and describe what we want this command to become and the reasons for
that, not what the command is after only this patch series. Also these
design notes were written at least one year ago, so parts of those 2
sections are not true anymore. I have added Phillip Wood's or Felipe
Contreras' notes (thanks to them) where that's the case, but some now
flawed parts may have missed.

After these two sections, starting with the "Important limitations"
section, you will find sections describing what is actually in this
patch series.

More interesting material is available in Elijah's design notes like
an "Intro via examples"
(https://github.com/newren/git/blob/replay/replay-design-notes.txt#L37-L132),
a discussion about "Preserving topology, replaying merges"
(https://github.com/newren/git/blob/replay/replay-design-notes.txt#L264-L341)
and a "Current status" section describing Elijah's work
(https://github.com/newren/git/blob/replay/replay-design-notes.txt#L344-L392)
before I started working on upstreaming it.

I have not included this material here though, as the documentation
added by this patch series for the `git replay` command already
includes an "EXAMPLES" section, and other sections of Elijah's design
notes might not be interesting for now. Also this cover letter is
already pretty long.  But reviewers can refer to the links above if
they think it can help.

# Quick Overview (from Elijah's design notes)

`git replay`, at a basic level, can perhaps be thought of as a
"default-to-dry-run rebase" -- meaning no updates to the working tree,
or to the index, or to any references.  However, it differs from
rebase in that it:

  * Works for branches that aren't checked out

  * Works in a bare repository

  * Can replay multiple branches simultaneously (with or without common
    history in the range being replayed)

  * Preserves relative topology by default (merges are replayed too in
    Elijah's original work, not in this series)

  * Focuses on performance

  * Has several altered defaults as a result of the above

I sometimes think of `git replay` as "fast-replay", a patch-based
analogue to the snapshot-based fast-export & fast-import tools.

# Reasons for diverging from cherry-pick & rebase (from Elijah's
  design notes)

There are multiple reasons to diverge from the defaults in cherry-pick and
rebase.

* Server side needs

  * Both cherry-pick and rebase, via the sequencer, are heavily tied
    to updating the working tree, index, some refs, and a lot of
    control files with every commit replayed, and invoke a mess of
    hooks[1] that might be hard to avoid for backward compatibility
    reasons (at least, that's been brought up a few times on the
    list).

  * cherry-pick and rebase both fork various subprocesses
    unnecessarily, but somewhat intrinsically in part to ensure the
    same hooks are called that old scripted implementations would have
    called.

    Note: since 356ee4659bb (sequencer: try to commit without forking
    'git commit', 2017-11-24) cherry-pick and rebase do not fork
    subprocesses other than hooks for the cases covered by this patch
    series (i.e. they do not fork "git commit" for simple picks).

  * "Dry run" behavior, where there are no updates to worktree, index,
    or even refs might be important.

  * Should not assume users only want to operate on HEAD (see next
    section)

* Decapitate HEAD-centric assumptions

  * cherry-pick forces commits to be played on top of HEAD;
    inflexible.

  * rebase assumes the range of commits to be replayed is
    upstream..HEAD by default, though it allows one to replay
    upstream..otherbranch -- but it still forcibly and needlessly
    checks out 'otherbranch' before starting to replay things.

    Note: since 767a9c417eb (rebase -i: stop checking out the tip of
    the branch to rebase, 2020-01-24) it's not true that rebase
    forcibly and needlessly checks out 'otherbranch'.

  * Assuming HEAD is involved severely limits replaying multiple
    (possibly divergent) branches.

    Note: since 89fc0b53fdb (rebase: update refs from 'update-ref'
    commands, 2022-07-19) the sequencer can update multiple
    branches. The issue with divergent branch is with command line
    arguments and the todo list generation rather than the
    capabilities of the sequencer.

  * Once you stop assuming HEAD has a certain meaning, there's not
    much reason to have two separate commands anymore (except for the
    funny extra not-necessarily-compatible options both have gained
    over time).

  * (Micro issue: Assuming HEAD is involved also makes it harder for
    new users to learn what rebase means and does; it makes command
    lines hard to parse.  Not sure I want to harp on this too much, as
    I have a suspicion I might be creating a tool for experts with
    complicated use cases, but it's a minor quibble.)

* Performance

  * jj is slaughtering us on rebase speed[2].  I would like us to become
    competitive.  (I dropped a few comments in the link at [2] about why
    git is currently so bad.)

  * From [3], there was a simple 4-patch series in linux.git that took
    53 seconds to rebase.  Switching to ort dropped it to 16 seconds.
    While that sounds great, only 11 *milliseconds* were needed to do
    the actual merges.  That means almost *all* the time (>99%) was
    overhead!  Big offenders:

    * --reapply-cherry-picks should be the default

    * can_fast_forward() should be ripped out, and perhaps other extraneous
      revision walks

      Note: d42c9ffa0f (rebase: factor out branch_base calculation,
      2022-10-17) might already deal with that (according to Felipe
      Contreras).

    * avoid updating working tree, index, refs, reflogs, and control
      structures except when needed (e.g. hitting a conflict, or operation
      finished)

  * Other performance ideas (mostly for future work, not in this
    series)

    * single-file control structures instead of directory of files
      (when doing interactive things which is in Elijah's original
      work, but not in this series)

    * avoid forking subprocesses unless explicitly requested (e.g.
      --exec, --strategy, --run-hooks).  For example, definitely do not
      invoke `git commit` or `git merge`.

    * Sanitize hooks:

      * dispense with all per-commit hooks for sure (pre-commit,
        post-commit, post-checkout).

      * pre-rebase also seems to assume exactly 1 ref is written, and
        invoking it repeatedly would be stupid.  Plus, it's specific
        to "rebase".  So...ignore?  (Stolee's --ref-update option for
        rebase probably broke the pre-rebase assumptions already...)

      * post-rewrite hook might make sense, but fast-import got
        exempted, and I think of replay like a patch-based analogue
        to the snapshot-based fast-import.

    * When not running server side, resolve conflicts in a sparse-cone
      sparse-index worktree to reduce number of files written to a
      working tree.  (See below as well.)

    * [High risk of possible premature optimization] Avoid large
      numbers of newly created loose objects, when replaying large
      numbers of commits.  Two possibilities: (1) Consider using
      tmp-objdir and pack objects from the tmp-objdir at end of
      exercise, (2) Lift code from git-fast-import to immediately
      stuff new objects into a pack?

* Multiple branches and non-checked out branches

  * The ability to operate on non-checked out branches also implies
    that we should generally be able to replay when in a dirty working
    tree (exception being when we expect to update HEAD and any of the
    dirty files is one that needs to be updated by the replay).

  * Also, if we are operating locally on a non-checked out branch and
    hit a conflict, we should have a way to resolve the conflict
    without messing with the user's work on their current
    branch. (This is not is this patch series though.)

    * Idea: new worktree with sparse cone + sparse index checkout,
      containing only files in the root directory, and whatever is
      necessary to get the conflicts

    * Companion to above idea: control structures should be written to
      $GIT_COMMON_DIR/replay-${worktree}, so users can have multiple
      replay sessions, and so we know which worktrees are associated
      with which replay operations.

  - [1] https://lore.kernel.org/git/pull.749.v3.git.git.1586044818132.gitgitgadget@gmail.com/
  - [2] https://github.com/martinvonz/jj/discussions/49
  - [3] https://lore.kernel.org/git/CABPp-BE48=97k_3tnNqXPjSEfA163F8hoE+HY0Zvz1SWB2B8EA@mail.gmail.com/

# Important limitations

* The code exits with code 1 if there are any conflict. No
  resumability. No nice output. No interactivity. No special exit code
  depending on the reason.

* When a commit becomes empty as it is replayed, it is still replayed
  as an empty commit, instead of being dropped.

* No replaying merges, nor root commits. Only regular commits.

* Signed commits are not properly handled. It's not clear what to do
  to such commits when replaying on the server side.

* Notes associated with replayed commits are not updated nor
  duplicated. (Thanks to Phillip Wood for noticing.)

# Commit overview

* 1/15 t6429: remove switching aspects of fast-rebase

    New preparatory commit to make it easier to later replace the
    fast-rebase test-tool by `git replay` without breaking existing
    tests.

* 2/15 replay: introduce new builtin

     This creates a minimal `git replay` command by moving the code
     from the `fast-rebase` test helper from `t/helper/` into
     `builtin/` and doing some renames and a few other needed changes.
     (No change on this commit since v1.)

* - 3/15 replay: start using parse_options API
  - 4/15 replay: die() instead of failing assert()
  - 5/15 replay: introduce pick_regular_commit()
  - 6/15 replay: don't simplify history
  - 7/15 replay: add an important FIXME comment about gpg signing
  - 8/15 replay: remove progress and info output
  - 9/15 replay: remove HEAD related sanity check

     These slowly change the command to make it behave more like a
     regular commands and to start cleaning up its output. (No change
     on these commits since v1.)

* 10/15 replay: make it a minimal server side command

     After the cleaning up in previous commits, it's now time to
     radically change the way it works by stopping it to do ref
     updates, to update the index and worktree, to consider HEAD as
     special. Instead just make it output commands that should be
     passed to `git update-ref --stdin`. (No change on this commit
     since v1.)

* 11/15 replay: use standard revision ranges

     Start addind new interesting features and also documentation and
     tests, as the interface of the command is cristalizing into its
     final form.

     Since v1, new tests to check that the command works well on bare
     repo and exits with code 1 in case of conflict have been
     added. The doc also now says that the new command is
     experimental.

* 12/15 replay: disallow revision specific options and pathspecs

     New commit to disallow for now revision specific options and
     pathspecs that are allowed and eaten by setup_revisions(), as
     it's not clear if all of these extra features are really needed,
     and anyway they would require tests and doc. So we leave them for
     future improvements.

* - 13/15 replay: add --advance or 'cherry-pick' mode
  - 14/15 replay: add --contained to rebase contained branches

     New commits, but extracted from a previous commit (that added both
     --advance and --contained at the same time) without any code
     change except that a few test cases for bare repos were added.

* 15/15 replay: stop assuming replayed branches do not diverge

      This adds another interesting feature, as well as related
      documentation and tests. (Since v1 a few test cases for bare
      repos were added.)

# Notes about `fast-rebase`, tests and documentation

The `fast-rebase` test-tool helper was developed by Elijah to
experiment with a rebasing tool that would be developed from scratch
based on his merge-ort work, could be used to test that merge-ort
work, and would not have the speed and interface limitations of `git
rebase` or `git cherry-pick`.

This `fast-rebase` helper was used before this series in:

t6429-merge-sequence-rename-caching.sh

So when `git replay` is created from `fast-rebase` in patch 2/15, the
t6429 test script is also converted to use `git replay`. This ensures
that `git replay` doesn't break too badly during the first 10 patches
in this patch series.

Tests and documentation are introduced specifically for `git replay`
only in 11/15 and later patches as it doesn't make much sense to
document and test behavior that we know is going to change soon. So
it's only when the command is crystalizing towards its final form that
we start documenting and testing it.

# Possibly controversial issues 

* bare or not bare: this series works towards a plumbing command with
  the end goal of it being usable and used first on bare repos,
  contrary to existing commands like `git rebase` and `git
  cherry-pick`. The tests check that the command works on both bare
  and non-bare repo though.

* exit status: a successful, non-conflicted replay exits with code
  0. When the replay has conflicts, the exit status is 1. If the
  replay is not able to complete (or start) due to some kind of error,
  the exit status is something other than 0 or 1. There are a few
  tests checking that. It has been suggested in an internal review
  that conflicts might want to get a more specific error code as an
  error code of 1 might be quite easy to return by accident. It
  doesn't seem to me from their docs (which might want to be improved,
  I didn't look at the code) that other commands like `git merge` and
  `git rebase` exit with a special error code in case of conflict.

* make worktree and index changes optional: commit 10/15 stops
  updating the index and worktree, but it might be better especially
  for cli users to make that optional. The issue is that this would
  make the command more complex while we are developing a number of
  important features so that the command can be used on bare repos. It
  seems that this should rather be done in an iterative improvement
  after the important features have landed.

* when and where to add tests and docs: although t6429 has tests that
  are changed to use the new command instead of the fast-rebase
  test-tool command as soon as the former is introduced, there is no
  specific test script and no doc for the new command until commit
  11/15 when standard revision ranges are used. This is done to avoid
  churn in tests and docs while the final form of the command hasn't
  crystalized enough. Adding tests and doc at this point makes this
  commit quite big and possibly more difficult to review than if they
  were in separate commits though. On the other hand when tests and
  docs are added in specific commits some reviewers say it would be
  better to introduce them when the related changes are made.

* --advance and --contained: these two advanced options might not
  belong to this first series and could perhaps be added in a followup
  series in separate commits. On the other hand the code for
  --contained seems involved with the code of --advance and it's nice
  to see soon that git replay can indeed do cherry-picking and rebase
  many refs at once, and this way fullfil these parts of its promise.

* replaying diverging branches: 15/15 the last patch in the series,
  which allow replaying diverging branches, can be seen as a
  fundamental fix or alternatively as adding an interesting
  feature. So it's debatable if it should be in its own patch along
  with its own tests as in this series, or if it should be merged into
  a previous patch and which one.

* only 2 patches: this patch series can be seen from a high level
  point of view as 1) introducing the new `git replay` command, and 2)
  using `git replay` to replace, and get rid of, the fast-rebase
  test-tool command. The fact that not much of the original
  fast-rebase code and interface is left would agree with that point
  of view. On the other hand, fast-rebase can also be seen as a first
  iteration towards `git replay`. So it can also make sense to see how
  `git replay` evolved from it.

# Changes between v1 and v2

Thanks to all the reviewers on the mailing list or during a Review
Club! Especially, thanks to Elijah, Dscho, Phillip Wood, Derrick
Stolee, Patrick Steinhardt, Junio, Felipe Contreras, Calvin Wan and
all the Review Club participants.

The Review Club notes (see those on 2023-04-18) are available on:

https://docs.google.com/document/d/14L8BAumGTpsXpjDY8VzZ4rRtpAjuGrFSRqn3stCuS_w/

Now the changes between v1 and v2 are:

* The patch series has been rebased on top of current master, so that
  conflicts with master are resolved. This required replacing calls to
  functions like get_oid() with repo_get_oid(), as well as changing
  some `#include "*.h"` to use different header files. This
  unfortunately makes the range-diff between v1 and v2 a bit more
  difficult to read than we would like.

* Patch 1/15 is a new patch. It has been introduced to rework t6429,
  so that this test script doesn't break when conflict output is
  removed later in the series. This allowed us to remove the old 9/14
  patch ("replay: very coarse worktree updating").

* The old 9/14 patch ("replay: very coarse worktree updating") was
  removed. This is to make sure that the new command can be used on
  bare repos. This enables us to focus first on making sure the new
  command a good plumbing command that works well on bare repos.

* The old 12/14 patch ("replay: introduce guess_new_base()") was
  removed. This is also to focus first on making sure the new command
  is a good plumbing command.

* The old 13/14 patch ("replay: add different modes") has been split
  into 2 patches: patch 13/15 ("replay: add --advance or 'cherry-pick'
  mode") and patch 14/15 ("replay: add --contained to rebase contained
  branches").

* Patch 12/15 ("replay: disallow revision specific options and
  pathspecs") is a new patch that disallow using revision specific
  options that would be allowed and eaten by setup_revisions(). Even
  if it would be very nice to have some of these options in the
  future, not all these options make sense for the new command, and it
  might require improving the setup_revisions() mechanism (as well as
  adding tests and doc) to cleanly allow those we want, but that is
  outside the scope of this first patch series.

  While at it, this patch also disallow pathspecs too. It would be
  nice to have them in the future, but for now it is not necessary and
  would require work (at least for tests and doc) that can be done
  later.

* Patches 11/15 to 15/15 include a number of new tests to check that
  the command works well on bare repos, and in case of 11/15 to check
  that exit code in case of conflict is 1.

* Patch 11/15, in which the doc for the `git replay` command is
  introduced, now says that the new command is experimental.

* Note that a segfault that was found by Derrick Stolee in the v1 is
  fixed by 000c4ceca7 (merge-ort: fix calling merge_finalize() with no
  intermediate merge, 2023-04-22) merged to master by 3927312601
  (Merge branch 'en/ort-finalize-after-0-merges-fix', 2023-05-02).


Christian Couder (1):
  replay: disallow revision specific options and pathspecs

Elijah Newren (14):
  t6429: remove switching aspects of fast-rebase
  replay: introduce new builtin
  replay: start using parse_options API
  replay: die() instead of failing assert()
  replay: introduce pick_regular_commit()
  replay: don't simplify history
  replay: add an important FIXME comment about gpg signing
  replay: remove progress and info output
  replay: remove HEAD related sanity check
  replay: make it a minimal server side command
  replay: use standard revision ranges
  replay: add --advance or 'cherry-pick' mode
  replay: add --contained to rebase contained branches
  replay: stop assuming replayed branches do not diverge

 .gitignore                               |   1 +
 Documentation/git-replay.txt             | 125 +++++++
 Makefile                                 |   2 +-
 builtin.h                                |   1 +
 builtin/replay.c                         | 431 +++++++++++++++++++++++
 command-list.txt                         |   1 +
 git.c                                    |   1 +
 t/helper/test-fast-rebase.c              | 240 -------------
 t/helper/test-tool.c                     |   1 -
 t/helper/test-tool.h                     |   1 -
 t/t3650-replay-basics.sh                 | 206 +++++++++++
 t/t6429-merge-sequence-rename-caching.sh |  45 +--
 12 files changed, 792 insertions(+), 263 deletions(-)
 create mode 100644 Documentation/git-replay.txt
 create mode 100644 builtin/replay.c
 delete mode 100644 t/helper/test-fast-rebase.c
 create mode 100755 t/t3650-replay-basics.sh

-- 
2.40.1.491.gdff9a222ea


^ permalink raw reply	[flat|nested] 208+ messages in thread

* [PATCH v2 01/15] t6429: remove switching aspects of fast-rebase
  2023-05-09 17:53 ` [PATCH v2 00/15] " Christian Couder
@ 2023-05-09 17:53   ` Christian Couder
  2023-05-09 17:53   ` [PATCH v2 02/15] replay: introduce new builtin Christian Couder
                     ` (16 subsequent siblings)
  17 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-05-09 17:53 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

From: Elijah Newren <newren@gmail.com>

At the time t6429 was written, merge-ort was still under development,
did not have quite as many tests, and certainly was not widely deployed.
Since t6429 was exercising some codepaths just a little differently, we
thought having them also test the "merge_switch_to_result()" bits of
merge-ort was useful even though they weren't intrinsic to the real
point of these tests.

However, the value provided by doing extra testing of the
"merge_switch_to_result()" bits has decreased a bit over time, and it's
actively making it harder to refactor `test-tool fast-rebase` into `git
replay`, which we are going to do in following commits.  Dispense with
these bits.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 t/helper/test-fast-rebase.c              | 9 +--------
 t/t6429-merge-sequence-rename-caching.sh | 9 +++++++--
 2 files changed, 8 insertions(+), 10 deletions(-)

diff --git a/t/helper/test-fast-rebase.c b/t/helper/test-fast-rebase.c
index d1d63feaa9..403bdf8e75 100644
--- a/t/helper/test-fast-rebase.c
+++ b/t/helper/test-fast-rebase.c
@@ -193,7 +193,7 @@ int cmd__fast_rebase(int argc, const char **argv)
 		last_commit = create_commit(result.tree, commit, last_commit);
 	}
 
-	merge_switch_to_result(&merge_opt, head_tree, &result, 1, !result.clean);
+	merge_finalize(&merge_opt, &result);
 
 	if (result.clean < 0)
 		exit(128);
@@ -212,9 +212,6 @@ int cmd__fast_rebase(int argc, const char **argv)
 		}
 		if (create_symref("HEAD", branch_name.buf, reflog_msg.buf) < 0)
 			die(_("unable to update HEAD"));
-
-		prime_cache_tree(the_repository, the_repository->index,
-				 result.tree);
 	} else {
 		fprintf(stderr, "\nAborting: Hit a conflict.\n");
 		strbuf_addf(&reflog_msg, "rebase progress up to %s",
@@ -227,10 +224,6 @@ int cmd__fast_rebase(int argc, const char **argv)
 			die("Failed to update %s", argv[4]);
 		}
 	}
-	if (write_locked_index(&the_index, &lock,
-			       COMMIT_LOCK | SKIP_IF_UNCHANGED))
-		die(_("unable to write %s"), get_index_file());
-
 	ret = (result.clean == 0);
 cleanup:
 	strbuf_release(&reflog_msg);
diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh
index d02fa16614..75d3fd2dba 100755
--- a/t/t6429-merge-sequence-rename-caching.sh
+++ b/t/t6429-merge-sequence-rename-caching.sh
@@ -72,6 +72,7 @@ test_expect_success 'caching renames does not preclude finding new ones' '
 		git switch upstream &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream~1..topic
 
 		git ls-files >tracked-files &&
@@ -200,6 +201,7 @@ test_expect_success 'rename same file identically, then reintroduce it' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream~1..topic &&
 
 		git ls-files >tracked &&
@@ -277,6 +279,7 @@ test_expect_success 'rename same file identically, then add file to old dir' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream~1..topic &&
 
 		git ls-files >tracked &&
@@ -356,8 +359,6 @@ test_expect_success 'cached dir rename does not prevent noticing later conflict'
 		test_must_fail test-tool fast-rebase --onto HEAD upstream~1 topic >output &&
 		#git cherry-pick upstream..topic &&
 
-		grep CONFLICT..rename/rename output &&
-
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls
 	)
@@ -456,6 +457,7 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
@@ -522,6 +524,7 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
@@ -624,6 +627,7 @@ test_expect_success 'caching renames only on upstream side, part 1' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
@@ -682,6 +686,7 @@ test_expect_success 'caching renames only on upstream side, part 2' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
-- 
2.40.1.491.gdff9a222ea


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v2 02/15] replay: introduce new builtin
  2023-05-09 17:53 ` [PATCH v2 00/15] " Christian Couder
  2023-05-09 17:53   ` [PATCH v2 01/15] t6429: remove switching aspects of fast-rebase Christian Couder
@ 2023-05-09 17:53   ` Christian Couder
  2023-05-09 17:53   ` [PATCH v2 03/15] replay: start using parse_options API Christian Couder
                     ` (15 subsequent siblings)
  17 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-05-09 17:53 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

From: Elijah Newren <newren@gmail.com>

For now, this is just a rename from `t/helper/test-fast-rebase.c` into
`builtin/replay.c` with minimal changes to make it build appropriately.

Subsequent commits will flesh out its capabilities and make it a more
standard regular builtin.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 .gitignore                                    |  1 +
 Makefile                                      |  2 +-
 builtin.h                                     |  1 +
 .../test-fast-rebase.c => builtin/replay.c    | 25 +++++------------
 command-list.txt                              |  1 +
 git.c                                         |  1 +
 t/helper/test-tool.c                          |  1 -
 t/helper/test-tool.h                          |  1 -
 t/t6429-merge-sequence-rename-caching.sh      | 27 +++++++------------
 9 files changed, 20 insertions(+), 40 deletions(-)
 rename t/helper/test-fast-rebase.c => builtin/replay.c (89%)

diff --git a/.gitignore b/.gitignore
index e875c59054..b5f025a296 100644
--- a/.gitignore
+++ b/.gitignore
@@ -135,6 +135,7 @@
 /git-remote-ext
 /git-repack
 /git-replace
+/git-replay
 /git-request-pull
 /git-rerere
 /git-reset
diff --git a/Makefile b/Makefile
index e440728c24..a6ab78840f 100644
--- a/Makefile
+++ b/Makefile
@@ -799,7 +799,6 @@ TEST_BUILTINS_OBJS += test-dump-split-index.o
 TEST_BUILTINS_OBJS += test-dump-untracked-cache.o
 TEST_BUILTINS_OBJS += test-env-helper.o
 TEST_BUILTINS_OBJS += test-example-decorate.o
-TEST_BUILTINS_OBJS += test-fast-rebase.o
 TEST_BUILTINS_OBJS += test-fsmonitor-client.o
 TEST_BUILTINS_OBJS += test-genrandom.o
 TEST_BUILTINS_OBJS += test-genzeros.o
@@ -1286,6 +1285,7 @@ BUILTIN_OBJS += builtin/remote-fd.o
 BUILTIN_OBJS += builtin/remote.o
 BUILTIN_OBJS += builtin/repack.o
 BUILTIN_OBJS += builtin/replace.o
+BUILTIN_OBJS += builtin/replay.o
 BUILTIN_OBJS += builtin/rerere.o
 BUILTIN_OBJS += builtin/reset.o
 BUILTIN_OBJS += builtin/rev-list.o
diff --git a/builtin.h b/builtin.h
index cb0db67681..c3f0b56915 100644
--- a/builtin.h
+++ b/builtin.h
@@ -214,6 +214,7 @@ int cmd_remote(int argc, const char **argv, const char *prefix);
 int cmd_remote_ext(int argc, const char **argv, const char *prefix);
 int cmd_remote_fd(int argc, const char **argv, const char *prefix);
 int cmd_repack(int argc, const char **argv, const char *prefix);
+int cmd_replay(int argc, const char **argv, const char *prefix);
 int cmd_rerere(int argc, const char **argv, const char *prefix);
 int cmd_reset(int argc, const char **argv, const char *prefix);
 int cmd_restore(int argc, const char **argv, const char *prefix);
diff --git a/t/helper/test-fast-rebase.c b/builtin/replay.c
similarity index 89%
rename from t/helper/test-fast-rebase.c
rename to builtin/replay.c
index 403bdf8e75..9637c31da2 100644
--- a/t/helper/test-fast-rebase.c
+++ b/builtin/replay.c
@@ -1,18 +1,11 @@
 /*
- * "git fast-rebase" builtin command
- *
- * FAST: Forking Any Subprocesses (is) Taboo
- *
- * This is meant SOLELY as a demo of what is possible.  sequencer.c and
- * rebase.c should be refactored to use the ideas here, rather than attempting
- * to extend this file to replace those (unless Phillip or Dscho say that
- * refactoring is too hard and we need a clean slate, but I'm guessing that
- * refactoring is the better route).
+ * "git replay" builtin command
  */
 
 #define USE_THE_INDEX_VARIABLE
-#include "test-tool.h"
-#include "cache.h"
+#include "git-compat-util.h"
+
+#include "builtin.h"
 #include "cache-tree.h"
 #include "commit.h"
 #include "environment.h"
@@ -93,7 +86,7 @@ static struct commit *create_commit(struct tree *tree,
 	return (struct commit *)obj;
 }
 
-int cmd__fast_rebase(int argc, const char **argv)
+int cmd_replay(int argc, const char **argv, const char *prefix)
 {
 	struct commit *onto;
 	struct commit *last_commit = NULL, *last_picked_commit = NULL;
@@ -109,12 +102,6 @@ int cmd__fast_rebase(int argc, const char **argv)
 	struct strbuf branch_name = STRBUF_INIT;
 	int ret = 0;
 
-	/*
-	 * test-tool stuff doesn't set up the git directory by default; need to
-	 * do that manually.
-	 */
-	setup_git_directory();
-
 	if (argc == 2 && !strcmp(argv[1], "-h")) {
 		printf("Sorry, I am not a psychiatrist; I can not give you the help you need.  Oh, you meant usage...\n");
 		exit(129);
@@ -135,7 +122,7 @@ int cmd__fast_rebase(int argc, const char **argv)
 	if (repo_read_index(the_repository) < 0)
 		BUG("Could not read index");
 
-	repo_init_revisions(the_repository, &revs, NULL);
+	repo_init_revisions(the_repository, &revs, prefix);
 	revs.verbose_header = 1;
 	revs.max_parents = 1;
 	revs.cherry_mark = 1;
diff --git a/command-list.txt b/command-list.txt
index 54b2a50f5f..d74836ab21 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -160,6 +160,7 @@ git-reflog                              ancillarymanipulators           complete
 git-remote                              ancillarymanipulators           complete
 git-repack                              ancillarymanipulators           complete
 git-replace                             ancillarymanipulators           complete
+git-replay                              mainporcelain           history
 git-request-pull                        foreignscminterface             complete
 git-rerere                              ancillaryinterrogators
 git-reset                               mainporcelain           history
diff --git a/git.c b/git.c
index 45899be826..a1252e4cd3 100644
--- a/git.c
+++ b/git.c
@@ -577,6 +577,7 @@ static struct cmd_struct commands[] = {
 	{ "remote-fd", cmd_remote_fd, NO_PARSEOPT },
 	{ "repack", cmd_repack, RUN_SETUP },
 	{ "replace", cmd_replace, RUN_SETUP },
+	{ "replay", cmd_replay, RUN_SETUP },
 	{ "rerere", cmd_rerere, RUN_SETUP },
 	{ "reset", cmd_reset, RUN_SETUP },
 	{ "restore", cmd_restore, RUN_SETUP | NEED_WORK_TREE },
diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
index abe8a785eb..9ca1586de7 100644
--- a/t/helper/test-tool.c
+++ b/t/helper/test-tool.c
@@ -30,7 +30,6 @@ static struct test_cmd cmds[] = {
 	{ "dump-untracked-cache", cmd__dump_untracked_cache },
 	{ "env-helper", cmd__env_helper },
 	{ "example-decorate", cmd__example_decorate },
-	{ "fast-rebase", cmd__fast_rebase },
 	{ "fsmonitor-client", cmd__fsmonitor_client },
 	{ "genrandom", cmd__genrandom },
 	{ "genzeros", cmd__genzeros },
diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h
index ea2672436c..a03bbfc6b2 100644
--- a/t/helper/test-tool.h
+++ b/t/helper/test-tool.h
@@ -24,7 +24,6 @@ int cmd__dump_untracked_cache(int argc, const char **argv);
 int cmd__dump_reftable(int argc, const char **argv);
 int cmd__env_helper(int argc, const char **argv);
 int cmd__example_decorate(int argc, const char **argv);
-int cmd__fast_rebase(int argc, const char **argv);
 int cmd__fsmonitor_client(int argc, const char **argv);
 int cmd__genrandom(int argc, const char **argv);
 int cmd__genzeros(int argc, const char **argv);
diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh
index 75d3fd2dba..7670b72008 100755
--- a/t/t6429-merge-sequence-rename-caching.sh
+++ b/t/t6429-merge-sequence-rename-caching.sh
@@ -71,9 +71,8 @@ test_expect_success 'caching renames does not preclude finding new ones' '
 
 		git switch upstream &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream~1..topic
 
 		git ls-files >tracked-files &&
 		test_line_count = 2 tracked-files &&
@@ -141,8 +140,7 @@ test_expect_success 'cherry-pick both a commit and its immediate revert' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
-		#git cherry-pick upstream~1..topic &&
+		git replay --onto HEAD upstream~1 topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 1 calls
@@ -200,9 +198,8 @@ test_expect_success 'rename same file identically, then reintroduce it' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream~1..topic &&
 
 		git ls-files >tracked &&
 		test_line_count = 2 tracked &&
@@ -278,9 +275,8 @@ test_expect_success 'rename same file identically, then add file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream~1..topic &&
 
 		git ls-files >tracked &&
 		test_line_count = 4 tracked &&
@@ -356,8 +352,7 @@ test_expect_success 'cached dir rename does not prevent noticing later conflict'
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test_must_fail test-tool fast-rebase --onto HEAD upstream~1 topic >output &&
-		#git cherry-pick upstream..topic &&
+		test_must_fail git replay --onto HEAD upstream~1 topic >output &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls
@@ -456,9 +451,8 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls &&
@@ -523,9 +517,8 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 3 calls &&
@@ -626,9 +619,8 @@ test_expect_success 'caching renames only on upstream side, part 1' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 1 calls &&
@@ -685,9 +677,8 @@ test_expect_success 'caching renames only on upstream side, part 2' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls &&
-- 
2.40.1.491.gdff9a222ea


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v2 03/15] replay: start using parse_options API
  2023-05-09 17:53 ` [PATCH v2 00/15] " Christian Couder
  2023-05-09 17:53   ` [PATCH v2 01/15] t6429: remove switching aspects of fast-rebase Christian Couder
  2023-05-09 17:53   ` [PATCH v2 02/15] replay: introduce new builtin Christian Couder
@ 2023-05-09 17:53   ` Christian Couder
  2023-05-09 17:53   ` [PATCH v2 04/15] replay: die() instead of failing assert() Christian Couder
                     ` (14 subsequent siblings)
  17 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-05-09 17:53 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

From: Elijah Newren <newren@gmail.com>

Instead of manually parsing arguments, let's start using the parse_options
API. This way this new builtin will look more standard, and in some
upcoming commits will more easily be able to handle more command line
options.

Note that we plan to later use standard revision ranges instead of
hardcoded "<oldbase> <branch>" arguments. When we will use standard
revision ranges, it will be easier to check if there are no spurious
arguments if we keep ARGV[0], so let's call parse_options() with
PARSE_OPT_KEEP_ARGV0 even if we don't need ARGV[0] right now to avoid
some useless code churn.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 44 ++++++++++++++++++++++++++++++++------------
 1 file changed, 32 insertions(+), 12 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 9637c31da2..4644bee3cf 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -14,6 +14,7 @@
 #include "lockfile.h"
 #include "merge-ort.h"
 #include "object-name.h"
+#include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
 #include "sequencer.h"
@@ -89,6 +90,7 @@ static struct commit *create_commit(struct tree *tree,
 int cmd_replay(int argc, const char **argv, const char *prefix)
 {
 	struct commit *onto;
+	const char *onto_name = NULL;
 	struct commit *last_commit = NULL, *last_picked_commit = NULL;
 	struct object_id head;
 	struct lock_file lock = LOCK_INIT;
@@ -102,16 +104,32 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct strbuf branch_name = STRBUF_INIT;
 	int ret = 0;
 
-	if (argc == 2 && !strcmp(argv[1], "-h")) {
-		printf("Sorry, I am not a psychiatrist; I can not give you the help you need.  Oh, you meant usage...\n");
-		exit(129);
+	const char * const replay_usage[] = {
+		N_("git replay --onto <newbase> <oldbase> <branch>"),
+		NULL
+	};
+	struct option replay_options[] = {
+		OPT_STRING(0, "onto", &onto_name,
+			   N_("revision"),
+			   N_("replay onto given commit")),
+		OPT_END()
+	};
+
+	argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
+			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
+
+	if (!onto_name) {
+		error(_("option --onto is mandatory"));
+		usage_with_options(replay_usage, replay_options);
 	}
 
-	if (argc != 5 || strcmp(argv[1], "--onto"))
-		die("usage: read the code, figure out how to use it, then do so");
+	if (argc != 3) {
+		error(_("bad number of arguments"));
+		usage_with_options(replay_usage, replay_options);
+	}
 
-	onto = peel_committish(argv[2]);
-	strbuf_addf(&branch_name, "refs/heads/%s", argv[4]);
+	onto = peel_committish(onto_name);
+	strbuf_addf(&branch_name, "refs/heads/%s", argv[2]);
 
 	/* Sanity check */
 	if (repo_get_oid(the_repository, "HEAD", &head))
@@ -123,6 +141,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		BUG("Could not read index");
 
 	repo_init_revisions(the_repository, &revs, prefix);
+
 	revs.verbose_header = 1;
 	revs.max_parents = 1;
 	revs.cherry_mark = 1;
@@ -131,7 +150,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	revs.right_only = 1;
 	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
 	revs.topo_order = 1;
-	strvec_pushl(&rev_walk_args, "", argv[4], "--not", argv[3], NULL);
+
+	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
 
 	if (setup_revisions(rev_walk_args.nr, rev_walk_args.v, &revs, NULL) > 1) {
 		ret = error(_("unhandled options"));
@@ -194,8 +214,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			       &last_commit->object.oid,
 			       &last_picked_commit->object.oid,
 			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
-			error(_("could not update %s"), argv[4]);
-			die("Failed to update %s", argv[4]);
+			error(_("could not update %s"), argv[2]);
+			die("Failed to update %s", argv[2]);
 		}
 		if (create_symref("HEAD", branch_name.buf, reflog_msg.buf) < 0)
 			die(_("unable to update HEAD"));
@@ -207,8 +227,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			       &last_commit->object.oid,
 			       &head,
 			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
-			error(_("could not update %s"), argv[4]);
-			die("Failed to update %s", argv[4]);
+			error(_("could not update %s"), argv[2]);
+			die("Failed to update %s", argv[2]);
 		}
 	}
 	ret = (result.clean == 0);
-- 
2.40.1.491.gdff9a222ea


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v2 04/15] replay: die() instead of failing assert()
  2023-05-09 17:53 ` [PATCH v2 00/15] " Christian Couder
                     ` (2 preceding siblings ...)
  2023-05-09 17:53   ` [PATCH v2 03/15] replay: start using parse_options API Christian Couder
@ 2023-05-09 17:53   ` Christian Couder
  2023-05-09 17:53   ` [PATCH v2 05/15] replay: introduce pick_regular_commit() Christian Couder
                     ` (13 subsequent siblings)
  17 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-05-09 17:53 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

From: Elijah Newren <newren@gmail.com>

It's not a good idea for regular Git commands to use an assert() to
check for things that could happen but are not supported.

Let's die() with an explanation of the issue instead.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 4644bee3cf..366ea8ef08 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -177,7 +177,12 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 
 		fprintf(stderr, "Rebasing %s...\r",
 			oid_to_hex(&commit->object.oid));
-		assert(commit->parents && !commit->parents->next);
+
+		if (!commit->parents)
+			die(_("replaying down to root commit is not supported yet!"));
+		if (commit->parents->next)
+			die(_("replaying merge commits is not supported yet!"));
+
 		base = commit->parents->item;
 
 		next_tree = repo_get_commit_tree(the_repository, commit);
-- 
2.40.1.491.gdff9a222ea


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v2 05/15] replay: introduce pick_regular_commit()
  2023-05-09 17:53 ` [PATCH v2 00/15] " Christian Couder
                     ` (3 preceding siblings ...)
  2023-05-09 17:53   ` [PATCH v2 04/15] replay: die() instead of failing assert() Christian Couder
@ 2023-05-09 17:53   ` Christian Couder
  2023-05-09 17:53   ` [PATCH v2 06/15] replay: don't simplify history Christian Couder
                     ` (12 subsequent siblings)
  17 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-05-09 17:53 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

From: Elijah Newren <newren@gmail.com>

Let's refactor the code to handle a regular commit (a commit that is
neither a root commit nor a merge commit) into a single function instead
of keeping it inside cmd_replay().

This is good for separation of concerns, and this will help further work
in the future to replay merge commits.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 54 ++++++++++++++++++++++++++++++------------------
 1 file changed, 34 insertions(+), 20 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 366ea8ef08..f087d97353 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -87,6 +87,35 @@ static struct commit *create_commit(struct tree *tree,
 	return (struct commit *)obj;
 }
 
+static struct commit *pick_regular_commit(struct commit *pickme,
+					  struct commit *last_commit,
+					  struct merge_options *merge_opt,
+					  struct merge_result *result)
+{
+	struct commit *base;
+	struct tree *pickme_tree, *base_tree;
+
+	base = pickme->parents->item;
+
+	pickme_tree = repo_get_commit_tree(the_repository, pickme);
+	base_tree = repo_get_commit_tree(the_repository, base);
+
+	merge_opt->branch2 = short_commit_name(pickme);
+	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
+
+	merge_incore_nonrecursive(merge_opt,
+				  base_tree,
+				  result->tree,
+				  pickme_tree,
+				  result);
+
+	free((char*)merge_opt->ancestor);
+	merge_opt->ancestor = NULL;
+	if (!result->clean)
+		return NULL;
+	return create_commit(result->tree, pickme, last_commit);
+}
+
 int cmd_replay(int argc, const char **argv, const char *prefix)
 {
 	struct commit *onto;
@@ -98,7 +127,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct rev_info revs;
 	struct commit *commit;
 	struct merge_options merge_opt;
-	struct tree *next_tree, *base_tree, *head_tree;
+	struct tree *head_tree;
 	struct merge_result result;
 	struct strbuf reflog_msg = STRBUF_INIT;
 	struct strbuf branch_name = STRBUF_INIT;
@@ -173,7 +202,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	result.tree = head_tree;
 	last_commit = onto;
 	while ((commit = get_revision(&revs))) {
-		struct commit *base;
+		struct commit *pick;
 
 		fprintf(stderr, "Rebasing %s...\r",
 			oid_to_hex(&commit->object.oid));
@@ -183,26 +212,11 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		if (commit->parents->next)
 			die(_("replaying merge commits is not supported yet!"));
 
-		base = commit->parents->item;
-
-		next_tree = repo_get_commit_tree(the_repository, commit);
-		base_tree = repo_get_commit_tree(the_repository, base);
-
-		merge_opt.branch2 = short_commit_name(commit);
-		merge_opt.ancestor = xstrfmt("parent of %s", merge_opt.branch2);
-
-		merge_incore_nonrecursive(&merge_opt,
-					  base_tree,
-					  result.tree,
-					  next_tree,
-					  &result);
-
-		free((char*)merge_opt.ancestor);
-		merge_opt.ancestor = NULL;
-		if (!result.clean)
+		pick = pick_regular_commit(commit, last_commit, &merge_opt, &result);
+		if (!pick)
 			break;
+		last_commit = pick;
 		last_picked_commit = commit;
-		last_commit = create_commit(result.tree, commit, last_commit);
 	}
 
 	merge_finalize(&merge_opt, &result);
-- 
2.40.1.491.gdff9a222ea


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v2 06/15] replay: don't simplify history
  2023-05-09 17:53 ` [PATCH v2 00/15] " Christian Couder
                     ` (4 preceding siblings ...)
  2023-05-09 17:53   ` [PATCH v2 05/15] replay: introduce pick_regular_commit() Christian Couder
@ 2023-05-09 17:53   ` Christian Couder
  2023-05-09 17:53   ` [PATCH v2 07/15] replay: add an important FIXME comment about gpg signing Christian Couder
                     ` (11 subsequent siblings)
  17 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-05-09 17:53 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

From: Elijah Newren <newren@gmail.com>

Let's set the rev walking options we need after calling
setup_revisions() instead of before. This makes it clearer which options
we need.

Also we don't want history simplification, as we want to deal with all
the commits in the affected range.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 15 ++++++---------
 1 file changed, 6 insertions(+), 9 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index f087d97353..9f6cca2972 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -171,15 +171,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 
 	repo_init_revisions(the_repository, &revs, prefix);
 
-	revs.verbose_header = 1;
-	revs.max_parents = 1;
-	revs.cherry_mark = 1;
-	revs.limited = 1;
-	revs.reverse = 1;
-	revs.right_only = 1;
-	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
-	revs.topo_order = 1;
-
 	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
 
 	if (setup_revisions(rev_walk_args.nr, rev_walk_args.v, &revs, NULL) > 1) {
@@ -187,6 +178,12 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		goto cleanup;
 	}
 
+	/* requirements/overrides for revs */
+	revs.reverse = 1;
+	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
+	revs.topo_order = 1;
+	revs.simplify_history = 0;
+
 	strvec_clear(&rev_walk_args);
 
 	if (prepare_revision_walk(&revs) < 0) {
-- 
2.40.1.491.gdff9a222ea


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v2 07/15] replay: add an important FIXME comment about gpg signing
  2023-05-09 17:53 ` [PATCH v2 00/15] " Christian Couder
                     ` (5 preceding siblings ...)
  2023-05-09 17:53   ` [PATCH v2 06/15] replay: don't simplify history Christian Couder
@ 2023-05-09 17:53   ` Christian Couder
  2023-05-09 17:53   ` [PATCH v2 08/15] replay: remove progress and info output Christian Couder
                     ` (10 subsequent siblings)
  17 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-05-09 17:53 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

From: Elijah Newren <newren@gmail.com>

We want to be able to handle signed commits in some way in the future,
but we are not ready to do it now. So for the time being let's just add
a FIXME comment to remind us about it.

These are different ways we could handle them:

  - in case of a cli user and if there was an interactive mode, we could
    perhaps ask if the user wants to sign again
  - we could add an option to just fail if there are signed commits

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 9f6cca2972..a9d88ac6df 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -60,7 +60,7 @@ static struct commit *create_commit(struct tree *tree,
 	struct object *obj;
 	struct commit_list *parents = NULL;
 	char *author;
-	char *sign_commit = NULL;
+	char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
 	struct commit_extra_header *extra;
 	struct strbuf msg = STRBUF_INIT;
 	const char *out_enc = get_commit_output_encoding();
-- 
2.40.1.491.gdff9a222ea


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v2 08/15] replay: remove progress and info output
  2023-05-09 17:53 ` [PATCH v2 00/15] " Christian Couder
                     ` (6 preceding siblings ...)
  2023-05-09 17:53   ` [PATCH v2 07/15] replay: add an important FIXME comment about gpg signing Christian Couder
@ 2023-05-09 17:53   ` Christian Couder
  2023-05-09 17:53   ` [PATCH v2 09/15] replay: remove HEAD related sanity check Christian Couder
                     ` (9 subsequent siblings)
  17 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-05-09 17:53 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

From: Elijah Newren <newren@gmail.com>

The replay command will be changed in a follow up commit, so that it
will not update refs directly, but instead it will print on stdout a
list of commands that can be consumed by `git update-ref --stdin`.

We don't want this output to be polluted by its current low value
output, so let's just remove the latter.

In the future, when the command gets an option to update refs by
itself, it will make a lot of sense to display a progress meter, but
we are not there yet.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 7 +------
 1 file changed, 1 insertion(+), 6 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index a9d88ac6df..ccbef6552a 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -193,7 +193,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
-	merge_opt.show_rename_progress = 1;
+	merge_opt.show_rename_progress = 0;
 	merge_opt.branch1 = "HEAD";
 	head_tree = repo_get_commit_tree(the_repository, onto);
 	result.tree = head_tree;
@@ -201,9 +201,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	while ((commit = get_revision(&revs))) {
 		struct commit *pick;
 
-		fprintf(stderr, "Rebasing %s...\r",
-			oid_to_hex(&commit->object.oid));
-
 		if (!commit->parents)
 			die(_("replaying down to root commit is not supported yet!"));
 		if (commit->parents->next)
@@ -222,7 +219,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		exit(128);
 
 	if (result.clean) {
-		fprintf(stderr, "\nDone.\n");
 		strbuf_addf(&reflog_msg, "finish rebase %s onto %s",
 			    oid_to_hex(&last_picked_commit->object.oid),
 			    oid_to_hex(&last_commit->object.oid));
@@ -236,7 +232,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		if (create_symref("HEAD", branch_name.buf, reflog_msg.buf) < 0)
 			die(_("unable to update HEAD"));
 	} else {
-		fprintf(stderr, "\nAborting: Hit a conflict.\n");
 		strbuf_addf(&reflog_msg, "rebase progress up to %s",
 			    oid_to_hex(&last_picked_commit->object.oid));
 		if (update_ref(reflog_msg.buf, "HEAD",
-- 
2.40.1.491.gdff9a222ea


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v2 09/15] replay: remove HEAD related sanity check
  2023-05-09 17:53 ` [PATCH v2 00/15] " Christian Couder
                     ` (7 preceding siblings ...)
  2023-05-09 17:53   ` [PATCH v2 08/15] replay: remove progress and info output Christian Couder
@ 2023-05-09 17:53   ` Christian Couder
  2023-05-09 17:53   ` [PATCH v2 10/15] replay: make it a minimal server side command Christian Couder
                     ` (8 subsequent siblings)
  17 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-05-09 17:53 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

From: Elijah Newren <newren@gmail.com>

We want replay to be a command that can be used on the server side on
any branch, not just the current one, so we are going to stop updating
HEAD in a future commit.

A "sanity check" that makes sure we are replaying the current branch
doesn't make sense anymore. Let's remove it.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 8 +-------
 1 file changed, 1 insertion(+), 7 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index ccbef6552a..6437633724 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -121,7 +121,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct commit *onto;
 	const char *onto_name = NULL;
 	struct commit *last_commit = NULL, *last_picked_commit = NULL;
-	struct object_id head;
 	struct lock_file lock = LOCK_INIT;
 	struct strvec rev_walk_args = STRVEC_INIT;
 	struct rev_info revs;
@@ -160,11 +159,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	onto = peel_committish(onto_name);
 	strbuf_addf(&branch_name, "refs/heads/%s", argv[2]);
 
-	/* Sanity check */
-	if (repo_get_oid(the_repository, "HEAD", &head))
-		die(_("Cannot read HEAD"));
-	assert(oideq(&onto->object.oid, &head));
-
 	repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
 	if (repo_read_index(the_repository) < 0)
 		BUG("Could not read index");
@@ -236,7 +230,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			    oid_to_hex(&last_picked_commit->object.oid));
 		if (update_ref(reflog_msg.buf, "HEAD",
 			       &last_commit->object.oid,
-			       &head,
+			       &onto->object.oid,
 			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
 			error(_("could not update %s"), argv[2]);
 			die("Failed to update %s", argv[2]);
-- 
2.40.1.491.gdff9a222ea


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v2 10/15] replay: make it a minimal server side command
  2023-05-09 17:53 ` [PATCH v2 00/15] " Christian Couder
                     ` (8 preceding siblings ...)
  2023-05-09 17:53   ` [PATCH v2 09/15] replay: remove HEAD related sanity check Christian Couder
@ 2023-05-09 17:53   ` Christian Couder
  2023-05-09 17:53   ` [PATCH v2 11/15] replay: use standard revision ranges Christian Couder
                     ` (7 subsequent siblings)
  17 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-05-09 17:53 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

From: Elijah Newren <newren@gmail.com>

We want this command to be a minimal command that just does server side
picking of commits, displaying the results on stdout for higher level
scripts to consume.

So let's simplify it:
  * remove the worktree and index reading/writing,
  * remove the ref (and reflog) updating,
  * remove the assumptions tying us to HEAD, since (a) this is not a
    rebase and (b) we want to be able to pick commits in a bare repo,
    i.e. to/from branches that are not checked out and not the main
    branch,
  * remove unneeded includes,
  * handle rebasing multiple branches by printing on stdout the update
    ref commands that should be performed.

The output can be piped into `git update-ref --stdin` for the ref
updates to happen.

In the future to make it easier for users to use this command
directly maybe an option can be added to automatically pipe its output
into `git update-ref`.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c                         | 79 ++++++++----------------
 t/t6429-merge-sequence-rename-caching.sh | 39 +++++++-----
 2 files changed, 51 insertions(+), 67 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 6437633724..46f00a2d71 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -6,10 +6,7 @@
 #include "git-compat-util.h"
 
 #include "builtin.h"
-#include "cache-tree.h"
-#include "commit.h"
 #include "environment.h"
-#include "gettext.h"
 #include "hex.h"
 #include "lockfile.h"
 #include "merge-ort.h"
@@ -17,10 +14,7 @@
 #include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
-#include "sequencer.h"
-#include "setup.h"
 #include "strvec.h"
-#include "tree.h"
 
 static const char *short_commit_name(struct commit *commit)
 {
@@ -100,6 +94,7 @@ static struct commit *pick_regular_commit(struct commit *pickme,
 	pickme_tree = repo_get_commit_tree(the_repository, pickme);
 	base_tree = repo_get_commit_tree(the_repository, base);
 
+	merge_opt->branch1 = short_commit_name(last_commit);
 	merge_opt->branch2 = short_commit_name(pickme);
 	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
 
@@ -120,15 +115,12 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 {
 	struct commit *onto;
 	const char *onto_name = NULL;
-	struct commit *last_commit = NULL, *last_picked_commit = NULL;
-	struct lock_file lock = LOCK_INIT;
+	struct commit *last_commit = NULL;
 	struct strvec rev_walk_args = STRVEC_INIT;
 	struct rev_info revs;
 	struct commit *commit;
 	struct merge_options merge_opt;
-	struct tree *head_tree;
 	struct merge_result result;
-	struct strbuf reflog_msg = STRBUF_INIT;
 	struct strbuf branch_name = STRBUF_INIT;
 	int ret = 0;
 
@@ -159,10 +151,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	onto = peel_committish(onto_name);
 	strbuf_addf(&branch_name, "refs/heads/%s", argv[2]);
 
-	repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
-	if (repo_read_index(the_repository) < 0)
-		BUG("Could not read index");
-
 	repo_init_revisions(the_repository, &revs, prefix);
 
 	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
@@ -188,58 +176,45 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
 	merge_opt.show_rename_progress = 0;
-	merge_opt.branch1 = "HEAD";
-	head_tree = repo_get_commit_tree(the_repository, onto);
-	result.tree = head_tree;
+	result.tree = repo_get_commit_tree(the_repository, onto);
 	last_commit = onto;
 	while ((commit = get_revision(&revs))) {
-		struct commit *pick;
+		const struct name_decoration *decoration;
 
 		if (!commit->parents)
 			die(_("replaying down to root commit is not supported yet!"));
 		if (commit->parents->next)
 			die(_("replaying merge commits is not supported yet!"));
 
-		pick = pick_regular_commit(commit, last_commit, &merge_opt, &result);
-		if (!pick)
+		last_commit = pick_regular_commit(commit, last_commit, &merge_opt, &result);
+		if (!last_commit)
 			break;
-		last_commit = pick;
-		last_picked_commit = commit;
+
+		decoration = get_name_decoration(&commit->object);
+		if (!decoration)
+			continue;
+
+		while (decoration) {
+			if (decoration->type == DECORATION_REF_LOCAL) {
+				printf("update %s %s %s\n",
+				       decoration->name,
+				       oid_to_hex(&last_commit->object.oid),
+				       oid_to_hex(&commit->object.oid));
+			}
+			decoration = decoration->next;
+		}
 	}
 
+	/* Cleanup */
 	merge_finalize(&merge_opt, &result);
+	ret = result.clean;
 
-	if (result.clean < 0)
-		exit(128);
-
-	if (result.clean) {
-		strbuf_addf(&reflog_msg, "finish rebase %s onto %s",
-			    oid_to_hex(&last_picked_commit->object.oid),
-			    oid_to_hex(&last_commit->object.oid));
-		if (update_ref(reflog_msg.buf, branch_name.buf,
-			       &last_commit->object.oid,
-			       &last_picked_commit->object.oid,
-			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
-			error(_("could not update %s"), argv[2]);
-			die("Failed to update %s", argv[2]);
-		}
-		if (create_symref("HEAD", branch_name.buf, reflog_msg.buf) < 0)
-			die(_("unable to update HEAD"));
-	} else {
-		strbuf_addf(&reflog_msg, "rebase progress up to %s",
-			    oid_to_hex(&last_picked_commit->object.oid));
-		if (update_ref(reflog_msg.buf, "HEAD",
-			       &last_commit->object.oid,
-			       &onto->object.oid,
-			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
-			error(_("could not update %s"), argv[2]);
-			die("Failed to update %s", argv[2]);
-		}
-	}
-	ret = (result.clean == 0);
 cleanup:
-	strbuf_release(&reflog_msg);
 	strbuf_release(&branch_name);
 	release_revisions(&revs);
-	return ret;
+
+	/* Return */
+	if (ret < 0)
+		exit(128);
+	return ret ? 0 : 1;
 }
diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh
index 7670b72008..099aefeffc 100755
--- a/t/t6429-merge-sequence-rename-caching.sh
+++ b/t/t6429-merge-sequence-rename-caching.sh
@@ -71,8 +71,9 @@ test_expect_success 'caching renames does not preclude finding new ones' '
 
 		git switch upstream &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		git ls-files >tracked-files &&
 		test_line_count = 2 tracked-files &&
@@ -140,7 +141,9 @@ test_expect_success 'cherry-pick both a commit and its immediate revert' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 1 calls
@@ -198,8 +201,9 @@ test_expect_success 'rename same file identically, then reintroduce it' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		git ls-files >tracked &&
 		test_line_count = 2 tracked &&
@@ -275,8 +279,9 @@ test_expect_success 'rename same file identically, then add file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		git ls-files >tracked &&
 		test_line_count = 4 tracked &&
@@ -451,8 +456,9 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls &&
@@ -517,8 +523,9 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 3 calls &&
@@ -619,8 +626,9 @@ test_expect_success 'caching renames only on upstream side, part 1' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 1 calls &&
@@ -677,8 +685,9 @@ test_expect_success 'caching renames only on upstream side, part 2' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls &&
-- 
2.40.1.491.gdff9a222ea


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v2 11/15] replay: use standard revision ranges
  2023-05-09 17:53 ` [PATCH v2 00/15] " Christian Couder
                     ` (9 preceding siblings ...)
  2023-05-09 17:53   ` [PATCH v2 10/15] replay: make it a minimal server side command Christian Couder
@ 2023-05-09 17:53   ` Christian Couder
  2023-05-09 17:53   ` [PATCH v2 12/15] replay: disallow revision specific options and pathspecs Christian Couder
                     ` (6 subsequent siblings)
  17 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-05-09 17:53 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

From: Elijah Newren <newren@gmail.com>

Instead of the fixed "<oldbase> <branch>" arguments, the replay
command now accepts "<revision-range>..." arguments in a similar
way as many other Git commands. This makes its interface more
standard and more flexible.

Also as the interface of the command is now mostly finalized,
we can add some documentation as well as testcases to make sure
the command will continue to work as designed in the future.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 Documentation/git-replay.txt             | 88 ++++++++++++++++++++++++
 builtin/replay.c                         | 21 ++----
 t/t3650-replay-basics.sh                 | 83 ++++++++++++++++++++++
 t/t6429-merge-sequence-rename-caching.sh | 18 ++---
 4 files changed, 184 insertions(+), 26 deletions(-)
 create mode 100644 Documentation/git-replay.txt
 create mode 100755 t/t3650-replay-basics.sh

diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
new file mode 100644
index 0000000000..394d7b0050
--- /dev/null
+++ b/Documentation/git-replay.txt
@@ -0,0 +1,88 @@
+git-replay(1)
+=============
+
+NAME
+----
+git-replay - Replay commits on a different base, without touching working tree
+
+
+SYNOPSIS
+--------
+[verse]
+'git replay' --onto <newbase> <revision-range>...
+
+DESCRIPTION
+-----------
+
+Takes a range of commits, and replays them onto a new location.  Does
+not touch the working tree or index, and does not update any
+references.  However, the output of this command is meant to be used
+as input to `git update-ref --stdin`, which would update the relevant
+branches.
+
+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
+
+OPTIONS
+-------
+
+--onto <newbase>::
+	Starting point at which to create the new commits.  May be any
+	valid commit, and not just an existing branch name.
++
+The update-ref commands in the output will update the branch(es)
+in the revision range to point at the new commits (in other
+words, this mimics a rebase operation).
+
+<revision-range>::
+	Range of commits to replay; see "Specifying Ranges" in
+	linkgit:git-rev-parse.
+
+OUTPUT
+------
+
+When there are no conflicts, the output of this command is usable as
+input to `git update-ref --stdin`.  It is basically of the form:
+
+	update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
+	update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
+	update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
+
+where the number of refs updated depend on the arguments passed.
+
+EXIT STATUS
+-----------
+
+For a successful, non-conflicted replay, the exit status is 0.  When
+the replay has conflicts, the exit status is 1.  If the replay is not
+able to complete (or start) due to some kind of error, the exit status
+is something other than 0 or 1.
+
+EXAMPLES
+--------
+
+To simply rebase mybranch onto target:
+
+------------
+$ git replay --onto target origin/main..mybranch
+update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
+------------
+
+When calling `git replay`, one does not need to specify a range of
+commits to replay using the syntax `A..B`; any range expression will
+do:
+
+------------
+$ git replay --onto origin/main ^base branch1 branch2 branch3
+update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
+update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
+update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
+------------
+
+This will simultaneously rebase branch1, branch2, and branch3 -- all
+commits they have since base, playing them on top of origin/main.
+These three branches may have commits on top of base that they have in
+common, but that does not need to be the case.
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/builtin/replay.c b/builtin/replay.c
index 46f00a2d71..c855e1a128 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -14,7 +14,6 @@
 #include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
-#include "strvec.h"
 
 static const char *short_commit_name(struct commit *commit)
 {
@@ -116,16 +115,14 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct commit *onto;
 	const char *onto_name = NULL;
 	struct commit *last_commit = NULL;
-	struct strvec rev_walk_args = STRVEC_INIT;
 	struct rev_info revs;
 	struct commit *commit;
 	struct merge_options merge_opt;
 	struct merge_result result;
-	struct strbuf branch_name = STRBUF_INIT;
 	int ret = 0;
 
 	const char * const replay_usage[] = {
-		N_("git replay --onto <newbase> <oldbase> <branch>"),
+		N_("git replay --onto <newbase> <revision-range>..."),
 		NULL
 	};
 	struct option replay_options[] = {
@@ -143,20 +140,13 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		usage_with_options(replay_usage, replay_options);
 	}
 
-	if (argc != 3) {
-		error(_("bad number of arguments"));
-		usage_with_options(replay_usage, replay_options);
-	}
-
 	onto = peel_committish(onto_name);
-	strbuf_addf(&branch_name, "refs/heads/%s", argv[2]);
 
 	repo_init_revisions(the_repository, &revs, prefix);
 
-	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
-
-	if (setup_revisions(rev_walk_args.nr, rev_walk_args.v, &revs, NULL) > 1) {
-		ret = error(_("unhandled options"));
+	argc = setup_revisions(argc, argv, &revs, NULL);
+	if (argc > 1) {
+		ret = error(_("unrecognized argument: %s"), argv[1]);
 		goto cleanup;
 	}
 
@@ -166,8 +156,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	revs.topo_order = 1;
 	revs.simplify_history = 0;
 
-	strvec_clear(&rev_walk_args);
-
 	if (prepare_revision_walk(&revs) < 0) {
 		ret = error(_("error preparing revisions"));
 		goto cleanup;
@@ -210,7 +198,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	ret = result.clean;
 
 cleanup:
-	strbuf_release(&branch_name);
 	release_revisions(&revs);
 
 	/* Return */
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
new file mode 100755
index 0000000000..a1da4f9ef9
--- /dev/null
+++ b/t/t3650-replay-basics.sh
@@ -0,0 +1,83 @@
+#!/bin/sh
+
+test_description='basic git replay tests'
+
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
+. ./test-lib.sh
+
+GIT_AUTHOR_NAME=author@name
+GIT_AUTHOR_EMAIL=bogus@email@address
+export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL
+
+test_expect_success 'setup' '
+	test_commit A &&
+	test_commit B &&
+
+	git switch -c topic1 &&
+	test_commit C &&
+	git switch -c topic2 &&
+	test_commit D &&
+	test_commit E &&
+	git switch topic1 &&
+	test_commit F &&
+	git switch -c topic3 &&
+	test_commit G &&
+	test_commit H &&
+	git switch -c topic4 main &&
+	test_commit I &&
+	test_commit J &&
+
+	git switch -c next main &&
+	test_commit K &&
+	git merge -m "Merge topic1" topic1 &&
+	git merge -m "Merge topic2" topic2 &&
+	git merge -m "Merge topic3" topic3 &&
+	>evil &&
+	git add evil &&
+	git commit --amend &&
+	git merge -m "Merge topic4" topic4 &&
+
+	git switch main &&
+	test_commit L &&
+	test_commit M &&
+
+	git switch -c conflict B &&
+	test_commit C.conflict C.t conflict
+'
+
+test_expect_success 'setup bare' '
+	git clone --bare . bare
+'
+
+test_expect_success 'using replay to rebase two branches, one on top of other' '
+	git replay --onto main topic1..topic2 >result &&
+
+	test_line_count = 1 result &&
+
+	git log --format=%s $(cut -f 3 -d " " result) >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/topic2 " >expect &&
+	printf "%s " $(cut -f 3 -d " " result) >>expect &&
+	git rev-parse topic2 >>expect &&
+
+	test_cmp expect result
+'
+
+test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '
+	git -C bare replay --onto main topic1..topic2 >result-bare &&
+	test_cmp expect result-bare
+'
+
+test_expect_success 'using replay to rebase with a conflict' '
+	test_expect_code 1 git replay --onto topic1 B..conflict
+'
+
+test_expect_success 'using replay on bare repo to rebase with a conflict' '
+	test_expect_code 1 git -C bare replay --onto topic1 B..conflict
+'
+
+test_done
diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh
index 099aefeffc..0f39ed0d08 100755
--- a/t/t6429-merge-sequence-rename-caching.sh
+++ b/t/t6429-merge-sequence-rename-caching.sh
@@ -71,7 +71,7 @@ test_expect_success 'caching renames does not preclude finding new ones' '
 
 		git switch upstream &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -141,7 +141,7 @@ test_expect_success 'cherry-pick both a commit and its immediate revert' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -201,7 +201,7 @@ test_expect_success 'rename same file identically, then reintroduce it' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -279,7 +279,7 @@ test_expect_success 'rename same file identically, then add file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -357,7 +357,7 @@ test_expect_success 'cached dir rename does not prevent noticing later conflict'
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test_must_fail git replay --onto HEAD upstream~1 topic >output &&
+		test_must_fail git replay --onto HEAD upstream~1..topic >output &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls
@@ -456,7 +456,7 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -523,7 +523,7 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -626,7 +626,7 @@ test_expect_success 'caching renames only on upstream side, part 1' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -685,7 +685,7 @@ test_expect_success 'caching renames only on upstream side, part 2' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
-- 
2.40.1.491.gdff9a222ea


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v2 12/15] replay: disallow revision specific options and pathspecs
  2023-05-09 17:53 ` [PATCH v2 00/15] " Christian Couder
                     ` (10 preceding siblings ...)
  2023-05-09 17:53   ` [PATCH v2 11/15] replay: use standard revision ranges Christian Couder
@ 2023-05-09 17:53   ` Christian Couder
  2023-05-16  4:25     ` Elijah Newren
  2023-05-09 17:53   ` [PATCH v2 13/15] replay: add --advance or 'cherry-pick' mode Christian Couder
                     ` (5 subsequent siblings)
  17 siblings, 1 reply; 208+ messages in thread
From: Christian Couder @ 2023-05-09 17:53 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder, Christian Couder

A previous commit changed `git replay` to make it accept standard
revision ranges using the setup_revisions() function. While this is good
thing to make this command more standard and more flexible, it has the
downside of enabling many revision related options accepted and eaten by
setup_revisions().

Some of these options might make sense, but others, like those
generating non-contiguous history, might not. Anyway those we might want
to allow should probably be tested and perhaps documented a bit, which
could be done in future work.

For now it is just simpler and safer to just disallow all of them, so
let's do that.

Other commands, like `git fast-export`, currently allow all these
revision specific options even though some of them might not make sense,
as these commands also use setup_revisions() but do not check the
options that might be passed to this function.

So a way to fix those commands as well as git replay could be to improve
or refactor the setup_revisions() mechanism to let callers allow and
disallow options in a relevant way for them. Such improvements are
outside the scope of this work though.

Pathspecs, which are also accepted and eaten by setup_revisions(), are
likely to result in disconnected history. That could perhaps be useful,
but that would need tests and documentation, which can be added in
future work. So, while at it, let's disallow them too.

Helped-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c         | 27 ++++++++++++++++++++++++++-
 t/t3650-replay-basics.sh | 16 ++++++++++++++++
 2 files changed, 42 insertions(+), 1 deletion(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index c855e1a128..7b3c0f2bc4 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -119,7 +119,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct commit *commit;
 	struct merge_options merge_opt;
 	struct merge_result result;
-	int ret = 0;
+	int ret = 0, i;
 
 	const char * const replay_usage[] = {
 		N_("git replay --onto <newbase> <revision-range>..."),
@@ -135,6 +135,20 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
 			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
 
+	/*
+	 * TODO: For now, we reject any unknown or invalid option,
+	 * including revision related ones, like --not,
+	 * --first-parent, etc that would be allowed and eaten by
+	 * setup_revisions() below. In the future we should definitely
+	 * accept those that make sense and add related tests and doc
+	 * though.
+	 */
+	for (i = 0; i < argc; i++)
+		if (argv[i][0] == '-') {
+			error(_("invalid option: %s"), argv[i]);
+			usage_with_options(replay_usage, replay_options);
+		}
+
 	if (!onto_name) {
 		error(_("option --onto is mandatory"));
 		usage_with_options(replay_usage, replay_options);
@@ -150,6 +164,17 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		goto cleanup;
 	}
 
+	/*
+	 * TODO: For now, we reject any pathspec. (They are allowed
+	 * and eaten by setup_revisions() above.) In the future we
+	 * should definitely accept them and add related tests and doc
+	 * though.
+	 */
+	if (revs.prune_data.nr) {
+		error(_("invalid pathspec: %s"), revs.prune_data.items[0].match);
+		usage_with_options(replay_usage, replay_options);
+	}
+
 	/* requirements/overrides for revs */
 	revs.reverse = 1;
 	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index a1da4f9ef9..de6e40950e 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -80,4 +80,20 @@ test_expect_success 'using replay on bare repo to rebase with a conflict' '
 	test_expect_code 1 git -C bare replay --onto topic1 B..conflict
 '
 
+test_expect_success 'using replay with (for now) disallowed revision specific option --not' '
+	test_must_fail git replay --onto main topic2 --not topic1
+'
+
+test_expect_success 'using replay on bare repo with (for now) disallowed revision specific option --first-parent' '
+	test_must_fail git -C bare replay --onto main --first-parent topic1..topic2
+'
+
+test_expect_success 'using replay with disallowed pathspec' '
+	test_must_fail git replay --onto main topic1..topic2 A.t
+'
+
+test_expect_success 'using replay on bare repo with disallowed pathspec' '
+	test_must_fail git -C bare replay --onto main topic1..topic2 -- A.t
+'
+
 test_done
-- 
2.40.1.491.gdff9a222ea


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v2 13/15] replay: add --advance or 'cherry-pick' mode
  2023-05-09 17:53 ` [PATCH v2 00/15] " Christian Couder
                     ` (11 preceding siblings ...)
  2023-05-09 17:53   ` [PATCH v2 12/15] replay: disallow revision specific options and pathspecs Christian Couder
@ 2023-05-09 17:53   ` Christian Couder
  2023-05-09 17:53   ` [PATCH v2 14/15] replay: add --contained to rebase contained branches Christian Couder
                     ` (4 subsequent siblings)
  17 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-05-09 17:53 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

From: Elijah Newren <newren@gmail.com>

There is already a 'rebase' mode with `--onto`. Let's add an 'advance' or
'cherry-pick' mode with `--advance`. This new mode will make the target
branch advance as we replay commits onto it.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 Documentation/git-replay.txt |  33 ++++++-
 builtin/replay.c             | 185 +++++++++++++++++++++++++++++++++--
 t/t3650-replay-basics.sh     |  26 +++++
 3 files changed, 230 insertions(+), 14 deletions(-)

diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
index 394d7b0050..439b2f92e7 100644
--- a/Documentation/git-replay.txt
+++ b/Documentation/git-replay.txt
@@ -9,7 +9,7 @@ git-replay - Replay commits on a different base, without touching working tree
 SYNOPSIS
 --------
 [verse]
-'git replay' --onto <newbase> <revision-range>...
+'git replay' (--onto <newbase> | --advance <branch>) <revision-range>...
 
 DESCRIPTION
 -----------
@@ -29,9 +29,17 @@ OPTIONS
 	Starting point at which to create the new commits.  May be any
 	valid commit, and not just an existing branch name.
 +
-The update-ref commands in the output will update the branch(es)
-in the revision range to point at the new commits (in other
-words, this mimics a rebase operation).
+When `--onto` is specified, the update-ref command(s) in the output will
+update the branch(es) in the revision range to point at the new
+commits (in other words, this mimics a rebase operation).
+
+--advance <branch>::
+	Starting point at which to create the new commits; must be a
+	branch name.
++
+When `--advance` is specified, the update-ref command(s) in the output
+will update the branch passed as an argument to `--advance` to point at
+the new commits (in other words, this mimics a cherry-pick operation).
 
 <revision-range>::
 	Range of commits to replay; see "Specifying Ranges" in
@@ -47,7 +55,10 @@ input to `git update-ref --stdin`.  It is basically of the form:
 	update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
 	update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
 
-where the number of refs updated depend on the arguments passed.
+where the number of refs updated depend on the arguments passed.  When
+using `--advance`, the number of refs updated is always one, but for
+`--onto`, it can be one or more (rebasing multiple branches
+simultaneously is supported).
 
 EXIT STATUS
 -----------
@@ -67,6 +78,18 @@ $ git replay --onto target origin/main..mybranch
 update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
 ------------
 
+To cherry-pick the commits from mybranch onto target:
+
+------------
+$ git replay --advance target origin/main..mybranch
+update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}
+------------
+
+Note that the first two examples replay the exact same commits and on
+top of the exact same new base, they only differ in that the first
+provides instructions to make mybranch point at the new commits and
+the second provides instructions to make target point at them.
+
 When calling `git replay`, one does not need to specify a range of
 commits to replay using the syntax `A..B`; any range expression will
 do:
diff --git a/builtin/replay.c b/builtin/replay.c
index 7b3c0f2bc4..c146f38f58 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -14,6 +14,7 @@
 #include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
+#include "strmap.h"
 
 static const char *short_commit_name(struct commit *commit)
 {
@@ -80,6 +81,146 @@ static struct commit *create_commit(struct tree *tree,
 	return (struct commit *)obj;
 }
 
+struct ref_info {
+	struct commit *onto;
+	struct strset positive_refs;
+	struct strset negative_refs;
+	int positive_refexprs;
+	int negative_refexprs;
+};
+
+static void get_ref_information(struct rev_cmdline_info *cmd_info,
+				struct ref_info *ref_info)
+{
+	int i;
+
+	ref_info->onto = NULL;
+	strset_init(&ref_info->positive_refs);
+	strset_init(&ref_info->negative_refs);
+	ref_info->positive_refexprs = 0;
+	ref_info->negative_refexprs = 0;
+
+	/*
+	 * When the user specifies e.g.
+	 *   git replay origin/main..mybranch
+	 *   git replay ^origin/next mybranch1 mybranch2
+	 * we want to be able to determine where to replay the commits.  In
+	 * these examples, the branches are probably based on an old version
+	 * of either origin/main or origin/next, so we want to replay on the
+	 * newest version of that branch.  In contrast we would want to error
+	 * out if they ran
+	 *   git replay ^origin/master ^origin/next mybranch
+	 *   git replay mybranch~2..mybranch
+	 * the first of those because there's no unique base to choose, and
+	 * the second because they'd likely just be replaying commits on top
+	 * of the same commit and not making any difference.
+	 */
+	for (i = 0; i < cmd_info->nr; i++) {
+		struct rev_cmdline_entry *e = cmd_info->rev + i;
+		struct object_id oid;
+		const char *refexpr = e->name;
+		char *fullname = NULL;
+		int can_uniquely_dwim = 1;
+
+		if (*refexpr == '^')
+			refexpr++;
+		if (repo_dwim_ref(the_repository, refexpr, strlen(refexpr), &oid, &fullname, 0) != 1)
+			can_uniquely_dwim = 0;
+
+		if (e->flags & BOTTOM) {
+			if (can_uniquely_dwim)
+				strset_add(&ref_info->negative_refs, fullname);
+			if (!ref_info->negative_refexprs)
+				ref_info->onto = lookup_commit_reference_gently(the_repository,
+										&e->item->oid, 1);
+			ref_info->negative_refexprs++;
+		} else {
+			if (can_uniquely_dwim)
+				strset_add(&ref_info->positive_refs, fullname);
+			ref_info->positive_refexprs++;
+		}
+
+		free(fullname);
+	}
+}
+
+static void determine_replay_mode(struct rev_cmdline_info *cmd_info,
+				  const char *onto_name,
+				  const char **advance_name,
+				  struct commit **onto,
+				  struct strset **update_refs)
+{
+	struct ref_info rinfo;
+
+	get_ref_information(cmd_info, &rinfo);
+	if (!rinfo.positive_refexprs)
+		die(_("need some commits to replay"));
+	if (onto_name && *advance_name)
+		die(_("--onto and --advance are incompatible"));
+	else if (onto_name) {
+		*onto = peel_committish(onto_name);
+		if (rinfo.positive_refexprs <
+		    strset_get_size(&rinfo.positive_refs))
+			die(_("all positive revisions given must be references"));
+	} else if (*advance_name) {
+		struct object_id oid;
+		char *fullname = NULL;
+
+		*onto = peel_committish(*advance_name);
+		if (repo_dwim_ref(the_repository, *advance_name, strlen(*advance_name),
+			     &oid, &fullname, 0) == 1) {
+			*advance_name = fullname;
+		} else {
+			die(_("argument to --advance must be a reference"));
+		}
+		if (rinfo.positive_refexprs > 1)
+			die(_("cannot advance target with multiple source branches because ordering would be ill-defined"));
+	} else {
+		int positive_refs_complete = (
+			rinfo.positive_refexprs ==
+			strset_get_size(&rinfo.positive_refs));
+		int negative_refs_complete = (
+			rinfo.negative_refexprs ==
+			strset_get_size(&rinfo.negative_refs));
+		/*
+		 * We need either positive_refs_complete or
+		 * negative_refs_complete, but not both.
+		 */
+		if (rinfo.negative_refexprs > 0 &&
+		    positive_refs_complete == negative_refs_complete)
+			die(_("cannot implicitly determine whether this is an --advance or --onto operation"));
+		if (negative_refs_complete) {
+			struct hashmap_iter iter;
+			struct strmap_entry *entry;
+
+			if (rinfo.negative_refexprs == 0)
+				die(_("all positive revisions given must be references"));
+			else if (rinfo.negative_refexprs > 1)
+				die(_("cannot implicitly determine whether this is an --advance or --onto operation"));
+			else if (rinfo.positive_refexprs > 1)
+				die(_("cannot advance target with multiple source branches because ordering would be ill-defined"));
+
+			/* Only one entry, but we have to loop to get it */
+			strset_for_each_entry(&rinfo.negative_refs,
+					      &iter, entry) {
+				*advance_name = entry->key;
+			}
+		} else { /* positive_refs_complete */
+			if (rinfo.negative_refexprs > 1)
+				die(_("cannot implicitly determine correct base for --onto"));
+			if (rinfo.negative_refexprs == 1)
+				*onto = rinfo.onto;
+		}
+	}
+	if (!*advance_name) {
+		*update_refs = xcalloc(1, sizeof(**update_refs));
+		**update_refs = rinfo.positive_refs;
+		memset(&rinfo.positive_refs, 0, sizeof(**update_refs));
+	}
+	strset_clear(&rinfo.negative_refs);
+	strset_clear(&rinfo.positive_refs);
+}
+
 static struct commit *pick_regular_commit(struct commit *pickme,
 					  struct commit *last_commit,
 					  struct merge_options *merge_opt,
@@ -112,20 +253,26 @@ static struct commit *pick_regular_commit(struct commit *pickme,
 
 int cmd_replay(int argc, const char **argv, const char *prefix)
 {
-	struct commit *onto;
+	const char *advance_name = NULL;
+	struct commit *onto = NULL;
 	const char *onto_name = NULL;
-	struct commit *last_commit = NULL;
+
 	struct rev_info revs;
+	struct commit *last_commit = NULL;
 	struct commit *commit;
 	struct merge_options merge_opt;
 	struct merge_result result;
+	struct strset *update_refs = NULL;
 	int ret = 0, i;
 
 	const char * const replay_usage[] = {
-		N_("git replay --onto <newbase> <revision-range>..."),
+		N_("git replay (--onto <newbase> | --advance <branch>) <revision-range>..."),
 		NULL
 	};
 	struct option replay_options[] = {
+		OPT_STRING(0, "advance", &advance_name,
+			   N_("branch"),
+			   N_("make replay advance given branch")),
 		OPT_STRING(0, "onto", &onto_name,
 			   N_("revision"),
 			   N_("replay onto given commit")),
@@ -149,13 +296,11 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			usage_with_options(replay_usage, replay_options);
 		}
 
-	if (!onto_name) {
-		error(_("option --onto is mandatory"));
+	if (!onto_name && !advance_name) {
+		error(_("option --onto or --advance is mandatory"));
 		usage_with_options(replay_usage, replay_options);
 	}
 
-	onto = peel_committish(onto_name);
-
 	repo_init_revisions(the_repository, &revs, prefix);
 
 	argc = setup_revisions(argc, argv, &revs, NULL);
@@ -181,6 +326,12 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	revs.topo_order = 1;
 	revs.simplify_history = 0;
 
+	determine_replay_mode(&revs.cmdline, onto_name, &advance_name,
+			      &onto, &update_refs);
+
+	if (!onto) /* FIXME: Should handle replaying down to root commit */
+		die("Replaying down to root commit is not supported yet!");
+
 	if (prepare_revision_walk(&revs) < 0) {
 		ret = error(_("error preparing revisions"));
 		goto cleanup;
@@ -189,6 +340,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
 	merge_opt.show_rename_progress = 0;
+
 	result.tree = repo_get_commit_tree(the_repository, onto);
 	last_commit = onto;
 	while ((commit = get_revision(&revs))) {
@@ -203,12 +355,15 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		if (!last_commit)
 			break;
 
+		/* Update any necessary branches */
+		if (advance_name)
+			continue;
 		decoration = get_name_decoration(&commit->object);
 		if (!decoration)
 			continue;
-
 		while (decoration) {
-			if (decoration->type == DECORATION_REF_LOCAL) {
+			if (decoration->type == DECORATION_REF_LOCAL &&
+			    strset_contains(update_refs, decoration->name)) {
 				printf("update %s %s %s\n",
 				       decoration->name,
 				       oid_to_hex(&last_commit->object.oid),
@@ -218,11 +373,23 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		}
 	}
 
+	/* In --advance mode, advance the target ref */
+	if (result.clean == 1 && advance_name) {
+		printf("update %s %s %s\n",
+		       advance_name,
+		       oid_to_hex(&last_commit->object.oid),
+		       oid_to_hex(&onto->object.oid));
+	}
+
 	/* Cleanup */
 	merge_finalize(&merge_opt, &result);
 	ret = result.clean;
 
 cleanup:
+	if (update_refs) {
+		strset_clear(update_refs);
+		free(update_refs);
+	}
 	release_revisions(&revs);
 
 	/* Return */
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index de6e40950e..bca405c431 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -96,4 +96,30 @@ test_expect_success 'using replay on bare repo with disallowed pathspec' '
 	test_must_fail git -C bare replay --onto main topic1..topic2 -- A.t
 '
 
+test_expect_success 'using replay to perform basic cherry-pick' '
+	# The differences between this test and previous ones are:
+	#   --advance vs --onto
+	# 2nd field of result is refs/heads/main vs. refs/heads/topic2
+	# 4th field of result is hash for main instead of hash for topic2
+
+	git replay --advance main topic1..topic2 >result &&
+
+	test_line_count = 1 result &&
+
+	git log --format=%s $(cut -f 3 -d " " result) >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/main " >expect &&
+	printf "%s " $(cut -f 3 -d " " result) >>expect &&
+	git rev-parse main >>expect &&
+
+	test_cmp expect result
+'
+
+test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
+	git -C bare replay --advance main topic1..topic2 >result-bare &&
+	test_cmp expect result-bare
+'
+
 test_done
-- 
2.40.1.491.gdff9a222ea


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v2 14/15] replay: add --contained to rebase contained branches
  2023-05-09 17:53 ` [PATCH v2 00/15] " Christian Couder
                     ` (12 preceding siblings ...)
  2023-05-09 17:53   ` [PATCH v2 13/15] replay: add --advance or 'cherry-pick' mode Christian Couder
@ 2023-05-09 17:53   ` Christian Couder
  2023-05-16  4:26     ` Elijah Newren
  2023-05-09 17:53   ` [PATCH v2 15/15] replay: stop assuming replayed branches do not diverge Christian Couder
                     ` (3 subsequent siblings)
  17 siblings, 1 reply; 208+ messages in thread
From: Christian Couder @ 2023-05-09 17:53 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

From: Elijah Newren <newren@gmail.com>

Let's add a `--contained` option that can be used along with
`--onto` to rebase all the branches contained in the <revision-range>
argument.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 Documentation/git-replay.txt | 16 +++++++++++++++-
 builtin/replay.c             | 12 ++++++++++--
 t/t3650-replay-basics.sh     | 29 +++++++++++++++++++++++++++++
 3 files changed, 54 insertions(+), 3 deletions(-)

diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
index 439b2f92e7..3e06ab2f5e 100644
--- a/Documentation/git-replay.txt
+++ b/Documentation/git-replay.txt
@@ -9,7 +9,7 @@ git-replay - Replay commits on a different base, without touching working tree
 SYNOPSIS
 --------
 [verse]
-'git replay' (--onto <newbase> | --advance <branch>) <revision-range>...
+'git replay' [--contained] (--onto <newbase> | --advance <branch>) <revision-range>...
 
 DESCRIPTION
 -----------
@@ -90,6 +90,20 @@ top of the exact same new base, they only differ in that the first
 provides instructions to make mybranch point at the new commits and
 the second provides instructions to make target point at them.
 
+What if you have a stack of branches, one depending upon another, and
+you'd really like to rebase the whole set?
+
+------------
+$ git replay --contained --onto origin/main origin/main..tipbranch
+update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
+update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
+update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}
+------------
+
+In contrast, trying to do this with rebase would require 3 separate
+rebases, eacho of which involves a different <ONTO> and <UPSTREAM> and
+forces you to first check out each branch in turn.
+
 When calling `git replay`, one does not need to specify a range of
 commits to replay using the syntax `A..B`; any range expression will
 do:
diff --git a/builtin/replay.c b/builtin/replay.c
index c146f38f58..4d24eb95d8 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -256,6 +256,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	const char *advance_name = NULL;
 	struct commit *onto = NULL;
 	const char *onto_name = NULL;
+	int contained = 0;
 
 	struct rev_info revs;
 	struct commit *last_commit = NULL;
@@ -266,7 +267,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	int ret = 0, i;
 
 	const char * const replay_usage[] = {
-		N_("git replay (--onto <newbase> | --advance <branch>) <revision-range>..."),
+		N_("git replay [--contained] (--onto <newbase> | --advance <branch>) <revision-range>..."),
 		NULL
 	};
 	struct option replay_options[] = {
@@ -276,6 +277,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		OPT_STRING(0, "onto", &onto_name,
 			   N_("revision"),
 			   N_("replay onto given commit")),
+		OPT_BOOL(0, "contained", &contained,
+			 N_("advance all branches contained in revision-range")),
 		OPT_END()
 	};
 
@@ -301,6 +304,10 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		usage_with_options(replay_usage, replay_options);
 	}
 
+	if (advance_name && contained)
+		die(_("options '%s' and '%s' cannot be used together"),
+		    "--advance", "--contained");
+
 	repo_init_revisions(the_repository, &revs, prefix);
 
 	argc = setup_revisions(argc, argv, &revs, NULL);
@@ -363,7 +370,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			continue;
 		while (decoration) {
 			if (decoration->type == DECORATION_REF_LOCAL &&
-			    strset_contains(update_refs, decoration->name)) {
+			    (contained || strset_contains(update_refs,
+							  decoration->name))) {
 				printf("update %s %s %s\n",
 				       decoration->name,
 				       oid_to_hex(&last_commit->object.oid),
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index bca405c431..3fb4167e69 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -122,4 +122,33 @@ test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
 	test_cmp expect result-bare
 '
 
+test_expect_success 'using replay to also rebase a contained branch' '
+	git replay --contained --onto main main..topic3 >result &&
+
+	test_line_count = 2 result &&
+	cut -f 3 -d " " result >new-branch-tips &&
+
+	git log --format=%s $(head -n 1 new-branch-tips) >actual &&
+	test_write_lines F C M L B A >expect &&
+	test_cmp expect actual &&
+
+	git log --format=%s $(tail -n 1 new-branch-tips) >actual &&
+	test_write_lines H G F C M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/topic1 " >expect &&
+	printf "%s " $(head -n 1 new-branch-tips) >>expect &&
+	git rev-parse topic1 >>expect &&
+	printf "update refs/heads/topic3 " >>expect &&
+	printf "%s " $(tail -n 1 new-branch-tips) >>expect &&
+	git rev-parse topic3 >>expect &&
+
+	test_cmp expect result
+'
+
+test_expect_success 'using replay on bare repo to also rebase a contained branch' '
+	git -C bare replay --contained --onto main main..topic3 >result-bare &&
+	test_cmp expect result-bare
+'
+
 test_done
-- 
2.40.1.491.gdff9a222ea


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v2 15/15] replay: stop assuming replayed branches do not diverge
  2023-05-09 17:53 ` [PATCH v2 00/15] " Christian Couder
                     ` (13 preceding siblings ...)
  2023-05-09 17:53   ` [PATCH v2 14/15] replay: add --contained to rebase contained branches Christian Couder
@ 2023-05-09 17:53   ` Christian Couder
  2023-05-16  4:26     ` Elijah Newren
  2023-05-09 22:28   ` [PATCH v2 00/15] Introduce new `git replay` command Junio C Hamano
                     ` (2 subsequent siblings)
  17 siblings, 1 reply; 208+ messages in thread
From: Christian Couder @ 2023-05-09 17:53 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

From: Elijah Newren <newren@gmail.com>

The replay command is able to replay multiple branches but when some of
them are based on other replayed branches, their commit should be
replayed onto already replayed commits.

For this purpose, let's store the replayed commit and its original
commit in a key value store, so that we can easily find and reused a
replayed commit instead of the original one.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c         | 44 ++++++++++++++++++++++++++--------
 t/t3650-replay-basics.sh | 52 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 86 insertions(+), 10 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 4d24eb95d8..7699d28f93 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -221,20 +221,33 @@ static void determine_replay_mode(struct rev_cmdline_info *cmd_info,
 	strset_clear(&rinfo.positive_refs);
 }
 
+static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,
+				    struct commit *commit,
+				    struct commit *fallback)
+{
+	khint_t pos = kh_get_oid_map(replayed_commits, commit->object.oid);
+	if (pos == kh_end(replayed_commits))
+		return fallback;
+	return kh_value(replayed_commits, pos);
+}
+
 static struct commit *pick_regular_commit(struct commit *pickme,
-					  struct commit *last_commit,
+					  kh_oid_map_t *replayed_commits,
+					  struct commit *onto,
 					  struct merge_options *merge_opt,
 					  struct merge_result *result)
 {
-	struct commit *base;
+	struct commit *base, *replayed_base;
 	struct tree *pickme_tree, *base_tree;
 
 	base = pickme->parents->item;
+	replayed_base = mapped_commit(replayed_commits, base, onto);
 
+	result->tree = repo_get_commit_tree(the_repository, replayed_base);
 	pickme_tree = repo_get_commit_tree(the_repository, pickme);
 	base_tree = repo_get_commit_tree(the_repository, base);
 
-	merge_opt->branch1 = short_commit_name(last_commit);
+	merge_opt->branch1 = short_commit_name(replayed_base);
 	merge_opt->branch2 = short_commit_name(pickme);
 	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
 
@@ -248,7 +261,7 @@ static struct commit *pick_regular_commit(struct commit *pickme,
 	merge_opt->ancestor = NULL;
 	if (!result->clean)
 		return NULL;
-	return create_commit(result->tree, pickme, last_commit);
+	return create_commit(result->tree, pickme, replayed_base);
 }
 
 int cmd_replay(int argc, const char **argv, const char *prefix)
@@ -264,6 +277,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct merge_options merge_opt;
 	struct merge_result result;
 	struct strset *update_refs = NULL;
+	kh_oid_map_t *replayed_commits;
 	int ret = 0, i;
 
 	const char * const replay_usage[] = {
@@ -347,21 +361,30 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
 	merge_opt.show_rename_progress = 0;
-
-	result.tree = repo_get_commit_tree(the_repository, onto);
 	last_commit = onto;
+	replayed_commits = kh_init_oid_map();
 	while ((commit = get_revision(&revs))) {
 		const struct name_decoration *decoration;
+		khint_t pos;
+		int hr;
 
 		if (!commit->parents)
 			die(_("replaying down to root commit is not supported yet!"));
 		if (commit->parents->next)
 			die(_("replaying merge commits is not supported yet!"));
 
-		last_commit = pick_regular_commit(commit, last_commit, &merge_opt, &result);
+		last_commit = pick_regular_commit(commit, replayed_commits, onto,
+						  &merge_opt, &result);
 		if (!last_commit)
 			break;
 
+		/* Record commit -> last_commit mapping */
+		pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr);
+		if (hr == 0)
+			BUG("Duplicate rewritten commit: %s\n",
+			    oid_to_hex(&commit->object.oid));
+		kh_value(replayed_commits, pos) = last_commit;
+
 		/* Update any necessary branches */
 		if (advance_name)
 			continue;
@@ -391,13 +414,14 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 
 	/* Cleanup */
 	merge_finalize(&merge_opt, &result);
-	ret = result.clean;
-
-cleanup:
+	kh_destroy_oid_map(replayed_commits);
 	if (update_refs) {
 		strset_clear(update_refs);
 		free(update_refs);
 	}
+	ret = result.clean;
+
+cleanup:
 	release_revisions(&revs);
 
 	/* Return */
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 3fb4167e69..5aafad448f 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -151,4 +151,56 @@ test_expect_success 'using replay on bare repo to also rebase a contained branch
 	test_cmp expect result-bare
 '
 
+test_expect_success 'using replay to rebase multiple divergent branches' '
+	git replay --onto main ^topic1 topic2 topic4 >result &&
+
+	test_line_count = 2 result &&
+	cut -f 3 -d " " result >new-branch-tips &&
+
+	git log --format=%s $(head -n 1 new-branch-tips) >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual &&
+
+	git log --format=%s $(tail -n 1 new-branch-tips) >actual &&
+	test_write_lines J I M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/topic2 " >expect &&
+	printf "%s " $(head -n 1 new-branch-tips) >>expect &&
+	git rev-parse topic2 >>expect &&
+	printf "update refs/heads/topic4 " >>expect &&
+	printf "%s " $(tail -n 1 new-branch-tips) >>expect &&
+	git rev-parse topic4 >>expect &&
+
+	test_cmp expect result
+'
+
+test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '
+	git -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&
+
+	test_line_count = 4 result &&
+	cut -f 3 -d " " result >new-branch-tips &&
+
+	>expect &&
+	for i in 2 1 3 4
+	do
+		printf "update refs/heads/topic$i " >>expect &&
+		printf "%s " $(grep topic$i result | cut -f 3 -d " ") >>expect &&
+		git -C bare rev-parse topic$i >>expect || return 1
+	done &&
+
+	test_cmp expect result &&
+
+	test_write_lines F C M L B A >expect1 &&
+	test_write_lines E D C M L B A >expect2 &&
+	test_write_lines H G F C M L B A >expect3 &&
+	test_write_lines J I M L B A >expect4 &&
+
+	for i in 1 2 3 4
+	do
+		git -C bare log --format=%s $(grep topic$i result | cut -f 3 -d " ") >actual &&
+		test_cmp expect$i actual || return 1
+	done
+'
+
 test_done
-- 
2.40.1.491.gdff9a222ea


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* Re: [PATCH v2 00/15] Introduce new `git replay` command
  2023-05-09 17:53 ` [PATCH v2 00/15] " Christian Couder
                     ` (14 preceding siblings ...)
  2023-05-09 17:53   ` [PATCH v2 15/15] replay: stop assuming replayed branches do not diverge Christian Couder
@ 2023-05-09 22:28   ` Junio C Hamano
  2023-05-10  7:33     ` Christian Couder
  2023-05-16  4:42   ` Elijah Newren
  2023-06-02 10:25   ` [PATCH v3 " Christian Couder
  17 siblings, 1 reply; 208+ messages in thread
From: Junio C Hamano @ 2023-05-09 22:28 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Patrick Steinhardt, Johannes Schindelin, Elijah Newren,
	John Cai, Derrick Stolee, Phillip Wood, Felipe Contreras,
	Calvin Wan

Christian Couder <christian.couder@gmail.com> writes:

> # Intro
>
> `git replay` has initially been developed entirely by Elijah Newren
> mostly last year (2022) at:
>
> https://github.com/newren/git/commits/replay
>
> I took over a few months ago to polish and upstream it as GitLab is
> interested in replacing libgit2, and for that purpose needs a command
> to do server side (so without using a worktree) rebases, cherry-picks
> and reverts.

It would help to include the following in future updated rounds, as
Elijah is shuffling the header files around agressively and these
patches do not build when merged into 'seen'.  When these two new
includes are added, the series would compile both standalone
(i.e. applied on top of 'master') and in 'seen' (i.e. with shuffled
headers).

Thanks.

 builtin/replay.c | 2 ++
 1 file changed, 2 insertions(+)

diff --git c/builtin/replay.c w/builtin/replay.c
index 7699d28f93..62dd1f7fff 100644
--- c/builtin/replay.c
+++ w/builtin/replay.c
@@ -15,6 +15,8 @@
 #include "refs.h"
 #include "revision.h"
 #include "strmap.h"
+#include <oidset.h>
+#include <tree.h>
 
 static const char *short_commit_name(struct commit *commit)
 {

^ permalink raw reply related	[flat|nested] 208+ messages in thread

* Re: [PATCH v2 00/15] Introduce new `git replay` command
  2023-05-09 22:28   ` [PATCH v2 00/15] Introduce new `git replay` command Junio C Hamano
@ 2023-05-10  7:33     ` Christian Couder
  0 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-05-10  7:33 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, Patrick Steinhardt, Johannes Schindelin, Elijah Newren,
	John Cai, Derrick Stolee, Phillip Wood, Felipe Contreras,
	Calvin Wan

On Wed, May 10, 2023 at 12:28 AM Junio C Hamano <gitster@pobox.com> wrote:
>
> It would help to include the following in future updated rounds, as
> Elijah is shuffling the header files around agressively and these
> patches do not build when merged into 'seen'.  When these two new
> includes are added, the series would compile both standalone
> (i.e. applied on top of 'master') and in 'seen' (i.e. with shuffled
> headers).

I have just added the two new includes in my current version.

Thanks!

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v2 12/15] replay: disallow revision specific options and pathspecs
  2023-05-09 17:53   ` [PATCH v2 12/15] replay: disallow revision specific options and pathspecs Christian Couder
@ 2023-05-16  4:25     ` Elijah Newren
  0 siblings, 0 replies; 208+ messages in thread
From: Elijah Newren @ 2023-05-16  4:25 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	John Cai, Derrick Stolee, Phillip Wood, Felipe Contreras,
	Calvin Wan, Christian Couder

On Tue, May 9, 2023 at 10:54 AM Christian Couder
<christian.couder@gmail.com> wrote:
>
> A previous commit changed `git replay` to make it accept standard
> revision ranges using the setup_revisions() function. While this is good

s/is good/is a good/

> thing to make this command more standard and more flexible, it has the
> downside of enabling many revision related options accepted and eaten by
> setup_revisions().
[...]
> @@ -135,6 +135,20 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
>         argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
>                              PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
>
> +       /*
> +        * TODO: For now, we reject any unknown or invalid option,
> +        * including revision related ones, like --not,
> +        * --first-parent, etc that would be allowed and eaten by
> +        * setup_revisions() below. In the future we should definitely
> +        * accept those that make sense and add related tests and doc
> +        * though.
> +        */
> +       for (i = 0; i < argc; i++)
> +               if (argv[i][0] == '-') {
> +                       error(_("invalid option: %s"), argv[i]);
> +                       usage_with_options(replay_usage, replay_options);
> +               }
> +
>         if (!onto_name) {
>                 error(_("option --onto is mandatory"));
>                 usage_with_options(replay_usage, replay_options);
> @@ -150,6 +164,17 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
>                 goto cleanup;
>         }
>
> +       /*
> +        * TODO: For now, we reject any pathspec. (They are allowed
> +        * and eaten by setup_revisions() above.) In the future we
> +        * should definitely accept them and add related tests and doc
> +        * though.
> +        */

I like the previous TODO, but I think this one can just be left out.
While it might be possible to do something sensible with pathspecs at
least for linear history, it's not clear to me how it could generally
work with non-linear history.  And since replay has handling
non-linear history as a primary (eventual) goal, the "definitely" here
seems incorrect to me.

[...]

Rest looks good.

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v2 14/15] replay: add --contained to rebase contained branches
  2023-05-09 17:53   ` [PATCH v2 14/15] replay: add --contained to rebase contained branches Christian Couder
@ 2023-05-16  4:26     ` Elijah Newren
  0 siblings, 0 replies; 208+ messages in thread
From: Elijah Newren @ 2023-05-16  4:26 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	John Cai, Derrick Stolee, Phillip Wood, Felipe Contreras,
	Calvin Wan, Christian Couder

On Tue, May 9, 2023 at 10:54 AM Christian Couder
<christian.couder@gmail.com> wrote:
>
> From: Elijah Newren <newren@gmail.com>
>
> Let's add a `--contained` option that can be used along with
> `--onto` to rebase all the branches contained in the <revision-range>
> argument.
>
> Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
> Signed-off-by: Elijah Newren <newren@gmail.com>
> Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
> ---
>  Documentation/git-replay.txt | 16 +++++++++++++++-
>  builtin/replay.c             | 12 ++++++++++--
>  t/t3650-replay-basics.sh     | 29 +++++++++++++++++++++++++++++
>  3 files changed, 54 insertions(+), 3 deletions(-)
>
> diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
> index 439b2f92e7..3e06ab2f5e 100644
> --- a/Documentation/git-replay.txt
> +++ b/Documentation/git-replay.txt
> @@ -9,7 +9,7 @@ git-replay - Replay commits on a different base, without touching working tree
>  SYNOPSIS
>  --------
>  [verse]
> -'git replay' (--onto <newbase> | --advance <branch>) <revision-range>...
> +'git replay' [--contained] (--onto <newbase> | --advance <branch>) <revision-range>...

Should this be

  'git replay' ([--contained] --onto <newbase> | --advance <branch>)
<revision-range>...

?

>
>  DESCRIPTION
>  -----------
> @@ -90,6 +90,20 @@ top of the exact same new base, they only differ in that the first
>  provides instructions to make mybranch point at the new commits and
>  the second provides instructions to make target point at them.
>
> +What if you have a stack of branches, one depending upon another, and
> +you'd really like to rebase the whole set?
> +
> +------------
> +$ git replay --contained --onto origin/main origin/main..tipbranch
> +update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
> +update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
> +update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}
> +------------
> +
> +In contrast, trying to do this with rebase would require 3 separate
> +rebases, eacho of which involves a different <ONTO> and <UPSTREAM> and

s/eacho/each/

> +forces you to first check out each branch in turn.

This paragraph isn't true anymore with rebase's --update-refs, right?

> +
>  When calling `git replay`, one does not need to specify a range of
>  commits to replay using the syntax `A..B`; any range expression will
>  do:
> diff --git a/builtin/replay.c b/builtin/replay.c
> index c146f38f58..4d24eb95d8 100644
> --- a/builtin/replay.c
> +++ b/builtin/replay.c
> @@ -256,6 +256,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
>         const char *advance_name = NULL;
>         struct commit *onto = NULL;
>         const char *onto_name = NULL;
> +       int contained = 0;
>
>         struct rev_info revs;
>         struct commit *last_commit = NULL;
> @@ -266,7 +267,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
>         int ret = 0, i;
>
>         const char * const replay_usage[] = {
> -               N_("git replay (--onto <newbase> | --advance <branch>) <revision-range>..."),
> +               N_("git replay [--contained] (--onto <newbase> | --advance <branch>) <revision-range>..."),

Possibly need to update this here too.

>                 NULL
>         };
>         struct option replay_options[] = {
> @@ -276,6 +277,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
>                 OPT_STRING(0, "onto", &onto_name,
>                            N_("revision"),
>                            N_("replay onto given commit")),
> +               OPT_BOOL(0, "contained", &contained,
> +                        N_("advance all branches contained in revision-range")),
>                 OPT_END()
>         };
>
> @@ -301,6 +304,10 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
>                 usage_with_options(replay_usage, replay_options);
>         }
>
> +       if (advance_name && contained)
> +               die(_("options '%s' and '%s' cannot be used together"),
> +                   "--advance", "--contained");

But the code does check that these are incompatible.  Good.


> +
>         repo_init_revisions(the_repository, &revs, prefix);
>
>         argc = setup_revisions(argc, argv, &revs, NULL);
> @@ -363,7 +370,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
>                         continue;
>                 while (decoration) {
>                         if (decoration->type == DECORATION_REF_LOCAL &&
> -                           strset_contains(update_refs, decoration->name)) {
> +                           (contained || strset_contains(update_refs,
> +                                                         decoration->name))) {
>                                 printf("update %s %s %s\n",
>                                        decoration->name,
>                                        oid_to_hex(&last_commit->object.oid),
> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
> index bca405c431..3fb4167e69 100755
> --- a/t/t3650-replay-basics.sh
> +++ b/t/t3650-replay-basics.sh
> @@ -122,4 +122,33 @@ test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
>         test_cmp expect result-bare
>  '
>
> +test_expect_success 'using replay to also rebase a contained branch' '
> +       git replay --contained --onto main main..topic3 >result &&
> +
> +       test_line_count = 2 result &&
> +       cut -f 3 -d " " result >new-branch-tips &&
> +
> +       git log --format=%s $(head -n 1 new-branch-tips) >actual &&
> +       test_write_lines F C M L B A >expect &&
> +       test_cmp expect actual &&
> +
> +       git log --format=%s $(tail -n 1 new-branch-tips) >actual &&
> +       test_write_lines H G F C M L B A >expect &&
> +       test_cmp expect actual &&
> +
> +       printf "update refs/heads/topic1 " >expect &&
> +       printf "%s " $(head -n 1 new-branch-tips) >>expect &&
> +       git rev-parse topic1 >>expect &&
> +       printf "update refs/heads/topic3 " >>expect &&
> +       printf "%s " $(tail -n 1 new-branch-tips) >>expect &&
> +       git rev-parse topic3 >>expect &&
> +
> +       test_cmp expect result
> +'
> +
> +test_expect_success 'using replay on bare repo to also rebase a contained branch' '
> +       git -C bare replay --contained --onto main main..topic3 >result-bare &&
> +       test_cmp expect result-bare
> +'
> +
>  test_done
> --
> 2.40.1.491.gdff9a222ea
>

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v2 15/15] replay: stop assuming replayed branches do not diverge
  2023-05-09 17:53   ` [PATCH v2 15/15] replay: stop assuming replayed branches do not diverge Christian Couder
@ 2023-05-16  4:26     ` Elijah Newren
  0 siblings, 0 replies; 208+ messages in thread
From: Elijah Newren @ 2023-05-16  4:26 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	John Cai, Derrick Stolee, Phillip Wood, Felipe Contreras,
	Calvin Wan, Christian Couder

On Tue, May 9, 2023 at 10:54 AM Christian Couder
<christian.couder@gmail.com> wrote:
>
> From: Elijah Newren <newren@gmail.com>
>
> The replay command is able to replay multiple branches but when some of
> them are based on other replayed branches, their commit should be
> replayed onto already replayed commits.
>
> For this purpose, let's store the replayed commit and its original
> commit in a key value store, so that we can easily find and reused a

s/resued/reuse/


> replayed commit instead of the original one.
>
> Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
> Signed-off-by: Elijah Newren <newren@gmail.com>
> Signed-off-by: Christian Couder <chriscool@tuxfamily.org>

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v2 00/15] Introduce new `git replay` command
  2023-05-09 17:53 ` [PATCH v2 00/15] " Christian Couder
                     ` (15 preceding siblings ...)
  2023-05-09 22:28   ` [PATCH v2 00/15] Introduce new `git replay` command Junio C Hamano
@ 2023-05-16  4:42   ` Elijah Newren
  2023-06-02 10:25   ` [PATCH v3 " Christian Couder
  17 siblings, 0 replies; 208+ messages in thread
From: Elijah Newren @ 2023-05-16  4:42 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	John Cai, Derrick Stolee, Phillip Wood, Calvin Wan

On Tue, May 9, 2023 at 10:54 AM Christian Couder
<christian.couder@gmail.com> wrote:
>
[...]
> Now the changes between v1 and v2 are:
>
> * The patch series has been rebased on top of current master, so that
>   conflicts with master are resolved. This required replacing calls to
>   functions like get_oid() with repo_get_oid(), as well as changing
>   some `#include "*.h"` to use different header files. This
>   unfortunately makes the range-diff between v1 and v2 a bit more
>   difficult to read than we would like.
>
> * Patch 1/15 is a new patch. It has been introduced to rework t6429,
>   so that this test script doesn't break when conflict output is
>   removed later in the series. This allowed us to remove the old 9/14
>   patch ("replay: very coarse worktree updating").
>
> * The old 9/14 patch ("replay: very coarse worktree updating") was
>   removed. This is to make sure that the new command can be used on
>   bare repos. This enables us to focus first on making sure the new
>   command a good plumbing command that works well on bare repos.
>
> * The old 12/14 patch ("replay: introduce guess_new_base()") was
>   removed. This is also to focus first on making sure the new command
>   is a good plumbing command.
>
> * The old 13/14 patch ("replay: add different modes") has been split
>   into 2 patches: patch 13/15 ("replay: add --advance or 'cherry-pick'
>   mode") and patch 14/15 ("replay: add --contained to rebase contained
>   branches").
>
> * Patch 12/15 ("replay: disallow revision specific options and
>   pathspecs") is a new patch that disallow using revision specific
>   options that would be allowed and eaten by setup_revisions(). Even
>   if it would be very nice to have some of these options in the
>   future, not all these options make sense for the new command, and it
>   might require improving the setup_revisions() mechanism (as well as
>   adding tests and doc) to cleanly allow those we want, but that is
>   outside the scope of this first patch series.
>
>   While at it, this patch also disallow pathspecs too.
>
>   It would be nice to have them in the future,

Other than these 10 words and the code comment they represent, all the
changes made in v2 look good to me...

>   but for now it is not necessary and
>   would require work (at least for tests and doc) that can be done
>   later.
>
> * Patches 11/15 to 15/15 include a number of new tests to check that
>   the command works well on bare repos, and in case of 11/15 to check
>   that exit code in case of conflict is 1.
>
> * Patch 11/15, in which the doc for the `git replay` command is
>   introduced, now says that the new command is experimental.

..especially this one.  Thanks for including this!

> * Note that a segfault that was found by Derrick Stolee in the v1 is
>   fixed by 000c4ceca7 (merge-ort: fix calling merge_finalize() with no
>   intermediate merge, 2023-04-22) merged to master by 3927312601
>   (Merge branch 'en/ort-finalize-after-0-merges-fix', 2023-05-02).

^ permalink raw reply	[flat|nested] 208+ messages in thread

* [PATCH v3 00/15] Introduce new `git replay` command
  2023-05-09 17:53 ` [PATCH v2 00/15] " Christian Couder
                     ` (16 preceding siblings ...)
  2023-05-16  4:42   ` Elijah Newren
@ 2023-06-02 10:25   ` Christian Couder
  2023-06-02 10:25     ` [PATCH v3 01/15] t6429: remove switching aspects of fast-rebase Christian Couder
                       ` (16 more replies)
  17 siblings, 17 replies; 208+ messages in thread
From: Christian Couder @ 2023-06-02 10:25 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

# Intro

`git replay` has initially been developed entirely by Elijah Newren
mostly last year (2022) at:

https://github.com/newren/git/commits/replay

I took over this year to polish and upstream it as GitLab is
interested in replacing libgit2, and for that purpose needs a command
to do server side (so without using a worktree) rebases, cherry-picks
and reverts.

I reduced the number of commits and features in this first patch
series, compared to what Elijah already developed. Especially I
stopped short of replaying merge commits and replaying
interactively. These and other features might be upstreamed in the
future after this patch series has graduated.

The focus in this series is to make it a good plumbing command that
can already be used server side and that replaces the "fast-rebase"
test-tool command. So things to make it easier to use on the command
line, and more advanced features (like replaying merges) are left out.

# Content of this cover letter

The "Quick Overview" and "Reasons for diverging from cherry-pick &
rebase" sections just below are describing the purpose of the new
command in the big scheme of things. They are taken from Elijah's
design notes
(https://github.com/newren/git/blob/replay/replay-design-notes.txt)
and describe what we want this command to become and the reasons for
that, not what the command is after only this patch series. Also these
design notes were written at least one year ago, so parts of those 2
sections are not true anymore. I have added Phillip Wood's or Felipe
Contreras' notes (thanks to them) where that's the case, but some now
flawed parts may have missed.

After these two sections, starting with the "Important limitations"
section, you will find sections describing what is actually in this
patch series.

More interesting material is available in Elijah's design notes like
an "Intro via examples"
(https://github.com/newren/git/blob/replay/replay-design-notes.txt#L37-L132),
a discussion about "Preserving topology, replaying merges"
(https://github.com/newren/git/blob/replay/replay-design-notes.txt#L264-L341)
and a "Current status" section describing Elijah's work
(https://github.com/newren/git/blob/replay/replay-design-notes.txt#L344-L392)
before I started working on upstreaming it.

I have not included this material here though, as the documentation
added by this patch series for the `git replay` command already
includes an "EXAMPLES" section, and other sections of Elijah's design
notes might not be interesting for now. Also this cover letter is
already pretty long.  But reviewers can refer to the links above if
they think it can help.

# Quick Overview (from Elijah's design notes)

`git replay`, at a basic level, can perhaps be thought of as a
"default-to-dry-run rebase" -- meaning no updates to the working tree,
or to the index, or to any references.  However, it differs from
rebase in that it:

  * Works for branches that aren't checked out

  * Works in a bare repository

  * Can replay multiple branches simultaneously (with or without common
    history in the range being replayed)

  * Preserves relative topology by default (merges are replayed too in
    Elijah's original work, not in this series)

  * Focuses on performance

  * Has several altered defaults as a result of the above

I sometimes think of `git replay` as "fast-replay", a patch-based
analogue to the snapshot-based fast-export & fast-import tools.

# Reasons for diverging from cherry-pick & rebase (from Elijah's
  design notes)

There are multiple reasons to diverge from the defaults in cherry-pick and
rebase.

* Server side needs

  * Both cherry-pick and rebase, via the sequencer, are heavily tied
    to updating the working tree, index, some refs, and a lot of
    control files with every commit replayed, and invoke a mess of
    hooks[1] that might be hard to avoid for backward compatibility
    reasons (at least, that's been brought up a few times on the
    list).

  * cherry-pick and rebase both fork various subprocesses
    unnecessarily, but somewhat intrinsically in part to ensure the
    same hooks are called that old scripted implementations would have
    called.

    Note: since 356ee4659bb (sequencer: try to commit without forking
    'git commit', 2017-11-24) cherry-pick and rebase do not fork
    subprocesses other than hooks for the cases covered by this patch
    series (i.e. they do not fork "git commit" for simple picks).

  * "Dry run" behavior, where there are no updates to worktree, index,
    or even refs might be important.

  * Should not assume users only want to operate on HEAD (see next
    section)

* Decapitate HEAD-centric assumptions

  * cherry-pick forces commits to be played on top of HEAD;
    inflexible.

  * rebase assumes the range of commits to be replayed is
    upstream..HEAD by default, though it allows one to replay
    upstream..otherbranch -- but it still forcibly and needlessly
    checks out 'otherbranch' before starting to replay things.

    Note: since 767a9c417eb (rebase -i: stop checking out the tip of
    the branch to rebase, 2020-01-24) it's not true that rebase
    forcibly and needlessly checks out 'otherbranch'.

  * Assuming HEAD is involved severely limits replaying multiple
    (possibly divergent) branches.

    Note: since 89fc0b53fdb (rebase: update refs from 'update-ref'
    commands, 2022-07-19) the sequencer can update multiple
    branches. The issue with divergent branch is with command line
    arguments and the todo list generation rather than the
    capabilities of the sequencer.

  * Once you stop assuming HEAD has a certain meaning, there's not
    much reason to have two separate commands anymore (except for the
    funny extra not-necessarily-compatible options both have gained
    over time).

  * (Micro issue: Assuming HEAD is involved also makes it harder for
    new users to learn what rebase means and does; it makes command
    lines hard to parse.  Not sure I want to harp on this too much, as
    I have a suspicion I might be creating a tool for experts with
    complicated use cases, but it's a minor quibble.)

* Performance

  * jj is slaughtering us on rebase speed[2].  I would like us to become
    competitive.  (I dropped a few comments in the link at [2] about why
    git is currently so bad.)

  * From [3], there was a simple 4-patch series in linux.git that took
    53 seconds to rebase.  Switching to ort dropped it to 16 seconds.
    While that sounds great, only 11 *milliseconds* were needed to do
    the actual merges.  That means almost *all* the time (>99%) was
    overhead!  Big offenders:

    * --reapply-cherry-picks should be the default

    * can_fast_forward() should be ripped out, and perhaps other extraneous
      revision walks

      Note: d42c9ffa0f (rebase: factor out branch_base calculation,
      2022-10-17) might already deal with that (according to Felipe
      Contreras).

    * avoid updating working tree, index, refs, reflogs, and control
      structures except when needed (e.g. hitting a conflict, or operation
      finished)

  * Other performance ideas (mostly for future work, not in this
    series)

    * single-file control structures instead of directory of files
      (when doing interactive things which is in Elijah's original
      work, but not in this series)

    * avoid forking subprocesses unless explicitly requested (e.g.
      --exec, --strategy, --run-hooks).  For example, definitely do not
      invoke `git commit` or `git merge`.

    * Sanitize hooks:

      * dispense with all per-commit hooks for sure (pre-commit,
        post-commit, post-checkout).

      * pre-rebase also seems to assume exactly 1 ref is written, and
        invoking it repeatedly would be stupid.  Plus, it's specific
        to "rebase".  So...ignore?  (Stolee's --ref-update option for
        rebase probably broke the pre-rebase assumptions already...)

      * post-rewrite hook might make sense, but fast-import got
        exempted, and I think of replay like a patch-based analogue
        to the snapshot-based fast-import.

    * When not running server side, resolve conflicts in a sparse-cone
      sparse-index worktree to reduce number of files written to a
      working tree.  (See below as well.)

    * [High risk of possible premature optimization] Avoid large
      numbers of newly created loose objects, when replaying large
      numbers of commits.  Two possibilities: (1) Consider using
      tmp-objdir and pack objects from the tmp-objdir at end of
      exercise, (2) Lift code from git-fast-import to immediately
      stuff new objects into a pack?

* Multiple branches and non-checked out branches

  * The ability to operate on non-checked out branches also implies
    that we should generally be able to replay when in a dirty working
    tree (exception being when we expect to update HEAD and any of the
    dirty files is one that needs to be updated by the replay).

  * Also, if we are operating locally on a non-checked out branch and
    hit a conflict, we should have a way to resolve the conflict
    without messing with the user's work on their current
    branch. (This is not is this patch series though.)

    * Idea: new worktree with sparse cone + sparse index checkout,
      containing only files in the root directory, and whatever is
      necessary to get the conflicts

    * Companion to above idea: control structures should be written to
      $GIT_COMMON_DIR/replay-${worktree}, so users can have multiple
      replay sessions, and so we know which worktrees are associated
      with which replay operations.

  - [1] https://lore.kernel.org/git/pull.749.v3.git.git.1586044818132.gitgitgadget@gmail.com/
  - [2] https://github.com/martinvonz/jj/discussions/49
  - [3] https://lore.kernel.org/git/CABPp-BE48=97k_3tnNqXPjSEfA163F8hoE+HY0Zvz1SWB2B8EA@mail.gmail.com/

# Important limitations

* The code exits with code 1 if there are any conflict. No
  resumability. No nice output. No interactivity. No special exit code
  depending on the reason.

* When a commit becomes empty as it is replayed, it is still replayed
  as an empty commit, instead of being dropped.

* No replaying merges, nor root commits. Only regular commits.

* Signed commits are not properly handled. It's not clear what to do
  to such commits when replaying on the server side.

* Notes associated with replayed commits are not updated nor
  duplicated. (Thanks to Phillip Wood for noticing.)

# Commit overview

* 1/15 t6429: remove switching aspects of fast-rebase

    New preparatory commit to make it easier to later replace the
    fast-rebase test-tool by `git replay` without breaking existing
    tests.

* 2/15 replay: introduce new builtin

     This creates a minimal `git replay` command by moving the code
     from the `fast-rebase` test helper from `t/helper/` into
     `builtin/` and doing some renames and a few other needed changes.
     (In v3 some `#include ...` were changed to deal with upstream
     changes in this area as suggested by Junio.)

* - 3/15 replay: start using parse_options API
  - 4/15 replay: die() instead of failing assert()
  - 5/15 replay: introduce pick_regular_commit()
  - 6/15 replay: don't simplify history
  - 7/15 replay: add an important FIXME comment about gpg signing
  - 8/15 replay: remove progress and info output
  - 9/15 replay: remove HEAD related sanity check

     These slowly change the command to make it behave more like
     regular commands and to start cleaning up its output.

* 10/15 replay: make it a minimal server side command

     After the cleaning up in previous commits, it's now time to
     radically change the way it works by stopping it to do ref
     updates, to update the index and worktree, to consider HEAD as
     special. Instead just make it output commands that should be
     passed to `git update-ref --stdin`.

* 11/15 replay: use standard revision ranges

     Start addind new interesting features and also documentation and
     tests, as the interface of the command is cristalizing into its
     final form.

* 12/15 replay: disallow revision specific options and pathspecs

     For now disallow revision specific options and pathspecs that are
     allowed and eaten by setup_revisions(), as it's not clear if all
     of these extra features are really needed, and anyway they would
     require tests and doc. So we leave them for future improvements.
     (In v3 a typo was fixed in the commit message and a code comment
     has been improved as suggested by Elijah.)

* - 13/15 replay: add --advance or 'cherry-pick' mode
  - 14/15 replay: add --contained to rebase contained branches

    Add new option and features to the command. (In v3, in commit
    14/15, the synopsys of the command was improved and a sentence
    about git rebase in its doc was removed as suggested by Elijah.)

* 15/15 replay: stop assuming replayed branches do not diverge

      This adds another interesting feature, as well as related
      documentation and tests. (In v3 a typo in the commit message was
      fixed as suggested by Elijah.)

# Notes about `fast-rebase`, tests and documentation

The `fast-rebase` test-tool helper was developed by Elijah to
experiment with a rebasing tool that would be developed from scratch
based on his merge-ort work, could be used to test that merge-ort
work, and would not have the speed and interface limitations of `git
rebase` or `git cherry-pick`.

This `fast-rebase` helper was used before this series in:

t6429-merge-sequence-rename-caching.sh

So when `git replay` is created from `fast-rebase` in patch 2/15, the
t6429 test script is also converted to use `git replay`. This ensures
that `git replay` doesn't break too badly during the first 10 patches
in this patch series.

Tests and documentation are introduced specifically for `git replay`
only in 11/15 and later patches as it doesn't make much sense to
document and test behavior that we know is going to change soon. So
it's only when the command is crystalizing towards its final form that
we start documenting and testing it.

# Possibly controversial issues 

* bare or not bare: this series works towards a plumbing command with
  the end goal of it being usable and used first on bare repos,
  contrary to existing commands like `git rebase` and `git
  cherry-pick`. The tests check that the command works on both bare
  and non-bare repo though.

* exit status: a successful, non-conflicted replay exits with code
  0. When the replay has conflicts, the exit status is 1. If the
  replay is not able to complete (or start) due to some kind of error,
  the exit status is something other than 0 or 1. There are a few
  tests checking that. It has been suggested in an internal review
  that conflicts might want to get a more specific error code as an
  error code of 1 might be quite easy to return by accident. It
  doesn't seem to me from their docs (which might want to be improved,
  I didn't look at the code) that other commands like `git merge` and
  `git rebase` exit with a special error code in case of conflict.

* make worktree and index changes optional: commit 10/15 stops
  updating the index and worktree, but it might be better especially
  for cli users to make that optional. The issue is that this would
  make the command more complex while we are developing a number of
  important features so that the command can be used on bare repos. It
  seems that this should rather be done in an iterative improvement
  after the important features have landed.

* when and where to add tests and docs: although t6429 has tests that
  are changed to use the new command instead of the fast-rebase
  test-tool command as soon as the former is introduced, there is no
  specific test script and no doc for the new command until commit
  11/15 when standard revision ranges are used. This is done to avoid
  churn in tests and docs while the final form of the command hasn't
  crystalized enough. Adding tests and doc at this point makes this
  commit quite big and possibly more difficult to review than if they
  were in separate commits though. On the other hand when tests and
  docs are added in specific commits some reviewers say it would be
  better to introduce them when the related changes are made.

* --advance and --contained: these two advanced options might not
  belong to this first series and could perhaps be added in a followup
  series in separate commits. On the other hand the code for
  --contained seems involved with the code of --advance and it's nice
  to see soon that git replay can indeed do cherry-picking and rebase
  many refs at once, and this way fullfil these parts of its promise.

* replaying diverging branches: 15/15 the last patch in the series,
  which allow replaying diverging branches, can be seen as a
  fundamental fix or alternatively as adding an interesting
  feature. So it's debatable if it should be in its own patch along
  with its own tests as in this series, or if it should be merged into
  a previous patch and which one.

* only 2 patches: this patch series can be seen from a high level
  point of view as 1) introducing the new `git replay` command, and 2)
  using `git replay` to replace, and get rid of, the fast-rebase
  test-tool command. The fact that not much of the original
  fast-rebase code and interface is left would agree with that point
  of view. On the other hand, fast-rebase can also be seen as a first
  iteration towards `git replay`. So it can also make sense to see how
  `git replay` evolved from it.

# Changes between v2 and v3

Thanks to Elijah and Junio for their suggestions on the previous
version! The very few and minor changes compared to v2 are:

* The patch series was rebased onto master at v2.41.0.

* Patch 2/15 contains `#include ...` changes suggested by Junio to
  deal with upstream changes in this area that were merged while I was
  working on v2 and that conflicted with this series.

* Patch 12/15 has a typo fix in its commit message and an improved
  code comment both suggested by ELijah. The code comment is about
  rejecting pathspec. I removed the 'TODO' mention in it and worded it
  like we "might accept" pathspec in the future, instead of saying
  that "we should definitely accept them".

* In patch 14/15, as suggested by Elijah, the command synopsys now
  contains:

  ([--contained] --onto <newbase> | --advance <branch>)

  instead of:

  [--contained] (--onto <newbase> | --advance <branch>)

  Also in the doc about --contained the sentence talking about git
  rebase has been removed.

* Patch 15/15 has a typo fix in its commit message.

# Changes between v1 and v2

Thanks to all the reviewers on the mailing list or during a Review
Club! Especially, thanks to Elijah, Dscho, Phillip Wood, Derrick
Stolee, Patrick Steinhardt, Junio, Felipe Contreras, Calvin Wan and
all the Review Club participants.

The Review Club notes (see those on 2023-04-18) are available on:

https://docs.google.com/document/d/14L8BAumGTpsXpjDY8VzZ4rRtpAjuGrFSRqn3stCuS_w/

Now the changes between v1 and v2 are:

* The patch series has been rebased on top of current master, so that
  conflicts with master are resolved. This required replacing calls to
  functions like get_oid() with repo_get_oid(), as well as changing
  some `#include "*.h"` to use different header files. This
  unfortunately makes the range-diff between v1 and v2 a bit more
  difficult to read than we would like.

* Patch 1/15 is a new patch. It has been introduced to rework t6429,
  so that this test script doesn't break when conflict output is
  removed later in the series. This allowed us to remove the old 9/14
  patch ("replay: very coarse worktree updating").

* The old 9/14 patch ("replay: very coarse worktree updating") was
  removed. This is to make sure that the new command can be used on
  bare repos. This enables us to focus first on making sure the new
  command a good plumbing command that works well on bare repos.

* The old 12/14 patch ("replay: introduce guess_new_base()") was
  removed. This is also to focus first on making sure the new command
  is a good plumbing command.

* The old 13/14 patch ("replay: add different modes") has been split
  into 2 patches: patch 13/15 ("replay: add --advance or 'cherry-pick'
  mode") and patch 14/15 ("replay: add --contained to rebase contained
  branches").

* Patch 12/15 ("replay: disallow revision specific options and
  pathspecs") is a new patch that disallow using revision specific
  options that would be allowed and eaten by setup_revisions(). Even
  if it would be very nice to have some of these options in the
  future, not all these options make sense for the new command, and it
  might require improving the setup_revisions() mechanism (as well as
  adding tests and doc) to cleanly allow those we want, but that is
  outside the scope of this first patch series.

  While at it, this patch also disallow pathspecs too. It would be
  nice to have them in the future, but for now it is not necessary and
  would require work (at least for tests and doc) that can be done
  later.

* Patches 11/15 to 15/15 include a number of new tests to check that
  the command works well on bare repos, and in case of 11/15 to check
  that exit code in case of conflict is 1.

* Patch 11/15, in which the doc for the `git replay` command is
  introduced, now says that the new command is experimental.

* Note that a segfault that was found by Derrick Stolee in the v1 is
  fixed by 000c4ceca7 (merge-ort: fix calling merge_finalize() with no
  intermediate merge, 2023-04-22) merged to master by 3927312601
  (Merge branch 'en/ort-finalize-after-0-merges-fix', 2023-05-02).

# Range-diff between v2 and v3

 1:  e1bef69d8f =  1:  51fa1c7aea t6429: remove switching aspects of fast-rebase
 2:  73396c4fc9 !  2:  19f8cf1b2e replay: introduce new builtin
    @@ t/helper/test-fast-rebase.c => builtin/replay.c
      #include "cache-tree.h"
      #include "commit.h"
      #include "environment.h"
    +@@
    + #include "sequencer.h"
    + #include "setup.h"
    + #include "strvec.h"
    +-#include "tree.h"
    ++#include <oidset.h>
    ++#include <tree.h>
    + 
    + static const char *short_commit_name(struct commit *commit)
    + {
     @@ builtin/replay.c: static struct commit *create_commit(struct tree *tree,
        return (struct commit *)obj;
      }
 3:  1a1b214146 =  3:  295e876db6 replay: start using parse_options API
 4:  f63d6f0c9f =  4:  6ece7d3751 replay: die() instead of failing assert()
 5:  e7de455815 =  5:  9b4bc659fb replay: introduce pick_regular_commit()
 6:  103c18b3eb =  6:  9ab68d50ab replay: don't simplify history
 7:  5be7eec24f =  7:  37e93faafa replay: add an important FIXME comment about gpg signing
 8:  bb5d3a0564 =  8:  03036781ed replay: remove progress and info output
 9:  a75b02ddae =  9:  4ea289952e replay: remove HEAD related sanity check
10:  cff278a0b9 ! 10:  fba98eda07 replay: make it a minimal server side command
    @@ builtin/replay.c
     -#include "sequencer.h"
     -#include "setup.h"
      #include "strvec.h"
    --#include "tree.h"
    - 
    - static const char *short_commit_name(struct commit *commit)
    - {
    + #include <oidset.h>
    + #include <tree.h>
     @@ builtin/replay.c: static struct commit *pick_regular_commit(struct commit *pickme,
        pickme_tree = repo_get_commit_tree(the_repository, pickme);
        base_tree = repo_get_commit_tree(the_repository, base);
11:  926b332d44 ! 11:  03f9f20f6e replay: use standard revision ranges
    @@ builtin/replay.c
      #include "refs.h"
      #include "revision.h"
     -#include "strvec.h"
    + #include <oidset.h>
    + #include <tree.h>
      
    - static const char *short_commit_name(struct commit *commit)
    - {
     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix)
        struct commit *onto;
        const char *onto_name = NULL;
12:  f7eb2230a7 ! 12:  e651250ac7 replay: disallow revision specific options and pathspecs
    @@ Commit message
         replay: disallow revision specific options and pathspecs
     
         A previous commit changed `git replay` to make it accept standard
    -    revision ranges using the setup_revisions() function. While this is good
    -    thing to make this command more standard and more flexible, it has the
    -    downside of enabling many revision related options accepted and eaten by
    -    setup_revisions().
    +    revision ranges using the setup_revisions() function. While this is a
    +    good thing to make this command more standard and more flexible, it has
    +    the downside of enabling many revision related options accepted and eaten
    +    by setup_revisions().
     
         Some of these options might make sense, but others, like those
         generating non-contiguous history, might not. Anyway those we might want
    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
        }
      
     +  /*
    -+   * TODO: For now, we reject any pathspec. (They are allowed
    -+   * and eaten by setup_revisions() above.) In the future we
    -+   * should definitely accept them and add related tests and doc
    -+   * though.
    ++   * Reject any pathspec. (They are allowed and eaten by
    ++   * setup_revisions() above.) In the future we might accept
    ++   * them, after adding related tests and doc though.
     +   */
     +  if (revs.prune_data.nr) {
     +          error(_("invalid pathspec: %s"), revs.prune_data.items[0].match);
13:  bc301cf93a ! 13:  56e5416dad replay: add --advance or 'cherry-pick' mode
    @@ builtin/replay.c
      #include "refs.h"
      #include "revision.h"
     +#include "strmap.h"
    + #include <oidset.h>
    + #include <tree.h>
      
    - static const char *short_commit_name(struct commit *commit)
    - {
     @@ builtin/replay.c: static struct commit *create_commit(struct tree *tree,
        return (struct commit *)obj;
      }
14:  3eb582b509 ! 14:  2cc17dfdc7 replay: add --contained to rebase contained branches
    @@ Documentation/git-replay.txt: git-replay - Replay commits on a different base, w
      --------
      [verse]
     -'git replay' (--onto <newbase> | --advance <branch>) <revision-range>...
    -+'git replay' [--contained] (--onto <newbase> | --advance <branch>) <revision-range>...
    ++'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...
      
      DESCRIPTION
      -----------
    @@ Documentation/git-replay.txt: top of the exact same new base, they only differ i
     +update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
     +update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}
     +------------
    -+
    -+In contrast, trying to do this with rebase would require 3 separate
    -+rebases, eacho of which involves a different <ONTO> and <UPSTREAM> and
    -+forces you to first check out each branch in turn.
     +
      When calling `git replay`, one does not need to specify a range of
      commits to replay using the syntax `A..B`; any range expression will
    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
      
        const char * const replay_usage[] = {
     -          N_("git replay (--onto <newbase> | --advance <branch>) <revision-range>..."),
    -+          N_("git replay [--contained] (--onto <newbase> | --advance <branch>) <revision-range>..."),
    ++          N_("git replay ([--contained] --onto <newbase> | --advance <branch>) <revision-range>..."),
                NULL
        };
        struct option replay_options[] = {
15:  bdd8a3b3e1 ! 15:  a6d88fc8f0 replay: stop assuming replayed branches do not diverge
    @@ Commit message
         replayed onto already replayed commits.
     
         For this purpose, let's store the replayed commit and its original
    -    commit in a key value store, so that we can easily find and reused a
    +    commit in a key value store, so that we can easily find and reuse a
         replayed commit instead of the original one.
     
         Co-authored-by: Christian Couder <chriscool@tuxfamily.org>


Christian Couder (1):
  replay: disallow revision specific options and pathspecs

Elijah Newren (14):
  t6429: remove switching aspects of fast-rebase
  replay: introduce new builtin
  replay: start using parse_options API
  replay: die() instead of failing assert()
  replay: introduce pick_regular_commit()
  replay: don't simplify history
  replay: add an important FIXME comment about gpg signing
  replay: remove progress and info output
  replay: remove HEAD related sanity check
  replay: make it a minimal server side command
  replay: use standard revision ranges
  replay: add --advance or 'cherry-pick' mode
  replay: add --contained to rebase contained branches
  replay: stop assuming replayed branches do not diverge

 .gitignore                               |   1 +
 Documentation/git-replay.txt             | 121 +++++++
 Makefile                                 |   2 +-
 builtin.h                                |   1 +
 builtin/replay.c                         | 432 +++++++++++++++++++++++
 command-list.txt                         |   1 +
 git.c                                    |   1 +
 t/helper/test-fast-rebase.c              | 240 -------------
 t/helper/test-tool.c                     |   1 -
 t/helper/test-tool.h                     |   1 -
 t/t3650-replay-basics.sh                 | 206 +++++++++++
 t/t6429-merge-sequence-rename-caching.sh |  45 +--
 12 files changed, 789 insertions(+), 263 deletions(-)
 create mode 100644 Documentation/git-replay.txt
 create mode 100644 builtin/replay.c
 delete mode 100644 t/helper/test-fast-rebase.c
 create mode 100755 t/t3650-replay-basics.sh

-- 
2.41.0.15.ga6d88fc8f0


^ permalink raw reply	[flat|nested] 208+ messages in thread

* [PATCH v3 01/15] t6429: remove switching aspects of fast-rebase
  2023-06-02 10:25   ` [PATCH v3 " Christian Couder
@ 2023-06-02 10:25     ` Christian Couder
  2023-06-02 10:25     ` [PATCH v3 02/15] replay: introduce new builtin Christian Couder
                       ` (15 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-06-02 10:25 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

From: Elijah Newren <newren@gmail.com>

At the time t6429 was written, merge-ort was still under development,
did not have quite as many tests, and certainly was not widely deployed.
Since t6429 was exercising some codepaths just a little differently, we
thought having them also test the "merge_switch_to_result()" bits of
merge-ort was useful even though they weren't intrinsic to the real
point of these tests.

However, the value provided by doing extra testing of the
"merge_switch_to_result()" bits has decreased a bit over time, and it's
actively making it harder to refactor `test-tool fast-rebase` into `git
replay`, which we are going to do in following commits.  Dispense with
these bits.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 t/helper/test-fast-rebase.c              | 9 +--------
 t/t6429-merge-sequence-rename-caching.sh | 9 +++++++--
 2 files changed, 8 insertions(+), 10 deletions(-)

diff --git a/t/helper/test-fast-rebase.c b/t/helper/test-fast-rebase.c
index d1d63feaa9..403bdf8e75 100644
--- a/t/helper/test-fast-rebase.c
+++ b/t/helper/test-fast-rebase.c
@@ -193,7 +193,7 @@ int cmd__fast_rebase(int argc, const char **argv)
 		last_commit = create_commit(result.tree, commit, last_commit);
 	}
 
-	merge_switch_to_result(&merge_opt, head_tree, &result, 1, !result.clean);
+	merge_finalize(&merge_opt, &result);
 
 	if (result.clean < 0)
 		exit(128);
@@ -212,9 +212,6 @@ int cmd__fast_rebase(int argc, const char **argv)
 		}
 		if (create_symref("HEAD", branch_name.buf, reflog_msg.buf) < 0)
 			die(_("unable to update HEAD"));
-
-		prime_cache_tree(the_repository, the_repository->index,
-				 result.tree);
 	} else {
 		fprintf(stderr, "\nAborting: Hit a conflict.\n");
 		strbuf_addf(&reflog_msg, "rebase progress up to %s",
@@ -227,10 +224,6 @@ int cmd__fast_rebase(int argc, const char **argv)
 			die("Failed to update %s", argv[4]);
 		}
 	}
-	if (write_locked_index(&the_index, &lock,
-			       COMMIT_LOCK | SKIP_IF_UNCHANGED))
-		die(_("unable to write %s"), get_index_file());
-
 	ret = (result.clean == 0);
 cleanup:
 	strbuf_release(&reflog_msg);
diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh
index d02fa16614..75d3fd2dba 100755
--- a/t/t6429-merge-sequence-rename-caching.sh
+++ b/t/t6429-merge-sequence-rename-caching.sh
@@ -72,6 +72,7 @@ test_expect_success 'caching renames does not preclude finding new ones' '
 		git switch upstream &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream~1..topic
 
 		git ls-files >tracked-files &&
@@ -200,6 +201,7 @@ test_expect_success 'rename same file identically, then reintroduce it' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream~1..topic &&
 
 		git ls-files >tracked &&
@@ -277,6 +279,7 @@ test_expect_success 'rename same file identically, then add file to old dir' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream~1..topic &&
 
 		git ls-files >tracked &&
@@ -356,8 +359,6 @@ test_expect_success 'cached dir rename does not prevent noticing later conflict'
 		test_must_fail test-tool fast-rebase --onto HEAD upstream~1 topic >output &&
 		#git cherry-pick upstream..topic &&
 
-		grep CONFLICT..rename/rename output &&
-
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls
 	)
@@ -456,6 +457,7 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
@@ -522,6 +524,7 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
@@ -624,6 +627,7 @@ test_expect_success 'caching renames only on upstream side, part 1' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
@@ -682,6 +686,7 @@ test_expect_success 'caching renames only on upstream side, part 2' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
-- 
2.41.0.15.ga6d88fc8f0


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v3 02/15] replay: introduce new builtin
  2023-06-02 10:25   ` [PATCH v3 " Christian Couder
  2023-06-02 10:25     ` [PATCH v3 01/15] t6429: remove switching aspects of fast-rebase Christian Couder
@ 2023-06-02 10:25     ` Christian Couder
  2023-06-02 10:25     ` [PATCH v3 03/15] replay: start using parse_options API Christian Couder
                       ` (14 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-06-02 10:25 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

From: Elijah Newren <newren@gmail.com>

For now, this is just a rename from `t/helper/test-fast-rebase.c` into
`builtin/replay.c` with minimal changes to make it build appropriately.

Subsequent commits will flesh out its capabilities and make it a more
standard regular builtin.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 .gitignore                                    |  1 +
 Makefile                                      |  2 +-
 builtin.h                                     |  1 +
 .../test-fast-rebase.c => builtin/replay.c    | 28 ++++++-------------
 command-list.txt                              |  1 +
 git.c                                         |  1 +
 t/helper/test-tool.c                          |  1 -
 t/helper/test-tool.h                          |  1 -
 t/t6429-merge-sequence-rename-caching.sh      | 27 ++++++------------
 9 files changed, 22 insertions(+), 41 deletions(-)
 rename t/helper/test-fast-rebase.c => builtin/replay.c (89%)

diff --git a/.gitignore b/.gitignore
index e875c59054..b5f025a296 100644
--- a/.gitignore
+++ b/.gitignore
@@ -135,6 +135,7 @@
 /git-remote-ext
 /git-repack
 /git-replace
+/git-replay
 /git-request-pull
 /git-rerere
 /git-reset
diff --git a/Makefile b/Makefile
index e440728c24..a6ab78840f 100644
--- a/Makefile
+++ b/Makefile
@@ -799,7 +799,6 @@ TEST_BUILTINS_OBJS += test-dump-split-index.o
 TEST_BUILTINS_OBJS += test-dump-untracked-cache.o
 TEST_BUILTINS_OBJS += test-env-helper.o
 TEST_BUILTINS_OBJS += test-example-decorate.o
-TEST_BUILTINS_OBJS += test-fast-rebase.o
 TEST_BUILTINS_OBJS += test-fsmonitor-client.o
 TEST_BUILTINS_OBJS += test-genrandom.o
 TEST_BUILTINS_OBJS += test-genzeros.o
@@ -1286,6 +1285,7 @@ BUILTIN_OBJS += builtin/remote-fd.o
 BUILTIN_OBJS += builtin/remote.o
 BUILTIN_OBJS += builtin/repack.o
 BUILTIN_OBJS += builtin/replace.o
+BUILTIN_OBJS += builtin/replay.o
 BUILTIN_OBJS += builtin/rerere.o
 BUILTIN_OBJS += builtin/reset.o
 BUILTIN_OBJS += builtin/rev-list.o
diff --git a/builtin.h b/builtin.h
index cb0db67681..c3f0b56915 100644
--- a/builtin.h
+++ b/builtin.h
@@ -214,6 +214,7 @@ int cmd_remote(int argc, const char **argv, const char *prefix);
 int cmd_remote_ext(int argc, const char **argv, const char *prefix);
 int cmd_remote_fd(int argc, const char **argv, const char *prefix);
 int cmd_repack(int argc, const char **argv, const char *prefix);
+int cmd_replay(int argc, const char **argv, const char *prefix);
 int cmd_rerere(int argc, const char **argv, const char *prefix);
 int cmd_reset(int argc, const char **argv, const char *prefix);
 int cmd_restore(int argc, const char **argv, const char *prefix);
diff --git a/t/helper/test-fast-rebase.c b/builtin/replay.c
similarity index 89%
rename from t/helper/test-fast-rebase.c
rename to builtin/replay.c
index 403bdf8e75..252af62d12 100644
--- a/t/helper/test-fast-rebase.c
+++ b/builtin/replay.c
@@ -1,18 +1,11 @@
 /*
- * "git fast-rebase" builtin command
- *
- * FAST: Forking Any Subprocesses (is) Taboo
- *
- * This is meant SOLELY as a demo of what is possible.  sequencer.c and
- * rebase.c should be refactored to use the ideas here, rather than attempting
- * to extend this file to replace those (unless Phillip or Dscho say that
- * refactoring is too hard and we need a clean slate, but I'm guessing that
- * refactoring is the better route).
+ * "git replay" builtin command
  */
 
 #define USE_THE_INDEX_VARIABLE
-#include "test-tool.h"
-#include "cache.h"
+#include "git-compat-util.h"
+
+#include "builtin.h"
 #include "cache-tree.h"
 #include "commit.h"
 #include "environment.h"
@@ -26,7 +19,8 @@
 #include "sequencer.h"
 #include "setup.h"
 #include "strvec.h"
-#include "tree.h"
+#include <oidset.h>
+#include <tree.h>
 
 static const char *short_commit_name(struct commit *commit)
 {
@@ -93,7 +87,7 @@ static struct commit *create_commit(struct tree *tree,
 	return (struct commit *)obj;
 }
 
-int cmd__fast_rebase(int argc, const char **argv)
+int cmd_replay(int argc, const char **argv, const char *prefix)
 {
 	struct commit *onto;
 	struct commit *last_commit = NULL, *last_picked_commit = NULL;
@@ -109,12 +103,6 @@ int cmd__fast_rebase(int argc, const char **argv)
 	struct strbuf branch_name = STRBUF_INIT;
 	int ret = 0;
 
-	/*
-	 * test-tool stuff doesn't set up the git directory by default; need to
-	 * do that manually.
-	 */
-	setup_git_directory();
-
 	if (argc == 2 && !strcmp(argv[1], "-h")) {
 		printf("Sorry, I am not a psychiatrist; I can not give you the help you need.  Oh, you meant usage...\n");
 		exit(129);
@@ -135,7 +123,7 @@ int cmd__fast_rebase(int argc, const char **argv)
 	if (repo_read_index(the_repository) < 0)
 		BUG("Could not read index");
 
-	repo_init_revisions(the_repository, &revs, NULL);
+	repo_init_revisions(the_repository, &revs, prefix);
 	revs.verbose_header = 1;
 	revs.max_parents = 1;
 	revs.cherry_mark = 1;
diff --git a/command-list.txt b/command-list.txt
index 54b2a50f5f..d74836ab21 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -160,6 +160,7 @@ git-reflog                              ancillarymanipulators           complete
 git-remote                              ancillarymanipulators           complete
 git-repack                              ancillarymanipulators           complete
 git-replace                             ancillarymanipulators           complete
+git-replay                              mainporcelain           history
 git-request-pull                        foreignscminterface             complete
 git-rerere                              ancillaryinterrogators
 git-reset                               mainporcelain           history
diff --git a/git.c b/git.c
index 2f42da20f4..cf95c61813 100644
--- a/git.c
+++ b/git.c
@@ -593,6 +593,7 @@ static struct cmd_struct commands[] = {
 	{ "remote-fd", cmd_remote_fd, NO_PARSEOPT },
 	{ "repack", cmd_repack, RUN_SETUP },
 	{ "replace", cmd_replace, RUN_SETUP },
+	{ "replay", cmd_replay, RUN_SETUP },
 	{ "rerere", cmd_rerere, RUN_SETUP },
 	{ "reset", cmd_reset, RUN_SETUP },
 	{ "restore", cmd_restore, RUN_SETUP | NEED_WORK_TREE },
diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
index abe8a785eb..9ca1586de7 100644
--- a/t/helper/test-tool.c
+++ b/t/helper/test-tool.c
@@ -30,7 +30,6 @@ static struct test_cmd cmds[] = {
 	{ "dump-untracked-cache", cmd__dump_untracked_cache },
 	{ "env-helper", cmd__env_helper },
 	{ "example-decorate", cmd__example_decorate },
-	{ "fast-rebase", cmd__fast_rebase },
 	{ "fsmonitor-client", cmd__fsmonitor_client },
 	{ "genrandom", cmd__genrandom },
 	{ "genzeros", cmd__genzeros },
diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h
index ea2672436c..a03bbfc6b2 100644
--- a/t/helper/test-tool.h
+++ b/t/helper/test-tool.h
@@ -24,7 +24,6 @@ int cmd__dump_untracked_cache(int argc, const char **argv);
 int cmd__dump_reftable(int argc, const char **argv);
 int cmd__env_helper(int argc, const char **argv);
 int cmd__example_decorate(int argc, const char **argv);
-int cmd__fast_rebase(int argc, const char **argv);
 int cmd__fsmonitor_client(int argc, const char **argv);
 int cmd__genrandom(int argc, const char **argv);
 int cmd__genzeros(int argc, const char **argv);
diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh
index 75d3fd2dba..7670b72008 100755
--- a/t/t6429-merge-sequence-rename-caching.sh
+++ b/t/t6429-merge-sequence-rename-caching.sh
@@ -71,9 +71,8 @@ test_expect_success 'caching renames does not preclude finding new ones' '
 
 		git switch upstream &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream~1..topic
 
 		git ls-files >tracked-files &&
 		test_line_count = 2 tracked-files &&
@@ -141,8 +140,7 @@ test_expect_success 'cherry-pick both a commit and its immediate revert' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
-		#git cherry-pick upstream~1..topic &&
+		git replay --onto HEAD upstream~1 topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 1 calls
@@ -200,9 +198,8 @@ test_expect_success 'rename same file identically, then reintroduce it' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream~1..topic &&
 
 		git ls-files >tracked &&
 		test_line_count = 2 tracked &&
@@ -278,9 +275,8 @@ test_expect_success 'rename same file identically, then add file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream~1..topic &&
 
 		git ls-files >tracked &&
 		test_line_count = 4 tracked &&
@@ -356,8 +352,7 @@ test_expect_success 'cached dir rename does not prevent noticing later conflict'
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test_must_fail test-tool fast-rebase --onto HEAD upstream~1 topic >output &&
-		#git cherry-pick upstream..topic &&
+		test_must_fail git replay --onto HEAD upstream~1 topic >output &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls
@@ -456,9 +451,8 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls &&
@@ -523,9 +517,8 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 3 calls &&
@@ -626,9 +619,8 @@ test_expect_success 'caching renames only on upstream side, part 1' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 1 calls &&
@@ -685,9 +677,8 @@ test_expect_success 'caching renames only on upstream side, part 2' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls &&
-- 
2.41.0.15.ga6d88fc8f0


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v3 03/15] replay: start using parse_options API
  2023-06-02 10:25   ` [PATCH v3 " Christian Couder
  2023-06-02 10:25     ` [PATCH v3 01/15] t6429: remove switching aspects of fast-rebase Christian Couder
  2023-06-02 10:25     ` [PATCH v3 02/15] replay: introduce new builtin Christian Couder
@ 2023-06-02 10:25     ` Christian Couder
  2023-06-02 10:25     ` [PATCH v3 04/15] replay: die() instead of failing assert() Christian Couder
                       ` (13 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-06-02 10:25 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

From: Elijah Newren <newren@gmail.com>

Instead of manually parsing arguments, let's start using the parse_options
API. This way this new builtin will look more standard, and in some
upcoming commits will more easily be able to handle more command line
options.

Note that we plan to later use standard revision ranges instead of
hardcoded "<oldbase> <branch>" arguments. When we will use standard
revision ranges, it will be easier to check if there are no spurious
arguments if we keep ARGV[0], so let's call parse_options() with
PARSE_OPT_KEEP_ARGV0 even if we don't need ARGV[0] right now to avoid
some useless code churn.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 44 ++++++++++++++++++++++++++++++++------------
 1 file changed, 32 insertions(+), 12 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 252af62d12..7eb05048a5 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -14,6 +14,7 @@
 #include "lockfile.h"
 #include "merge-ort.h"
 #include "object-name.h"
+#include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
 #include "sequencer.h"
@@ -90,6 +91,7 @@ static struct commit *create_commit(struct tree *tree,
 int cmd_replay(int argc, const char **argv, const char *prefix)
 {
 	struct commit *onto;
+	const char *onto_name = NULL;
 	struct commit *last_commit = NULL, *last_picked_commit = NULL;
 	struct object_id head;
 	struct lock_file lock = LOCK_INIT;
@@ -103,16 +105,32 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct strbuf branch_name = STRBUF_INIT;
 	int ret = 0;
 
-	if (argc == 2 && !strcmp(argv[1], "-h")) {
-		printf("Sorry, I am not a psychiatrist; I can not give you the help you need.  Oh, you meant usage...\n");
-		exit(129);
+	const char * const replay_usage[] = {
+		N_("git replay --onto <newbase> <oldbase> <branch>"),
+		NULL
+	};
+	struct option replay_options[] = {
+		OPT_STRING(0, "onto", &onto_name,
+			   N_("revision"),
+			   N_("replay onto given commit")),
+		OPT_END()
+	};
+
+	argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
+			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
+
+	if (!onto_name) {
+		error(_("option --onto is mandatory"));
+		usage_with_options(replay_usage, replay_options);
 	}
 
-	if (argc != 5 || strcmp(argv[1], "--onto"))
-		die("usage: read the code, figure out how to use it, then do so");
+	if (argc != 3) {
+		error(_("bad number of arguments"));
+		usage_with_options(replay_usage, replay_options);
+	}
 
-	onto = peel_committish(argv[2]);
-	strbuf_addf(&branch_name, "refs/heads/%s", argv[4]);
+	onto = peel_committish(onto_name);
+	strbuf_addf(&branch_name, "refs/heads/%s", argv[2]);
 
 	/* Sanity check */
 	if (repo_get_oid(the_repository, "HEAD", &head))
@@ -124,6 +142,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		BUG("Could not read index");
 
 	repo_init_revisions(the_repository, &revs, prefix);
+
 	revs.verbose_header = 1;
 	revs.max_parents = 1;
 	revs.cherry_mark = 1;
@@ -132,7 +151,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	revs.right_only = 1;
 	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
 	revs.topo_order = 1;
-	strvec_pushl(&rev_walk_args, "", argv[4], "--not", argv[3], NULL);
+
+	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
 
 	if (setup_revisions(rev_walk_args.nr, rev_walk_args.v, &revs, NULL) > 1) {
 		ret = error(_("unhandled options"));
@@ -195,8 +215,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			       &last_commit->object.oid,
 			       &last_picked_commit->object.oid,
 			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
-			error(_("could not update %s"), argv[4]);
-			die("Failed to update %s", argv[4]);
+			error(_("could not update %s"), argv[2]);
+			die("Failed to update %s", argv[2]);
 		}
 		if (create_symref("HEAD", branch_name.buf, reflog_msg.buf) < 0)
 			die(_("unable to update HEAD"));
@@ -208,8 +228,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			       &last_commit->object.oid,
 			       &head,
 			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
-			error(_("could not update %s"), argv[4]);
-			die("Failed to update %s", argv[4]);
+			error(_("could not update %s"), argv[2]);
+			die("Failed to update %s", argv[2]);
 		}
 	}
 	ret = (result.clean == 0);
-- 
2.41.0.15.ga6d88fc8f0


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v3 04/15] replay: die() instead of failing assert()
  2023-06-02 10:25   ` [PATCH v3 " Christian Couder
                       ` (2 preceding siblings ...)
  2023-06-02 10:25     ` [PATCH v3 03/15] replay: start using parse_options API Christian Couder
@ 2023-06-02 10:25     ` Christian Couder
  2023-06-02 10:25     ` [PATCH v3 05/15] replay: introduce pick_regular_commit() Christian Couder
                       ` (12 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-06-02 10:25 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

From: Elijah Newren <newren@gmail.com>

It's not a good idea for regular Git commands to use an assert() to
check for things that could happen but are not supported.

Let's die() with an explanation of the issue instead.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 7eb05048a5..2537adbaea 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -178,7 +178,12 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 
 		fprintf(stderr, "Rebasing %s...\r",
 			oid_to_hex(&commit->object.oid));
-		assert(commit->parents && !commit->parents->next);
+
+		if (!commit->parents)
+			die(_("replaying down to root commit is not supported yet!"));
+		if (commit->parents->next)
+			die(_("replaying merge commits is not supported yet!"));
+
 		base = commit->parents->item;
 
 		next_tree = repo_get_commit_tree(the_repository, commit);
-- 
2.41.0.15.ga6d88fc8f0


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v3 05/15] replay: introduce pick_regular_commit()
  2023-06-02 10:25   ` [PATCH v3 " Christian Couder
                       ` (3 preceding siblings ...)
  2023-06-02 10:25     ` [PATCH v3 04/15] replay: die() instead of failing assert() Christian Couder
@ 2023-06-02 10:25     ` Christian Couder
  2023-06-02 10:25     ` [PATCH v3 06/15] replay: don't simplify history Christian Couder
                       ` (11 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-06-02 10:25 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

From: Elijah Newren <newren@gmail.com>

Let's refactor the code to handle a regular commit (a commit that is
neither a root commit nor a merge commit) into a single function instead
of keeping it inside cmd_replay().

This is good for separation of concerns, and this will help further work
in the future to replay merge commits.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 54 ++++++++++++++++++++++++++++++------------------
 1 file changed, 34 insertions(+), 20 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 2537adbaea..319020f5fd 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -88,6 +88,35 @@ static struct commit *create_commit(struct tree *tree,
 	return (struct commit *)obj;
 }
 
+static struct commit *pick_regular_commit(struct commit *pickme,
+					  struct commit *last_commit,
+					  struct merge_options *merge_opt,
+					  struct merge_result *result)
+{
+	struct commit *base;
+	struct tree *pickme_tree, *base_tree;
+
+	base = pickme->parents->item;
+
+	pickme_tree = repo_get_commit_tree(the_repository, pickme);
+	base_tree = repo_get_commit_tree(the_repository, base);
+
+	merge_opt->branch2 = short_commit_name(pickme);
+	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
+
+	merge_incore_nonrecursive(merge_opt,
+				  base_tree,
+				  result->tree,
+				  pickme_tree,
+				  result);
+
+	free((char*)merge_opt->ancestor);
+	merge_opt->ancestor = NULL;
+	if (!result->clean)
+		return NULL;
+	return create_commit(result->tree, pickme, last_commit);
+}
+
 int cmd_replay(int argc, const char **argv, const char *prefix)
 {
 	struct commit *onto;
@@ -99,7 +128,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct rev_info revs;
 	struct commit *commit;
 	struct merge_options merge_opt;
-	struct tree *next_tree, *base_tree, *head_tree;
+	struct tree *head_tree;
 	struct merge_result result;
 	struct strbuf reflog_msg = STRBUF_INIT;
 	struct strbuf branch_name = STRBUF_INIT;
@@ -174,7 +203,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	result.tree = head_tree;
 	last_commit = onto;
 	while ((commit = get_revision(&revs))) {
-		struct commit *base;
+		struct commit *pick;
 
 		fprintf(stderr, "Rebasing %s...\r",
 			oid_to_hex(&commit->object.oid));
@@ -184,26 +213,11 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		if (commit->parents->next)
 			die(_("replaying merge commits is not supported yet!"));
 
-		base = commit->parents->item;
-
-		next_tree = repo_get_commit_tree(the_repository, commit);
-		base_tree = repo_get_commit_tree(the_repository, base);
-
-		merge_opt.branch2 = short_commit_name(commit);
-		merge_opt.ancestor = xstrfmt("parent of %s", merge_opt.branch2);
-
-		merge_incore_nonrecursive(&merge_opt,
-					  base_tree,
-					  result.tree,
-					  next_tree,
-					  &result);
-
-		free((char*)merge_opt.ancestor);
-		merge_opt.ancestor = NULL;
-		if (!result.clean)
+		pick = pick_regular_commit(commit, last_commit, &merge_opt, &result);
+		if (!pick)
 			break;
+		last_commit = pick;
 		last_picked_commit = commit;
-		last_commit = create_commit(result.tree, commit, last_commit);
 	}
 
 	merge_finalize(&merge_opt, &result);
-- 
2.41.0.15.ga6d88fc8f0


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v3 06/15] replay: don't simplify history
  2023-06-02 10:25   ` [PATCH v3 " Christian Couder
                       ` (4 preceding siblings ...)
  2023-06-02 10:25     ` [PATCH v3 05/15] replay: introduce pick_regular_commit() Christian Couder
@ 2023-06-02 10:25     ` Christian Couder
  2023-06-02 10:25     ` [PATCH v3 07/15] replay: add an important FIXME comment about gpg signing Christian Couder
                       ` (10 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-06-02 10:25 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

From: Elijah Newren <newren@gmail.com>

Let's set the rev walking options we need after calling
setup_revisions() instead of before. This makes it clearer which options
we need.

Also we don't want history simplification, as we want to deal with all
the commits in the affected range.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 15 ++++++---------
 1 file changed, 6 insertions(+), 9 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 319020f5fd..d28d468008 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -172,15 +172,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 
 	repo_init_revisions(the_repository, &revs, prefix);
 
-	revs.verbose_header = 1;
-	revs.max_parents = 1;
-	revs.cherry_mark = 1;
-	revs.limited = 1;
-	revs.reverse = 1;
-	revs.right_only = 1;
-	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
-	revs.topo_order = 1;
-
 	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
 
 	if (setup_revisions(rev_walk_args.nr, rev_walk_args.v, &revs, NULL) > 1) {
@@ -188,6 +179,12 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		goto cleanup;
 	}
 
+	/* requirements/overrides for revs */
+	revs.reverse = 1;
+	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
+	revs.topo_order = 1;
+	revs.simplify_history = 0;
+
 	strvec_clear(&rev_walk_args);
 
 	if (prepare_revision_walk(&revs) < 0) {
-- 
2.41.0.15.ga6d88fc8f0


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v3 07/15] replay: add an important FIXME comment about gpg signing
  2023-06-02 10:25   ` [PATCH v3 " Christian Couder
                       ` (5 preceding siblings ...)
  2023-06-02 10:25     ` [PATCH v3 06/15] replay: don't simplify history Christian Couder
@ 2023-06-02 10:25     ` Christian Couder
  2023-06-02 10:25     ` [PATCH v3 08/15] replay: remove progress and info output Christian Couder
                       ` (9 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-06-02 10:25 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

From: Elijah Newren <newren@gmail.com>

We want to be able to handle signed commits in some way in the future,
but we are not ready to do it now. So for the time being let's just add
a FIXME comment to remind us about it.

These are different ways we could handle them:

  - in case of a cli user and if there was an interactive mode, we could
    perhaps ask if the user wants to sign again
  - we could add an option to just fail if there are signed commits

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index d28d468008..52db8e0941 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -61,7 +61,7 @@ static struct commit *create_commit(struct tree *tree,
 	struct object *obj;
 	struct commit_list *parents = NULL;
 	char *author;
-	char *sign_commit = NULL;
+	char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
 	struct commit_extra_header *extra;
 	struct strbuf msg = STRBUF_INIT;
 	const char *out_enc = get_commit_output_encoding();
-- 
2.41.0.15.ga6d88fc8f0


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v3 08/15] replay: remove progress and info output
  2023-06-02 10:25   ` [PATCH v3 " Christian Couder
                       ` (6 preceding siblings ...)
  2023-06-02 10:25     ` [PATCH v3 07/15] replay: add an important FIXME comment about gpg signing Christian Couder
@ 2023-06-02 10:25     ` Christian Couder
  2023-06-02 10:25     ` [PATCH v3 09/15] replay: remove HEAD related sanity check Christian Couder
                       ` (8 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-06-02 10:25 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

From: Elijah Newren <newren@gmail.com>

The replay command will be changed in a follow up commit, so that it
will not update refs directly, but instead it will print on stdout a
list of commands that can be consumed by `git update-ref --stdin`.

We don't want this output to be polluted by its current low value
output, so let's just remove the latter.

In the future, when the command gets an option to update refs by
itself, it will make a lot of sense to display a progress meter, but
we are not there yet.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 7 +------
 1 file changed, 1 insertion(+), 6 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 52db8e0941..2ad0835c06 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -194,7 +194,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
-	merge_opt.show_rename_progress = 1;
+	merge_opt.show_rename_progress = 0;
 	merge_opt.branch1 = "HEAD";
 	head_tree = repo_get_commit_tree(the_repository, onto);
 	result.tree = head_tree;
@@ -202,9 +202,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	while ((commit = get_revision(&revs))) {
 		struct commit *pick;
 
-		fprintf(stderr, "Rebasing %s...\r",
-			oid_to_hex(&commit->object.oid));
-
 		if (!commit->parents)
 			die(_("replaying down to root commit is not supported yet!"));
 		if (commit->parents->next)
@@ -223,7 +220,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		exit(128);
 
 	if (result.clean) {
-		fprintf(stderr, "\nDone.\n");
 		strbuf_addf(&reflog_msg, "finish rebase %s onto %s",
 			    oid_to_hex(&last_picked_commit->object.oid),
 			    oid_to_hex(&last_commit->object.oid));
@@ -237,7 +233,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		if (create_symref("HEAD", branch_name.buf, reflog_msg.buf) < 0)
 			die(_("unable to update HEAD"));
 	} else {
-		fprintf(stderr, "\nAborting: Hit a conflict.\n");
 		strbuf_addf(&reflog_msg, "rebase progress up to %s",
 			    oid_to_hex(&last_picked_commit->object.oid));
 		if (update_ref(reflog_msg.buf, "HEAD",
-- 
2.41.0.15.ga6d88fc8f0


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v3 09/15] replay: remove HEAD related sanity check
  2023-06-02 10:25   ` [PATCH v3 " Christian Couder
                       ` (7 preceding siblings ...)
  2023-06-02 10:25     ` [PATCH v3 08/15] replay: remove progress and info output Christian Couder
@ 2023-06-02 10:25     ` Christian Couder
  2023-06-02 10:25     ` [PATCH v3 10/15] replay: make it a minimal server side command Christian Couder
                       ` (7 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-06-02 10:25 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

From: Elijah Newren <newren@gmail.com>

We want replay to be a command that can be used on the server side on
any branch, not just the current one, so we are going to stop updating
HEAD in a future commit.

A "sanity check" that makes sure we are replaying the current branch
doesn't make sense anymore. Let's remove it.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 8 +-------
 1 file changed, 1 insertion(+), 7 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 2ad0835c06..3b86fe140f 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -122,7 +122,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct commit *onto;
 	const char *onto_name = NULL;
 	struct commit *last_commit = NULL, *last_picked_commit = NULL;
-	struct object_id head;
 	struct lock_file lock = LOCK_INIT;
 	struct strvec rev_walk_args = STRVEC_INIT;
 	struct rev_info revs;
@@ -161,11 +160,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	onto = peel_committish(onto_name);
 	strbuf_addf(&branch_name, "refs/heads/%s", argv[2]);
 
-	/* Sanity check */
-	if (repo_get_oid(the_repository, "HEAD", &head))
-		die(_("Cannot read HEAD"));
-	assert(oideq(&onto->object.oid, &head));
-
 	repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
 	if (repo_read_index(the_repository) < 0)
 		BUG("Could not read index");
@@ -237,7 +231,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			    oid_to_hex(&last_picked_commit->object.oid));
 		if (update_ref(reflog_msg.buf, "HEAD",
 			       &last_commit->object.oid,
-			       &head,
+			       &onto->object.oid,
 			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
 			error(_("could not update %s"), argv[2]);
 			die("Failed to update %s", argv[2]);
-- 
2.41.0.15.ga6d88fc8f0


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v3 10/15] replay: make it a minimal server side command
  2023-06-02 10:25   ` [PATCH v3 " Christian Couder
                       ` (8 preceding siblings ...)
  2023-06-02 10:25     ` [PATCH v3 09/15] replay: remove HEAD related sanity check Christian Couder
@ 2023-06-02 10:25     ` Christian Couder
  2023-06-22 10:01       ` Toon Claes
  2023-06-02 10:25     ` [PATCH v3 11/15] replay: use standard revision ranges Christian Couder
                       ` (6 subsequent siblings)
  16 siblings, 1 reply; 208+ messages in thread
From: Christian Couder @ 2023-06-02 10:25 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

From: Elijah Newren <newren@gmail.com>

We want this command to be a minimal command that just does server side
picking of commits, displaying the results on stdout for higher level
scripts to consume.

So let's simplify it:
  * remove the worktree and index reading/writing,
  * remove the ref (and reflog) updating,
  * remove the assumptions tying us to HEAD, since (a) this is not a
    rebase and (b) we want to be able to pick commits in a bare repo,
    i.e. to/from branches that are not checked out and not the main
    branch,
  * remove unneeded includes,
  * handle rebasing multiple branches by printing on stdout the update
    ref commands that should be performed.

The output can be piped into `git update-ref --stdin` for the ref
updates to happen.

In the future to make it easier for users to use this command
directly maybe an option can be added to automatically pipe its output
into `git update-ref`.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c                         | 78 ++++++++----------------
 t/t6429-merge-sequence-rename-caching.sh | 39 +++++++-----
 2 files changed, 51 insertions(+), 66 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 3b86fe140f..9385973ffc 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -6,10 +6,7 @@
 #include "git-compat-util.h"
 
 #include "builtin.h"
-#include "cache-tree.h"
-#include "commit.h"
 #include "environment.h"
-#include "gettext.h"
 #include "hex.h"
 #include "lockfile.h"
 #include "merge-ort.h"
@@ -17,8 +14,6 @@
 #include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
-#include "sequencer.h"
-#include "setup.h"
 #include "strvec.h"
 #include <oidset.h>
 #include <tree.h>
@@ -101,6 +96,7 @@ static struct commit *pick_regular_commit(struct commit *pickme,
 	pickme_tree = repo_get_commit_tree(the_repository, pickme);
 	base_tree = repo_get_commit_tree(the_repository, base);
 
+	merge_opt->branch1 = short_commit_name(last_commit);
 	merge_opt->branch2 = short_commit_name(pickme);
 	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
 
@@ -121,15 +117,12 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 {
 	struct commit *onto;
 	const char *onto_name = NULL;
-	struct commit *last_commit = NULL, *last_picked_commit = NULL;
-	struct lock_file lock = LOCK_INIT;
+	struct commit *last_commit = NULL;
 	struct strvec rev_walk_args = STRVEC_INIT;
 	struct rev_info revs;
 	struct commit *commit;
 	struct merge_options merge_opt;
-	struct tree *head_tree;
 	struct merge_result result;
-	struct strbuf reflog_msg = STRBUF_INIT;
 	struct strbuf branch_name = STRBUF_INIT;
 	int ret = 0;
 
@@ -160,10 +153,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	onto = peel_committish(onto_name);
 	strbuf_addf(&branch_name, "refs/heads/%s", argv[2]);
 
-	repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
-	if (repo_read_index(the_repository) < 0)
-		BUG("Could not read index");
-
 	repo_init_revisions(the_repository, &revs, prefix);
 
 	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
@@ -189,58 +178,45 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
 	merge_opt.show_rename_progress = 0;
-	merge_opt.branch1 = "HEAD";
-	head_tree = repo_get_commit_tree(the_repository, onto);
-	result.tree = head_tree;
+	result.tree = repo_get_commit_tree(the_repository, onto);
 	last_commit = onto;
 	while ((commit = get_revision(&revs))) {
-		struct commit *pick;
+		const struct name_decoration *decoration;
 
 		if (!commit->parents)
 			die(_("replaying down to root commit is not supported yet!"));
 		if (commit->parents->next)
 			die(_("replaying merge commits is not supported yet!"));
 
-		pick = pick_regular_commit(commit, last_commit, &merge_opt, &result);
-		if (!pick)
+		last_commit = pick_regular_commit(commit, last_commit, &merge_opt, &result);
+		if (!last_commit)
 			break;
-		last_commit = pick;
-		last_picked_commit = commit;
+
+		decoration = get_name_decoration(&commit->object);
+		if (!decoration)
+			continue;
+
+		while (decoration) {
+			if (decoration->type == DECORATION_REF_LOCAL) {
+				printf("update %s %s %s\n",
+				       decoration->name,
+				       oid_to_hex(&last_commit->object.oid),
+				       oid_to_hex(&commit->object.oid));
+			}
+			decoration = decoration->next;
+		}
 	}
 
+	/* Cleanup */
 	merge_finalize(&merge_opt, &result);
+	ret = result.clean;
 
-	if (result.clean < 0)
-		exit(128);
-
-	if (result.clean) {
-		strbuf_addf(&reflog_msg, "finish rebase %s onto %s",
-			    oid_to_hex(&last_picked_commit->object.oid),
-			    oid_to_hex(&last_commit->object.oid));
-		if (update_ref(reflog_msg.buf, branch_name.buf,
-			       &last_commit->object.oid,
-			       &last_picked_commit->object.oid,
-			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
-			error(_("could not update %s"), argv[2]);
-			die("Failed to update %s", argv[2]);
-		}
-		if (create_symref("HEAD", branch_name.buf, reflog_msg.buf) < 0)
-			die(_("unable to update HEAD"));
-	} else {
-		strbuf_addf(&reflog_msg, "rebase progress up to %s",
-			    oid_to_hex(&last_picked_commit->object.oid));
-		if (update_ref(reflog_msg.buf, "HEAD",
-			       &last_commit->object.oid,
-			       &onto->object.oid,
-			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
-			error(_("could not update %s"), argv[2]);
-			die("Failed to update %s", argv[2]);
-		}
-	}
-	ret = (result.clean == 0);
 cleanup:
-	strbuf_release(&reflog_msg);
 	strbuf_release(&branch_name);
 	release_revisions(&revs);
-	return ret;
+
+	/* Return */
+	if (ret < 0)
+		exit(128);
+	return ret ? 0 : 1;
 }
diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh
index 7670b72008..099aefeffc 100755
--- a/t/t6429-merge-sequence-rename-caching.sh
+++ b/t/t6429-merge-sequence-rename-caching.sh
@@ -71,8 +71,9 @@ test_expect_success 'caching renames does not preclude finding new ones' '
 
 		git switch upstream &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		git ls-files >tracked-files &&
 		test_line_count = 2 tracked-files &&
@@ -140,7 +141,9 @@ test_expect_success 'cherry-pick both a commit and its immediate revert' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 1 calls
@@ -198,8 +201,9 @@ test_expect_success 'rename same file identically, then reintroduce it' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		git ls-files >tracked &&
 		test_line_count = 2 tracked &&
@@ -275,8 +279,9 @@ test_expect_success 'rename same file identically, then add file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		git ls-files >tracked &&
 		test_line_count = 4 tracked &&
@@ -451,8 +456,9 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls &&
@@ -517,8 +523,9 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 3 calls &&
@@ -619,8 +626,9 @@ test_expect_success 'caching renames only on upstream side, part 1' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 1 calls &&
@@ -677,8 +685,9 @@ test_expect_success 'caching renames only on upstream side, part 2' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls &&
-- 
2.41.0.15.ga6d88fc8f0


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v3 11/15] replay: use standard revision ranges
  2023-06-02 10:25   ` [PATCH v3 " Christian Couder
                       ` (9 preceding siblings ...)
  2023-06-02 10:25     ` [PATCH v3 10/15] replay: make it a minimal server side command Christian Couder
@ 2023-06-02 10:25     ` Christian Couder
  2023-06-22 10:03       ` Toon Claes
  2023-06-02 10:25     ` [PATCH v3 12/15] replay: disallow revision specific options and pathspecs Christian Couder
                       ` (5 subsequent siblings)
  16 siblings, 1 reply; 208+ messages in thread
From: Christian Couder @ 2023-06-02 10:25 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

From: Elijah Newren <newren@gmail.com>

Instead of the fixed "<oldbase> <branch>" arguments, the replay
command now accepts "<revision-range>..." arguments in a similar
way as many other Git commands. This makes its interface more
standard and more flexible.

Also as the interface of the command is now mostly finalized,
we can add some documentation as well as testcases to make sure
the command will continue to work as designed in the future.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 Documentation/git-replay.txt             | 88 ++++++++++++++++++++++++
 builtin/replay.c                         | 21 ++----
 t/t3650-replay-basics.sh                 | 83 ++++++++++++++++++++++
 t/t6429-merge-sequence-rename-caching.sh | 18 ++---
 4 files changed, 184 insertions(+), 26 deletions(-)
 create mode 100644 Documentation/git-replay.txt
 create mode 100755 t/t3650-replay-basics.sh

diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
new file mode 100644
index 0000000000..394d7b0050
--- /dev/null
+++ b/Documentation/git-replay.txt
@@ -0,0 +1,88 @@
+git-replay(1)
+=============
+
+NAME
+----
+git-replay - Replay commits on a different base, without touching working tree
+
+
+SYNOPSIS
+--------
+[verse]
+'git replay' --onto <newbase> <revision-range>...
+
+DESCRIPTION
+-----------
+
+Takes a range of commits, and replays them onto a new location.  Does
+not touch the working tree or index, and does not update any
+references.  However, the output of this command is meant to be used
+as input to `git update-ref --stdin`, which would update the relevant
+branches.
+
+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
+
+OPTIONS
+-------
+
+--onto <newbase>::
+	Starting point at which to create the new commits.  May be any
+	valid commit, and not just an existing branch name.
++
+The update-ref commands in the output will update the branch(es)
+in the revision range to point at the new commits (in other
+words, this mimics a rebase operation).
+
+<revision-range>::
+	Range of commits to replay; see "Specifying Ranges" in
+	linkgit:git-rev-parse.
+
+OUTPUT
+------
+
+When there are no conflicts, the output of this command is usable as
+input to `git update-ref --stdin`.  It is basically of the form:
+
+	update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
+	update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
+	update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
+
+where the number of refs updated depend on the arguments passed.
+
+EXIT STATUS
+-----------
+
+For a successful, non-conflicted replay, the exit status is 0.  When
+the replay has conflicts, the exit status is 1.  If the replay is not
+able to complete (or start) due to some kind of error, the exit status
+is something other than 0 or 1.
+
+EXAMPLES
+--------
+
+To simply rebase mybranch onto target:
+
+------------
+$ git replay --onto target origin/main..mybranch
+update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
+------------
+
+When calling `git replay`, one does not need to specify a range of
+commits to replay using the syntax `A..B`; any range expression will
+do:
+
+------------
+$ git replay --onto origin/main ^base branch1 branch2 branch3
+update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
+update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
+update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
+------------
+
+This will simultaneously rebase branch1, branch2, and branch3 -- all
+commits they have since base, playing them on top of origin/main.
+These three branches may have commits on top of base that they have in
+common, but that does not need to be the case.
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/builtin/replay.c b/builtin/replay.c
index 9385973ffc..c1bd72c0e5 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -14,7 +14,6 @@
 #include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
-#include "strvec.h"
 #include <oidset.h>
 #include <tree.h>
 
@@ -118,16 +117,14 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct commit *onto;
 	const char *onto_name = NULL;
 	struct commit *last_commit = NULL;
-	struct strvec rev_walk_args = STRVEC_INIT;
 	struct rev_info revs;
 	struct commit *commit;
 	struct merge_options merge_opt;
 	struct merge_result result;
-	struct strbuf branch_name = STRBUF_INIT;
 	int ret = 0;
 
 	const char * const replay_usage[] = {
-		N_("git replay --onto <newbase> <oldbase> <branch>"),
+		N_("git replay --onto <newbase> <revision-range>..."),
 		NULL
 	};
 	struct option replay_options[] = {
@@ -145,20 +142,13 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		usage_with_options(replay_usage, replay_options);
 	}
 
-	if (argc != 3) {
-		error(_("bad number of arguments"));
-		usage_with_options(replay_usage, replay_options);
-	}
-
 	onto = peel_committish(onto_name);
-	strbuf_addf(&branch_name, "refs/heads/%s", argv[2]);
 
 	repo_init_revisions(the_repository, &revs, prefix);
 
-	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
-
-	if (setup_revisions(rev_walk_args.nr, rev_walk_args.v, &revs, NULL) > 1) {
-		ret = error(_("unhandled options"));
+	argc = setup_revisions(argc, argv, &revs, NULL);
+	if (argc > 1) {
+		ret = error(_("unrecognized argument: %s"), argv[1]);
 		goto cleanup;
 	}
 
@@ -168,8 +158,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	revs.topo_order = 1;
 	revs.simplify_history = 0;
 
-	strvec_clear(&rev_walk_args);
-
 	if (prepare_revision_walk(&revs) < 0) {
 		ret = error(_("error preparing revisions"));
 		goto cleanup;
@@ -212,7 +200,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	ret = result.clean;
 
 cleanup:
-	strbuf_release(&branch_name);
 	release_revisions(&revs);
 
 	/* Return */
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
new file mode 100755
index 0000000000..a1da4f9ef9
--- /dev/null
+++ b/t/t3650-replay-basics.sh
@@ -0,0 +1,83 @@
+#!/bin/sh
+
+test_description='basic git replay tests'
+
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
+. ./test-lib.sh
+
+GIT_AUTHOR_NAME=author@name
+GIT_AUTHOR_EMAIL=bogus@email@address
+export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL
+
+test_expect_success 'setup' '
+	test_commit A &&
+	test_commit B &&
+
+	git switch -c topic1 &&
+	test_commit C &&
+	git switch -c topic2 &&
+	test_commit D &&
+	test_commit E &&
+	git switch topic1 &&
+	test_commit F &&
+	git switch -c topic3 &&
+	test_commit G &&
+	test_commit H &&
+	git switch -c topic4 main &&
+	test_commit I &&
+	test_commit J &&
+
+	git switch -c next main &&
+	test_commit K &&
+	git merge -m "Merge topic1" topic1 &&
+	git merge -m "Merge topic2" topic2 &&
+	git merge -m "Merge topic3" topic3 &&
+	>evil &&
+	git add evil &&
+	git commit --amend &&
+	git merge -m "Merge topic4" topic4 &&
+
+	git switch main &&
+	test_commit L &&
+	test_commit M &&
+
+	git switch -c conflict B &&
+	test_commit C.conflict C.t conflict
+'
+
+test_expect_success 'setup bare' '
+	git clone --bare . bare
+'
+
+test_expect_success 'using replay to rebase two branches, one on top of other' '
+	git replay --onto main topic1..topic2 >result &&
+
+	test_line_count = 1 result &&
+
+	git log --format=%s $(cut -f 3 -d " " result) >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/topic2 " >expect &&
+	printf "%s " $(cut -f 3 -d " " result) >>expect &&
+	git rev-parse topic2 >>expect &&
+
+	test_cmp expect result
+'
+
+test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '
+	git -C bare replay --onto main topic1..topic2 >result-bare &&
+	test_cmp expect result-bare
+'
+
+test_expect_success 'using replay to rebase with a conflict' '
+	test_expect_code 1 git replay --onto topic1 B..conflict
+'
+
+test_expect_success 'using replay on bare repo to rebase with a conflict' '
+	test_expect_code 1 git -C bare replay --onto topic1 B..conflict
+'
+
+test_done
diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh
index 099aefeffc..0f39ed0d08 100755
--- a/t/t6429-merge-sequence-rename-caching.sh
+++ b/t/t6429-merge-sequence-rename-caching.sh
@@ -71,7 +71,7 @@ test_expect_success 'caching renames does not preclude finding new ones' '
 
 		git switch upstream &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -141,7 +141,7 @@ test_expect_success 'cherry-pick both a commit and its immediate revert' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -201,7 +201,7 @@ test_expect_success 'rename same file identically, then reintroduce it' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -279,7 +279,7 @@ test_expect_success 'rename same file identically, then add file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -357,7 +357,7 @@ test_expect_success 'cached dir rename does not prevent noticing later conflict'
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test_must_fail git replay --onto HEAD upstream~1 topic >output &&
+		test_must_fail git replay --onto HEAD upstream~1..topic >output &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls
@@ -456,7 +456,7 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -523,7 +523,7 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -626,7 +626,7 @@ test_expect_success 'caching renames only on upstream side, part 1' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -685,7 +685,7 @@ test_expect_success 'caching renames only on upstream side, part 2' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
-- 
2.41.0.15.ga6d88fc8f0


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v3 12/15] replay: disallow revision specific options and pathspecs
  2023-06-02 10:25   ` [PATCH v3 " Christian Couder
                       ` (10 preceding siblings ...)
  2023-06-02 10:25     ` [PATCH v3 11/15] replay: use standard revision ranges Christian Couder
@ 2023-06-02 10:25     ` Christian Couder
  2023-07-25 21:16       ` Junio C Hamano
  2023-06-02 10:25     ` [PATCH v3 13/15] replay: add --advance or 'cherry-pick' mode Christian Couder
                       ` (4 subsequent siblings)
  16 siblings, 1 reply; 208+ messages in thread
From: Christian Couder @ 2023-06-02 10:25 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder, Christian Couder

A previous commit changed `git replay` to make it accept standard
revision ranges using the setup_revisions() function. While this is a
good thing to make this command more standard and more flexible, it has
the downside of enabling many revision related options accepted and eaten
by setup_revisions().

Some of these options might make sense, but others, like those
generating non-contiguous history, might not. Anyway those we might want
to allow should probably be tested and perhaps documented a bit, which
could be done in future work.

For now it is just simpler and safer to just disallow all of them, so
let's do that.

Other commands, like `git fast-export`, currently allow all these
revision specific options even though some of them might not make sense,
as these commands also use setup_revisions() but do not check the
options that might be passed to this function.

So a way to fix those commands as well as git replay could be to improve
or refactor the setup_revisions() mechanism to let callers allow and
disallow options in a relevant way for them. Such improvements are
outside the scope of this work though.

Pathspecs, which are also accepted and eaten by setup_revisions(), are
likely to result in disconnected history. That could perhaps be useful,
but that would need tests and documentation, which can be added in
future work. So, while at it, let's disallow them too.

Helped-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c         | 26 +++++++++++++++++++++++++-
 t/t3650-replay-basics.sh | 16 ++++++++++++++++
 2 files changed, 41 insertions(+), 1 deletion(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index c1bd72c0e5..cffbf34290 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -121,7 +121,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct commit *commit;
 	struct merge_options merge_opt;
 	struct merge_result result;
-	int ret = 0;
+	int ret = 0, i;
 
 	const char * const replay_usage[] = {
 		N_("git replay --onto <newbase> <revision-range>..."),
@@ -137,6 +137,20 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
 			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
 
+	/*
+	 * TODO: For now, we reject any unknown or invalid option,
+	 * including revision related ones, like --not,
+	 * --first-parent, etc that would be allowed and eaten by
+	 * setup_revisions() below. In the future we should definitely
+	 * accept those that make sense and add related tests and doc
+	 * though.
+	 */
+	for (i = 0; i < argc; i++)
+		if (argv[i][0] == '-') {
+			error(_("invalid option: %s"), argv[i]);
+			usage_with_options(replay_usage, replay_options);
+		}
+
 	if (!onto_name) {
 		error(_("option --onto is mandatory"));
 		usage_with_options(replay_usage, replay_options);
@@ -152,6 +166,16 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		goto cleanup;
 	}
 
+	/*
+	 * Reject any pathspec. (They are allowed and eaten by
+	 * setup_revisions() above.) In the future we might accept
+	 * them, after adding related tests and doc though.
+	 */
+	if (revs.prune_data.nr) {
+		error(_("invalid pathspec: %s"), revs.prune_data.items[0].match);
+		usage_with_options(replay_usage, replay_options);
+	}
+
 	/* requirements/overrides for revs */
 	revs.reverse = 1;
 	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index a1da4f9ef9..de6e40950e 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -80,4 +80,20 @@ test_expect_success 'using replay on bare repo to rebase with a conflict' '
 	test_expect_code 1 git -C bare replay --onto topic1 B..conflict
 '
 
+test_expect_success 'using replay with (for now) disallowed revision specific option --not' '
+	test_must_fail git replay --onto main topic2 --not topic1
+'
+
+test_expect_success 'using replay on bare repo with (for now) disallowed revision specific option --first-parent' '
+	test_must_fail git -C bare replay --onto main --first-parent topic1..topic2
+'
+
+test_expect_success 'using replay with disallowed pathspec' '
+	test_must_fail git replay --onto main topic1..topic2 A.t
+'
+
+test_expect_success 'using replay on bare repo with disallowed pathspec' '
+	test_must_fail git -C bare replay --onto main topic1..topic2 -- A.t
+'
+
 test_done
-- 
2.41.0.15.ga6d88fc8f0


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v3 13/15] replay: add --advance or 'cherry-pick' mode
  2023-06-02 10:25   ` [PATCH v3 " Christian Couder
                       ` (11 preceding siblings ...)
  2023-06-02 10:25     ` [PATCH v3 12/15] replay: disallow revision specific options and pathspecs Christian Couder
@ 2023-06-02 10:25     ` Christian Couder
  2023-06-22 10:05       ` Toon Claes
  2023-07-25 21:41       ` Junio C Hamano
  2023-06-02 10:25     ` [PATCH v3 14/15] replay: add --contained to rebase contained branches Christian Couder
                       ` (3 subsequent siblings)
  16 siblings, 2 replies; 208+ messages in thread
From: Christian Couder @ 2023-06-02 10:25 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

From: Elijah Newren <newren@gmail.com>

There is already a 'rebase' mode with `--onto`. Let's add an 'advance' or
'cherry-pick' mode with `--advance`. This new mode will make the target
branch advance as we replay commits onto it.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 Documentation/git-replay.txt |  33 ++++++-
 builtin/replay.c             | 185 +++++++++++++++++++++++++++++++++--
 t/t3650-replay-basics.sh     |  26 +++++
 3 files changed, 230 insertions(+), 14 deletions(-)

diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
index 394d7b0050..439b2f92e7 100644
--- a/Documentation/git-replay.txt
+++ b/Documentation/git-replay.txt
@@ -9,7 +9,7 @@ git-replay - Replay commits on a different base, without touching working tree
 SYNOPSIS
 --------
 [verse]
-'git replay' --onto <newbase> <revision-range>...
+'git replay' (--onto <newbase> | --advance <branch>) <revision-range>...
 
 DESCRIPTION
 -----------
@@ -29,9 +29,17 @@ OPTIONS
 	Starting point at which to create the new commits.  May be any
 	valid commit, and not just an existing branch name.
 +
-The update-ref commands in the output will update the branch(es)
-in the revision range to point at the new commits (in other
-words, this mimics a rebase operation).
+When `--onto` is specified, the update-ref command(s) in the output will
+update the branch(es) in the revision range to point at the new
+commits (in other words, this mimics a rebase operation).
+
+--advance <branch>::
+	Starting point at which to create the new commits; must be a
+	branch name.
++
+When `--advance` is specified, the update-ref command(s) in the output
+will update the branch passed as an argument to `--advance` to point at
+the new commits (in other words, this mimics a cherry-pick operation).
 
 <revision-range>::
 	Range of commits to replay; see "Specifying Ranges" in
@@ -47,7 +55,10 @@ input to `git update-ref --stdin`.  It is basically of the form:
 	update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
 	update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
 
-where the number of refs updated depend on the arguments passed.
+where the number of refs updated depend on the arguments passed.  When
+using `--advance`, the number of refs updated is always one, but for
+`--onto`, it can be one or more (rebasing multiple branches
+simultaneously is supported).
 
 EXIT STATUS
 -----------
@@ -67,6 +78,18 @@ $ git replay --onto target origin/main..mybranch
 update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
 ------------
 
+To cherry-pick the commits from mybranch onto target:
+
+------------
+$ git replay --advance target origin/main..mybranch
+update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}
+------------
+
+Note that the first two examples replay the exact same commits and on
+top of the exact same new base, they only differ in that the first
+provides instructions to make mybranch point at the new commits and
+the second provides instructions to make target point at them.
+
 When calling `git replay`, one does not need to specify a range of
 commits to replay using the syntax `A..B`; any range expression will
 do:
diff --git a/builtin/replay.c b/builtin/replay.c
index cffbf34290..4b64d15159 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -14,6 +14,7 @@
 #include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
+#include "strmap.h"
 #include <oidset.h>
 #include <tree.h>
 
@@ -82,6 +83,146 @@ static struct commit *create_commit(struct tree *tree,
 	return (struct commit *)obj;
 }
 
+struct ref_info {
+	struct commit *onto;
+	struct strset positive_refs;
+	struct strset negative_refs;
+	int positive_refexprs;
+	int negative_refexprs;
+};
+
+static void get_ref_information(struct rev_cmdline_info *cmd_info,
+				struct ref_info *ref_info)
+{
+	int i;
+
+	ref_info->onto = NULL;
+	strset_init(&ref_info->positive_refs);
+	strset_init(&ref_info->negative_refs);
+	ref_info->positive_refexprs = 0;
+	ref_info->negative_refexprs = 0;
+
+	/*
+	 * When the user specifies e.g.
+	 *   git replay origin/main..mybranch
+	 *   git replay ^origin/next mybranch1 mybranch2
+	 * we want to be able to determine where to replay the commits.  In
+	 * these examples, the branches are probably based on an old version
+	 * of either origin/main or origin/next, so we want to replay on the
+	 * newest version of that branch.  In contrast we would want to error
+	 * out if they ran
+	 *   git replay ^origin/master ^origin/next mybranch
+	 *   git replay mybranch~2..mybranch
+	 * the first of those because there's no unique base to choose, and
+	 * the second because they'd likely just be replaying commits on top
+	 * of the same commit and not making any difference.
+	 */
+	for (i = 0; i < cmd_info->nr; i++) {
+		struct rev_cmdline_entry *e = cmd_info->rev + i;
+		struct object_id oid;
+		const char *refexpr = e->name;
+		char *fullname = NULL;
+		int can_uniquely_dwim = 1;
+
+		if (*refexpr == '^')
+			refexpr++;
+		if (repo_dwim_ref(the_repository, refexpr, strlen(refexpr), &oid, &fullname, 0) != 1)
+			can_uniquely_dwim = 0;
+
+		if (e->flags & BOTTOM) {
+			if (can_uniquely_dwim)
+				strset_add(&ref_info->negative_refs, fullname);
+			if (!ref_info->negative_refexprs)
+				ref_info->onto = lookup_commit_reference_gently(the_repository,
+										&e->item->oid, 1);
+			ref_info->negative_refexprs++;
+		} else {
+			if (can_uniquely_dwim)
+				strset_add(&ref_info->positive_refs, fullname);
+			ref_info->positive_refexprs++;
+		}
+
+		free(fullname);
+	}
+}
+
+static void determine_replay_mode(struct rev_cmdline_info *cmd_info,
+				  const char *onto_name,
+				  const char **advance_name,
+				  struct commit **onto,
+				  struct strset **update_refs)
+{
+	struct ref_info rinfo;
+
+	get_ref_information(cmd_info, &rinfo);
+	if (!rinfo.positive_refexprs)
+		die(_("need some commits to replay"));
+	if (onto_name && *advance_name)
+		die(_("--onto and --advance are incompatible"));
+	else if (onto_name) {
+		*onto = peel_committish(onto_name);
+		if (rinfo.positive_refexprs <
+		    strset_get_size(&rinfo.positive_refs))
+			die(_("all positive revisions given must be references"));
+	} else if (*advance_name) {
+		struct object_id oid;
+		char *fullname = NULL;
+
+		*onto = peel_committish(*advance_name);
+		if (repo_dwim_ref(the_repository, *advance_name, strlen(*advance_name),
+			     &oid, &fullname, 0) == 1) {
+			*advance_name = fullname;
+		} else {
+			die(_("argument to --advance must be a reference"));
+		}
+		if (rinfo.positive_refexprs > 1)
+			die(_("cannot advance target with multiple source branches because ordering would be ill-defined"));
+	} else {
+		int positive_refs_complete = (
+			rinfo.positive_refexprs ==
+			strset_get_size(&rinfo.positive_refs));
+		int negative_refs_complete = (
+			rinfo.negative_refexprs ==
+			strset_get_size(&rinfo.negative_refs));
+		/*
+		 * We need either positive_refs_complete or
+		 * negative_refs_complete, but not both.
+		 */
+		if (rinfo.negative_refexprs > 0 &&
+		    positive_refs_complete == negative_refs_complete)
+			die(_("cannot implicitly determine whether this is an --advance or --onto operation"));
+		if (negative_refs_complete) {
+			struct hashmap_iter iter;
+			struct strmap_entry *entry;
+
+			if (rinfo.negative_refexprs == 0)
+				die(_("all positive revisions given must be references"));
+			else if (rinfo.negative_refexprs > 1)
+				die(_("cannot implicitly determine whether this is an --advance or --onto operation"));
+			else if (rinfo.positive_refexprs > 1)
+				die(_("cannot advance target with multiple source branches because ordering would be ill-defined"));
+
+			/* Only one entry, but we have to loop to get it */
+			strset_for_each_entry(&rinfo.negative_refs,
+					      &iter, entry) {
+				*advance_name = entry->key;
+			}
+		} else { /* positive_refs_complete */
+			if (rinfo.negative_refexprs > 1)
+				die(_("cannot implicitly determine correct base for --onto"));
+			if (rinfo.negative_refexprs == 1)
+				*onto = rinfo.onto;
+		}
+	}
+	if (!*advance_name) {
+		*update_refs = xcalloc(1, sizeof(**update_refs));
+		**update_refs = rinfo.positive_refs;
+		memset(&rinfo.positive_refs, 0, sizeof(**update_refs));
+	}
+	strset_clear(&rinfo.negative_refs);
+	strset_clear(&rinfo.positive_refs);
+}
+
 static struct commit *pick_regular_commit(struct commit *pickme,
 					  struct commit *last_commit,
 					  struct merge_options *merge_opt,
@@ -114,20 +255,26 @@ static struct commit *pick_regular_commit(struct commit *pickme,
 
 int cmd_replay(int argc, const char **argv, const char *prefix)
 {
-	struct commit *onto;
+	const char *advance_name = NULL;
+	struct commit *onto = NULL;
 	const char *onto_name = NULL;
-	struct commit *last_commit = NULL;
+
 	struct rev_info revs;
+	struct commit *last_commit = NULL;
 	struct commit *commit;
 	struct merge_options merge_opt;
 	struct merge_result result;
+	struct strset *update_refs = NULL;
 	int ret = 0, i;
 
 	const char * const replay_usage[] = {
-		N_("git replay --onto <newbase> <revision-range>..."),
+		N_("git replay (--onto <newbase> | --advance <branch>) <revision-range>..."),
 		NULL
 	};
 	struct option replay_options[] = {
+		OPT_STRING(0, "advance", &advance_name,
+			   N_("branch"),
+			   N_("make replay advance given branch")),
 		OPT_STRING(0, "onto", &onto_name,
 			   N_("revision"),
 			   N_("replay onto given commit")),
@@ -151,13 +298,11 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			usage_with_options(replay_usage, replay_options);
 		}
 
-	if (!onto_name) {
-		error(_("option --onto is mandatory"));
+	if (!onto_name && !advance_name) {
+		error(_("option --onto or --advance is mandatory"));
 		usage_with_options(replay_usage, replay_options);
 	}
 
-	onto = peel_committish(onto_name);
-
 	repo_init_revisions(the_repository, &revs, prefix);
 
 	argc = setup_revisions(argc, argv, &revs, NULL);
@@ -182,6 +327,12 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	revs.topo_order = 1;
 	revs.simplify_history = 0;
 
+	determine_replay_mode(&revs.cmdline, onto_name, &advance_name,
+			      &onto, &update_refs);
+
+	if (!onto) /* FIXME: Should handle replaying down to root commit */
+		die("Replaying down to root commit is not supported yet!");
+
 	if (prepare_revision_walk(&revs) < 0) {
 		ret = error(_("error preparing revisions"));
 		goto cleanup;
@@ -190,6 +341,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
 	merge_opt.show_rename_progress = 0;
+
 	result.tree = repo_get_commit_tree(the_repository, onto);
 	last_commit = onto;
 	while ((commit = get_revision(&revs))) {
@@ -204,12 +356,15 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		if (!last_commit)
 			break;
 
+		/* Update any necessary branches */
+		if (advance_name)
+			continue;
 		decoration = get_name_decoration(&commit->object);
 		if (!decoration)
 			continue;
-
 		while (decoration) {
-			if (decoration->type == DECORATION_REF_LOCAL) {
+			if (decoration->type == DECORATION_REF_LOCAL &&
+			    strset_contains(update_refs, decoration->name)) {
 				printf("update %s %s %s\n",
 				       decoration->name,
 				       oid_to_hex(&last_commit->object.oid),
@@ -219,11 +374,23 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		}
 	}
 
+	/* In --advance mode, advance the target ref */
+	if (result.clean == 1 && advance_name) {
+		printf("update %s %s %s\n",
+		       advance_name,
+		       oid_to_hex(&last_commit->object.oid),
+		       oid_to_hex(&onto->object.oid));
+	}
+
 	/* Cleanup */
 	merge_finalize(&merge_opt, &result);
 	ret = result.clean;
 
 cleanup:
+	if (update_refs) {
+		strset_clear(update_refs);
+		free(update_refs);
+	}
 	release_revisions(&revs);
 
 	/* Return */
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index de6e40950e..bca405c431 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -96,4 +96,30 @@ test_expect_success 'using replay on bare repo with disallowed pathspec' '
 	test_must_fail git -C bare replay --onto main topic1..topic2 -- A.t
 '
 
+test_expect_success 'using replay to perform basic cherry-pick' '
+	# The differences between this test and previous ones are:
+	#   --advance vs --onto
+	# 2nd field of result is refs/heads/main vs. refs/heads/topic2
+	# 4th field of result is hash for main instead of hash for topic2
+
+	git replay --advance main topic1..topic2 >result &&
+
+	test_line_count = 1 result &&
+
+	git log --format=%s $(cut -f 3 -d " " result) >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/main " >expect &&
+	printf "%s " $(cut -f 3 -d " " result) >>expect &&
+	git rev-parse main >>expect &&
+
+	test_cmp expect result
+'
+
+test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
+	git -C bare replay --advance main topic1..topic2 >result-bare &&
+	test_cmp expect result-bare
+'
+
 test_done
-- 
2.41.0.15.ga6d88fc8f0


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v3 14/15] replay: add --contained to rebase contained branches
  2023-06-02 10:25   ` [PATCH v3 " Christian Couder
                       ` (12 preceding siblings ...)
  2023-06-02 10:25     ` [PATCH v3 13/15] replay: add --advance or 'cherry-pick' mode Christian Couder
@ 2023-06-02 10:25     ` Christian Couder
  2023-06-22 10:10       ` Toon Claes
  2023-06-02 10:25     ` [PATCH v3 15/15] replay: stop assuming replayed branches do not diverge Christian Couder
                       ` (2 subsequent siblings)
  16 siblings, 1 reply; 208+ messages in thread
From: Christian Couder @ 2023-06-02 10:25 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

From: Elijah Newren <newren@gmail.com>

Let's add a `--contained` option that can be used along with
`--onto` to rebase all the branches contained in the <revision-range>
argument.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 Documentation/git-replay.txt | 12 +++++++++++-
 builtin/replay.c             | 12 ++++++++++--
 t/t3650-replay-basics.sh     | 29 +++++++++++++++++++++++++++++
 3 files changed, 50 insertions(+), 3 deletions(-)

diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
index 439b2f92e7..6fcaa44ef2 100644
--- a/Documentation/git-replay.txt
+++ b/Documentation/git-replay.txt
@@ -9,7 +9,7 @@ git-replay - Replay commits on a different base, without touching working tree
 SYNOPSIS
 --------
 [verse]
-'git replay' (--onto <newbase> | --advance <branch>) <revision-range>...
+'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...
 
 DESCRIPTION
 -----------
@@ -90,6 +90,16 @@ top of the exact same new base, they only differ in that the first
 provides instructions to make mybranch point at the new commits and
 the second provides instructions to make target point at them.
 
+What if you have a stack of branches, one depending upon another, and
+you'd really like to rebase the whole set?
+
+------------
+$ git replay --contained --onto origin/main origin/main..tipbranch
+update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
+update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
+update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}
+------------
+
 When calling `git replay`, one does not need to specify a range of
 commits to replay using the syntax `A..B`; any range expression will
 do:
diff --git a/builtin/replay.c b/builtin/replay.c
index 4b64d15159..9b182eaa90 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -258,6 +258,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	const char *advance_name = NULL;
 	struct commit *onto = NULL;
 	const char *onto_name = NULL;
+	int contained = 0;
 
 	struct rev_info revs;
 	struct commit *last_commit = NULL;
@@ -268,7 +269,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	int ret = 0, i;
 
 	const char * const replay_usage[] = {
-		N_("git replay (--onto <newbase> | --advance <branch>) <revision-range>..."),
+		N_("git replay ([--contained] --onto <newbase> | --advance <branch>) <revision-range>..."),
 		NULL
 	};
 	struct option replay_options[] = {
@@ -278,6 +279,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		OPT_STRING(0, "onto", &onto_name,
 			   N_("revision"),
 			   N_("replay onto given commit")),
+		OPT_BOOL(0, "contained", &contained,
+			 N_("advance all branches contained in revision-range")),
 		OPT_END()
 	};
 
@@ -303,6 +306,10 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		usage_with_options(replay_usage, replay_options);
 	}
 
+	if (advance_name && contained)
+		die(_("options '%s' and '%s' cannot be used together"),
+		    "--advance", "--contained");
+
 	repo_init_revisions(the_repository, &revs, prefix);
 
 	argc = setup_revisions(argc, argv, &revs, NULL);
@@ -364,7 +371,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			continue;
 		while (decoration) {
 			if (decoration->type == DECORATION_REF_LOCAL &&
-			    strset_contains(update_refs, decoration->name)) {
+			    (contained || strset_contains(update_refs,
+							  decoration->name))) {
 				printf("update %s %s %s\n",
 				       decoration->name,
 				       oid_to_hex(&last_commit->object.oid),
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index bca405c431..3fb4167e69 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -122,4 +122,33 @@ test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
 	test_cmp expect result-bare
 '
 
+test_expect_success 'using replay to also rebase a contained branch' '
+	git replay --contained --onto main main..topic3 >result &&
+
+	test_line_count = 2 result &&
+	cut -f 3 -d " " result >new-branch-tips &&
+
+	git log --format=%s $(head -n 1 new-branch-tips) >actual &&
+	test_write_lines F C M L B A >expect &&
+	test_cmp expect actual &&
+
+	git log --format=%s $(tail -n 1 new-branch-tips) >actual &&
+	test_write_lines H G F C M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/topic1 " >expect &&
+	printf "%s " $(head -n 1 new-branch-tips) >>expect &&
+	git rev-parse topic1 >>expect &&
+	printf "update refs/heads/topic3 " >>expect &&
+	printf "%s " $(tail -n 1 new-branch-tips) >>expect &&
+	git rev-parse topic3 >>expect &&
+
+	test_cmp expect result
+'
+
+test_expect_success 'using replay on bare repo to also rebase a contained branch' '
+	git -C bare replay --contained --onto main main..topic3 >result-bare &&
+	test_cmp expect result-bare
+'
+
 test_done
-- 
2.41.0.15.ga6d88fc8f0


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v3 15/15] replay: stop assuming replayed branches do not diverge
  2023-06-02 10:25   ` [PATCH v3 " Christian Couder
                       ` (13 preceding siblings ...)
  2023-06-02 10:25     ` [PATCH v3 14/15] replay: add --contained to rebase contained branches Christian Couder
@ 2023-06-02 10:25     ` Christian Couder
  2023-06-03  1:42     ` [PATCH v3 00/15] Introduce new `git replay` command Junio C Hamano
  2023-09-07  9:25     ` [PATCH v4 " Christian Couder
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-06-02 10:25 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

From: Elijah Newren <newren@gmail.com>

The replay command is able to replay multiple branches but when some of
them are based on other replayed branches, their commit should be
replayed onto already replayed commits.

For this purpose, let's store the replayed commit and its original
commit in a key value store, so that we can easily find and reuse a
replayed commit instead of the original one.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c         | 44 ++++++++++++++++++++++++++--------
 t/t3650-replay-basics.sh | 52 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 86 insertions(+), 10 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 9b182eaa90..a06649fa6e 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -223,20 +223,33 @@ static void determine_replay_mode(struct rev_cmdline_info *cmd_info,
 	strset_clear(&rinfo.positive_refs);
 }
 
+static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,
+				    struct commit *commit,
+				    struct commit *fallback)
+{
+	khint_t pos = kh_get_oid_map(replayed_commits, commit->object.oid);
+	if (pos == kh_end(replayed_commits))
+		return fallback;
+	return kh_value(replayed_commits, pos);
+}
+
 static struct commit *pick_regular_commit(struct commit *pickme,
-					  struct commit *last_commit,
+					  kh_oid_map_t *replayed_commits,
+					  struct commit *onto,
 					  struct merge_options *merge_opt,
 					  struct merge_result *result)
 {
-	struct commit *base;
+	struct commit *base, *replayed_base;
 	struct tree *pickme_tree, *base_tree;
 
 	base = pickme->parents->item;
+	replayed_base = mapped_commit(replayed_commits, base, onto);
 
+	result->tree = repo_get_commit_tree(the_repository, replayed_base);
 	pickme_tree = repo_get_commit_tree(the_repository, pickme);
 	base_tree = repo_get_commit_tree(the_repository, base);
 
-	merge_opt->branch1 = short_commit_name(last_commit);
+	merge_opt->branch1 = short_commit_name(replayed_base);
 	merge_opt->branch2 = short_commit_name(pickme);
 	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
 
@@ -250,7 +263,7 @@ static struct commit *pick_regular_commit(struct commit *pickme,
 	merge_opt->ancestor = NULL;
 	if (!result->clean)
 		return NULL;
-	return create_commit(result->tree, pickme, last_commit);
+	return create_commit(result->tree, pickme, replayed_base);
 }
 
 int cmd_replay(int argc, const char **argv, const char *prefix)
@@ -266,6 +279,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct merge_options merge_opt;
 	struct merge_result result;
 	struct strset *update_refs = NULL;
+	kh_oid_map_t *replayed_commits;
 	int ret = 0, i;
 
 	const char * const replay_usage[] = {
@@ -348,21 +362,30 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
 	merge_opt.show_rename_progress = 0;
-
-	result.tree = repo_get_commit_tree(the_repository, onto);
 	last_commit = onto;
+	replayed_commits = kh_init_oid_map();
 	while ((commit = get_revision(&revs))) {
 		const struct name_decoration *decoration;
+		khint_t pos;
+		int hr;
 
 		if (!commit->parents)
 			die(_("replaying down to root commit is not supported yet!"));
 		if (commit->parents->next)
 			die(_("replaying merge commits is not supported yet!"));
 
-		last_commit = pick_regular_commit(commit, last_commit, &merge_opt, &result);
+		last_commit = pick_regular_commit(commit, replayed_commits, onto,
+						  &merge_opt, &result);
 		if (!last_commit)
 			break;
 
+		/* Record commit -> last_commit mapping */
+		pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr);
+		if (hr == 0)
+			BUG("Duplicate rewritten commit: %s\n",
+			    oid_to_hex(&commit->object.oid));
+		kh_value(replayed_commits, pos) = last_commit;
+
 		/* Update any necessary branches */
 		if (advance_name)
 			continue;
@@ -392,13 +415,14 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 
 	/* Cleanup */
 	merge_finalize(&merge_opt, &result);
-	ret = result.clean;
-
-cleanup:
+	kh_destroy_oid_map(replayed_commits);
 	if (update_refs) {
 		strset_clear(update_refs);
 		free(update_refs);
 	}
+	ret = result.clean;
+
+cleanup:
 	release_revisions(&revs);
 
 	/* Return */
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 3fb4167e69..5aafad448f 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -151,4 +151,56 @@ test_expect_success 'using replay on bare repo to also rebase a contained branch
 	test_cmp expect result-bare
 '
 
+test_expect_success 'using replay to rebase multiple divergent branches' '
+	git replay --onto main ^topic1 topic2 topic4 >result &&
+
+	test_line_count = 2 result &&
+	cut -f 3 -d " " result >new-branch-tips &&
+
+	git log --format=%s $(head -n 1 new-branch-tips) >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual &&
+
+	git log --format=%s $(tail -n 1 new-branch-tips) >actual &&
+	test_write_lines J I M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/topic2 " >expect &&
+	printf "%s " $(head -n 1 new-branch-tips) >>expect &&
+	git rev-parse topic2 >>expect &&
+	printf "update refs/heads/topic4 " >>expect &&
+	printf "%s " $(tail -n 1 new-branch-tips) >>expect &&
+	git rev-parse topic4 >>expect &&
+
+	test_cmp expect result
+'
+
+test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '
+	git -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&
+
+	test_line_count = 4 result &&
+	cut -f 3 -d " " result >new-branch-tips &&
+
+	>expect &&
+	for i in 2 1 3 4
+	do
+		printf "update refs/heads/topic$i " >>expect &&
+		printf "%s " $(grep topic$i result | cut -f 3 -d " ") >>expect &&
+		git -C bare rev-parse topic$i >>expect || return 1
+	done &&
+
+	test_cmp expect result &&
+
+	test_write_lines F C M L B A >expect1 &&
+	test_write_lines E D C M L B A >expect2 &&
+	test_write_lines H G F C M L B A >expect3 &&
+	test_write_lines J I M L B A >expect4 &&
+
+	for i in 1 2 3 4
+	do
+		git -C bare log --format=%s $(grep topic$i result | cut -f 3 -d " ") >actual &&
+		test_cmp expect$i actual || return 1
+	done
+'
+
 test_done
-- 
2.41.0.15.ga6d88fc8f0


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* Re: [PATCH v3 00/15] Introduce new `git replay` command
  2023-06-02 10:25   ` [PATCH v3 " Christian Couder
                       ` (14 preceding siblings ...)
  2023-06-02 10:25     ` [PATCH v3 15/15] replay: stop assuming replayed branches do not diverge Christian Couder
@ 2023-06-03  1:42     ` Junio C Hamano
  2023-06-05  7:11       ` Christian Couder
  2023-09-07  9:25     ` [PATCH v4 " Christian Couder
  16 siblings, 1 reply; 208+ messages in thread
From: Junio C Hamano @ 2023-06-03  1:42 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Patrick Steinhardt, Johannes Schindelin, Elijah Newren,
	John Cai, Derrick Stolee, Phillip Wood, Felipe Contreras,
	Calvin Wan

Christian Couder <christian.couder@gmail.com> writes:

> # Changes between v2 and v3
>
> Thanks to Elijah and Junio for their suggestions on the previous
> version! The very few and minor changes compared to v2 are:
>
> * The patch series was rebased onto master at v2.41.0.

It is good to say this than not to say it, but without "in order to
..." it does not help very much.  I was hoping "by rebasing, we can
avoid unnecessary conflicts with what happened in the upstream in
the meantime, namely, modify-remove conflict of X is now gone" or
something like that.


^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v3 00/15] Introduce new `git replay` command
  2023-06-03  1:42     ` [PATCH v3 00/15] Introduce new `git replay` command Junio C Hamano
@ 2023-06-05  7:11       ` Christian Couder
  0 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-06-05  7:11 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, Patrick Steinhardt, Johannes Schindelin, Elijah Newren,
	John Cai, Derrick Stolee, Phillip Wood, Felipe Contreras,
	Calvin Wan

On Sat, Jun 3, 2023 at 3:42 AM Junio C Hamano <gitster@pobox.com> wrote:
>
> Christian Couder <christian.couder@gmail.com> writes:
>
> > # Changes between v2 and v3
> >
> > Thanks to Elijah and Junio for their suggestions on the previous
> > version! The very few and minor changes compared to v2 are:
> >
> > * The patch series was rebased onto master at v2.41.0.
>
> It is good to say this than not to say it, but without "in order to
> ..." it does not help very much.  I was hoping "by rebasing, we can
> avoid unnecessary conflicts with what happened in the upstream in
> the meantime, namely, modify-remove conflict of X is now gone" or
> something like that.

The reason was because v2 had conflicts with a patch series from
Elijah that changed "#include ...". You asked me to squash a small
patch that fixed it, and I did squash it into v3, but I thought it was
safer to also rebase.

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v3 10/15] replay: make it a minimal server side command
  2023-06-02 10:25     ` [PATCH v3 10/15] replay: make it a minimal server side command Christian Couder
@ 2023-06-22 10:01       ` Toon Claes
  2023-09-07  8:32         ` Christian Couder
  0 siblings, 1 reply; 208+ messages in thread
From: Toon Claes @ 2023-06-22 10:01 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder


Christian Couder <christian.couder@gmail.com> writes:

> +			decoration = decoration->next;
> +		}
>  	}
>
> +	/* Cleanup */

Nit: I don't think this comment adds much value. I would argue it's more
confusing to have it, because there's a label cleanup: just a few lines
down.

>  	merge_finalize(&merge_opt, &result);
> +	ret = result.clean;

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v3 11/15] replay: use standard revision ranges
  2023-06-02 10:25     ` [PATCH v3 11/15] replay: use standard revision ranges Christian Couder
@ 2023-06-22 10:03       ` Toon Claes
  2023-09-07  8:32         ` Christian Couder
  0 siblings, 1 reply; 208+ messages in thread
From: Toon Claes @ 2023-06-22 10:03 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder


Christian Couder <christian.couder@gmail.com> writes:

> +DESCRIPTION
> +-----------
> +
> +Takes a range of commits, and replays them onto a new location.  Does
> +not touch the working tree or index, and does not update any
> +references.  However, the output of this command is meant to be used

Small suggestion here:

Takes a range of commits, and replays them onto a new location.  Does
neither touch the working tree nor index, and does not update any
references.

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v3 13/15] replay: add --advance or 'cherry-pick' mode
  2023-06-02 10:25     ` [PATCH v3 13/15] replay: add --advance or 'cherry-pick' mode Christian Couder
@ 2023-06-22 10:05       ` Toon Claes
  2023-09-07  8:35         ` Christian Couder
  2023-07-25 21:41       ` Junio C Hamano
  1 sibling, 1 reply; 208+ messages in thread
From: Toon Claes @ 2023-06-22 10:05 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder


Christian Couder <christian.couder@gmail.com> writes:

> +	/*
> +	 * When the user specifies e.g.
> +	 *   git replay origin/main..mybranch
> +	 *   git replay ^origin/next mybranch1 mybranch2

When I'm trying these, I'm getting the error:
    error: option --onto or --advance is mandatory

In what situation can I omit both --onto and --advance?

> +static void determine_replay_mode(struct rev_cmdline_info *cmd_info,
> +				  const char *onto_name,
> +				  const char **advance_name,
> +				  struct commit **onto,

Would it make sense to call this target?

> +				  struct strset **update_refs)
> +{
> +	struct ref_info rinfo;
> +
> +	get_ref_information(cmd_info, &rinfo);
> +	if (!rinfo.positive_refexprs)
> +		die(_("need some commits to replay"));
> +	if (onto_name && *advance_name)
> +		die(_("--onto and --advance are incompatible"));

Do we actually need to disallow this? I mean from git-replay's point of
view, there's no technical limitation why can cannot support both modes
at once. The update-ref commands in the output will update both target
and source branches, but it's not up to us whether that's desired.

> +	else if (onto_name) {

No need to 'else' here IMHO.

> +		*onto = peel_committish(onto_name);
> +		if (rinfo.positive_refexprs <
> +		    strset_get_size(&rinfo.positive_refs))
> +			die(_("all positive revisions given must be references"));

I tested this locally with the following command:

$ git replay --onto main OID..OID

This command didn't give any errors, neither did it return any
update-ref lines. I would have expected to hit this die().

> +	} else if (*advance_name) {
> +		struct object_id oid;
> +		char *fullname = NULL;
> +
> +		*onto = peel_committish(*advance_name);
> +		if (repo_dwim_ref(the_repository, *advance_name, strlen(*advance_name),
> +			     &oid, &fullname, 0) == 1) {
> +			*advance_name = fullname;
> +		} else {
> +			die(_("argument to --advance must be a reference"));
> +		}
> +		if (rinfo.positive_refexprs > 1)
> +			die(_("cannot advance target with multiple source branches because ordering would be ill-defined"));

The sources aren't always branches, so I suggest something like:

+			die(_("cannot advance target with multiple sources because ordering would be ill-defined"));

> +	determine_replay_mode(&revs.cmdline, onto_name, &advance_name,
> +			      &onto, &update_refs);
> +
> +	if (!onto) /* FIXME: Should handle replaying down to root commit */
> +		die("Replaying down to root commit is not supported yet!");

When I was testing locally I tried the following:

$ git replay --onto main feature

I was expecting this command to find the common ancestor automatically,
but instead I got this error. I'm fine if for now the command does not
determine the common ancestor yet, but I think we should provide a
better error for this scenario.

> +test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
> +	git -C bare replay --advance main topic1..topic2 >result-bare &&
> +	test_cmp expect result-bare
> +'
> +
>  test_done

Shall we add a test case when providing both --onto and --advance? And
one that omits both?

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v3 14/15] replay: add --contained to rebase contained branches
  2023-06-02 10:25     ` [PATCH v3 14/15] replay: add --contained to rebase contained branches Christian Couder
@ 2023-06-22 10:10       ` Toon Claes
  2023-09-07  8:37         ` Christian Couder
  0 siblings, 1 reply; 208+ messages in thread
From: Toon Claes @ 2023-06-22 10:10 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder


Christian Couder <christian.couder@gmail.com> writes:

> diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
> index 439b2f92e7..6fcaa44ef2 100644
> --- a/Documentation/git-replay.txt
> +++ b/Documentation/git-replay.txt
> @@ -9,7 +9,7 @@ git-replay - Replay commits on a different base, without touching working tree
>  SYNOPSIS
>  --------
>  [verse]
> -'git replay' (--onto <newbase> | --advance <branch>) <revision-range>...
> +'git replay' ([--contained] --onto <newbase> | --advance <branch>)
> <revision-range>...

I'm not sure we need this, or at least not right now.
I've been testing with a repo having:

* a13d9c4 (another-feature) yet another commit
* c7afc2e (HEAD -> feature) third commit
* e95cecc second commit
* f141e77 first commit
| * 7bb62ac (main) later commit
| * 506cb0a another commit
|/
* e7acac6 initial commit

I tried both commands:

$ git replay --onto main main..feature main..another-feature
$ git replay --onto main --contained main..another-feature

and they both give the same result (especially with the commit following
up this one). What is the upside of having this --contained option?

Maybe it's better to defer this patch to a separate series.

And another question, in git-rebase(1) a similar option is called
--update-refs. Would you consider reusing that name here is a good idea
for consistency?

> diff --git a/builtin/replay.c b/builtin/replay.c
> index 4b64d15159..9b182eaa90 100644
> --- a/builtin/replay.c
> +++ b/builtin/replay.c
> @@ -303,6 +306,10 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
>  		usage_with_options(replay_usage, replay_options);
>  	}
>
> +	if (advance_name && contained)
> +		die(_("options '%s' and '%s' cannot be used together"),
> +		    "--advance", "--contained");

Is there a technical issue why we don't support this? I don't really see
why we couldn't.

--
Toon

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v3 12/15] replay: disallow revision specific options and pathspecs
  2023-06-02 10:25     ` [PATCH v3 12/15] replay: disallow revision specific options and pathspecs Christian Couder
@ 2023-07-25 21:16       ` Junio C Hamano
  2023-09-07  8:33         ` Christian Couder
  0 siblings, 1 reply; 208+ messages in thread
From: Junio C Hamano @ 2023-07-25 21:16 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Patrick Steinhardt, Johannes Schindelin, Elijah Newren,
	John Cai, Derrick Stolee, Phillip Wood, Felipe Contreras,
	Calvin Wan, Christian Couder

Christian Couder <christian.couder@gmail.com> writes:

> +	/*
> +	 * Reject any pathspec. (They are allowed and eaten by
> +	 * setup_revisions() above.) In the future we might accept
> +	 * them, after adding related tests and doc though.
> +	 */
> +	if (revs.prune_data.nr) {
> +		error(_("invalid pathspec: %s"), revs.prune_data.items[0].match);

This made me waste a few minutes wondering if and how I misspelt my
pathspec elements.  If we mean "no pathspec is allowed", we should
say so instead.

> +		usage_with_options(replay_usage, replay_options);
> +	}
> +
>  	/* requirements/overrides for revs */
>  	revs.reverse = 1;
>  	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
> index a1da4f9ef9..de6e40950e 100755
> --- a/t/t3650-replay-basics.sh
> +++ b/t/t3650-replay-basics.sh
> @@ -80,4 +80,20 @@ test_expect_success 'using replay on bare repo to rebase with a conflict' '
>  	test_expect_code 1 git -C bare replay --onto topic1 B..conflict
>  '
>  
> +test_expect_success 'using replay with (for now) disallowed revision specific option --not' '
> +	test_must_fail git replay --onto main topic2 --not topic1
> +'
> +
> +test_expect_success 'using replay on bare repo with (for now) disallowed revision specific option --first-parent' '
> +	test_must_fail git -C bare replay --onto main --first-parent topic1..topic2
> +'
> +
> +test_expect_success 'using replay with disallowed pathspec' '
> +	test_must_fail git replay --onto main topic1..topic2 A.t
> +'
> +
> +test_expect_success 'using replay on bare repo with disallowed pathspec' '
> +	test_must_fail git -C bare replay --onto main topic1..topic2 -- A.t
> +'
> +
>  test_done

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v3 13/15] replay: add --advance or 'cherry-pick' mode
  2023-06-02 10:25     ` [PATCH v3 13/15] replay: add --advance or 'cherry-pick' mode Christian Couder
  2023-06-22 10:05       ` Toon Claes
@ 2023-07-25 21:41       ` Junio C Hamano
  2023-09-07  8:35         ` Christian Couder
  1 sibling, 1 reply; 208+ messages in thread
From: Junio C Hamano @ 2023-07-25 21:41 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Patrick Steinhardt, Johannes Schindelin, Elijah Newren,
	John Cai, Derrick Stolee, Phillip Wood, Felipe Contreras,
	Calvin Wan, Christian Couder

Christian Couder <christian.couder@gmail.com> writes:

> There is already a 'rebase' mode with `--onto`. Let's add an 'advance' or
> 'cherry-pick' mode with `--advance`. This new mode will make the target
> branch advance as we replay commits onto it.

If I say

	$ git replay --(advance|onto) xyzzy frotz..nitfol yomin

where the topology of the cherry-picked range look like this

                      x---x---Y yomin
                     /
            E---F---x----x----N nitfol
           /  frotz
          /
         X xyzzy     

after transplanting the commits, we would get something like

                      x---x---Y yomin
                     /
            E---F---x----x----N nitfol
           /  frotz
          /
         X---x----x----N'
              \
               x---x---Y'

Now, if this was done with --onto, nitfol and yomin would point at
N' and Y', but with --advance, where would xyzzy go?

Yes, my point is that without --advance, there always is a
reasonable set of branch tips that will be moved, but with
"--advance", you cannot guarantee that you have any reasonable
answer to that question.

The answer could be "when there is no single 'tip of the new
history', the command with '--advance' errors out", but whatever
behaviour we choose, it should be documented.

> @@ -29,9 +29,17 @@ OPTIONS
>  	Starting point at which to create the new commits.  May be any
>  	valid commit, and not just an existing branch name.
>  +
> -The update-ref commands in the output will update the branch(es)
> -in the revision range to point at the new commits (in other
> -words, this mimics a rebase operation).
> +When `--onto` is specified, the update-ref command(s) in the output will
> +update the branch(es) in the revision range to point at the new
> +commits (in other words, this mimics a rebase operation).
> +
> +--advance <branch>::
> +	Starting point at which to create the new commits; must be a
> +	branch name.
> ++
> +When `--advance` is specified, the update-ref command(s) in the output
> +will update the branch passed as an argument to `--advance` to point at
> +the new commits (in other words, this mimics a cherry-pick operation).

This part is not giving much useful information to determine the
answer (which might be fine, as long as the answer can easily be
found in some other parts of this document, but I would have
expected everything necessary would be found here or one item before
this one about --onto).

> @@ -47,7 +55,10 @@ input to `git update-ref --stdin`.  It is basically of the form:
>  	update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
>  	update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
>  
> -where the number of refs updated depend on the arguments passed.
> +where the number of refs updated depend on the arguments passed.  When
> +using `--advance`, the number of refs updated is always one, but for
> +`--onto`, it can be one or more (rebasing multiple branches
> +simultaneously is supported).

"dependS on the arguments passed" is not incorrect per-se, in the
sense that if you "replay --onto X E..N" (in the above picture), I
think you'll move F and N (two), while "F..N" will only move N
(one).  But the important thing to convey is how many branches had
their tips in the replayed range, no?  "depends on the shape of the
history being replayed" would be a more correct thing to say for the
"--onto" mode.  For "--advance", presumably you would require to
have a single positive endpoint [*], so "depends on the arguments"
is still not wrong per-se, because "when --advance is part of the
arguments, the number becomes one".

	Side note: even then, I suspect that

		git replay --advance X E..F N

        should be allowed, simply because there is only one sensible
        interpretation.  You'll end up with a single strand of
        pearls F--x--x--N transplanted on top of X, and the range
        happens to contain F and N but it is obvious the end result
        should advance xyzzy to N because F fast-forwards to N.

I'd say "where the number of ..." and everything after these sample
"update" lines should be removed, and instead we should add a few
words to the main description of the options, e.g.  for "--onto"

> +When `--onto` is specified, the update-ref command(s) in the output will
> +update the branch(es) in the revision range to point at the new
> +commits (in other words, this mimics a rebase operation).

we could update the above to something like

    ... will update the branches in the revision range to point at the
    new commits, similar to the way how "rebase --update-refs" updates
    multiple branches in the affected range.


^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH 11/14] replay: use standard revision ranges
  2023-04-30  6:45                       ` Elijah Newren
@ 2023-09-03 15:47                         ` Johannes Schindelin
  2023-09-07  8:39                           ` Christian Couder
  0 siblings, 1 reply; 208+ messages in thread
From: Johannes Schindelin @ 2023-09-03 15:47 UTC (permalink / raw)
  To: Elijah Newren
  Cc: Derrick Stolee, Christian Couder, git, Junio C Hamano,
	Patrick Steinhardt, John Cai, Christian Couder

[-- Attachment #1: Type: text/plain, Size: 2812 bytes --]

Hi Elijah & Stolee,

On Sat, 29 Apr 2023, Elijah Newren wrote:

> On Mon, Apr 24, 2023 at 8:23 AM Derrick Stolee <derrickstolee@github.com> wrote:
> >
> > On 4/22/2023 9:18 PM, Elijah Newren wrote:
> > > On Thu, Apr 20, 2023 at 6:44 AM Derrick Stolee <derrickstolee@github.com> wrote:
> > >>
> > >> On 4/20/2023 12:53 AM, Elijah Newren wrote:
> > >>> On Tue, Apr 18, 2023 at 6:10 AM Derrick Stolee <derrickstolee@github.com> wrote:
> >
> > >>  3. (Ordering options) Modifications to how those commits are ordered,
> > >>     such as --topo-order, --date-order, and --reverse. These seem to
> > >>     be overridden by git-replay (although, --reverse probably causes
> > >>     some confusion right now).
> > >
> > > Yep, intentionally overridden.
> > >
> > > Could you elaborate on what you mean by --reverse causing confusion?
> >
> > It's very unlikely that a list of patches will successfully apply
> > when applied in the reverse order. If we allow the argument, then
> > someone will try it thinking they can flip their commits. Then they
> > might be surprised when there are a bunch of conflicts on every
> > commit.
> >
> > Basically, I'm not super thrilled about exposing options that are
> > unlikely to be valuable to users and instead are more likely to cause
> > confusion due to changes that won't successfully apply.
>
> Oh, I got thrown by the "right now" portion of your comment; I
> couldn't see how time or future changes would affect anything to make
> it less (or more) confusing for users.
>
> Quick clarification, though: while you correctly point out the type of
> confusion the user would experience without my overriding, my
> overriding of rev.reverse (after setup_revisions() returns, not before
> it is called) precludes that experience.  The override means none of
> the above happens, and they would instead just wonder why their option
> is being ignored.

FWIW here is my view on the matter: `git replay`, at least in its current
incarnation, is a really low-level tool. As such, I actually do not want
to worry much about protecting users from nonsensical invocations.

In that light, I would like to see that code rejecting all revision
options except `--diff-algorithm` be dropped. Should we ever decide to add
a non-low-level mode to `git replay`, we can easily add some user-friendly
sanity check of the options then, and only for that non-low-level code.
For now, I feel that it's just complicating things, and `git replay` is in
the experimental phase anyway.

And further, I would even like to see that `--reverse` override go, and
turn it into `revs.reverse = !revs.reverse` instead. (And yes, I can
easily think of instances where I would have wanted to reverse a series of
patches...).

Ciao,
Johannes

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v3 10/15] replay: make it a minimal server side command
  2023-06-22 10:01       ` Toon Claes
@ 2023-09-07  8:32         ` Christian Couder
  0 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-09-07  8:32 UTC (permalink / raw)
  To: Toon Claes
  Cc: git, Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

On Thu, Jun 22, 2023 at 12:01 PM Toon Claes <toon@iotcl.com> wrote:
>
> Christian Couder <christian.couder@gmail.com> writes:
>
> > +                     decoration = decoration->next;
> > +             }
> >       }
> >
> > +     /* Cleanup */
>
> Nit: I don't think this comment adds much value. I would argue it's more
> confusing to have it, because there's a label cleanup: just a few lines
> down.

Ok, I have removed that comment in the version 4 I will send soon.

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v3 11/15] replay: use standard revision ranges
  2023-06-22 10:03       ` Toon Claes
@ 2023-09-07  8:32         ` Christian Couder
  2023-09-07 21:02           ` Dragan Simic
  0 siblings, 1 reply; 208+ messages in thread
From: Christian Couder @ 2023-09-07  8:32 UTC (permalink / raw)
  To: Toon Claes
  Cc: git, Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

On Thu, Jun 22, 2023 at 12:03 PM Toon Claes <toon@iotcl.com> wrote:
>
> Christian Couder <christian.couder@gmail.com> writes:
>
> > +DESCRIPTION
> > +-----------
> > +
> > +Takes a range of commits, and replays them onto a new location.  Does
> > +not touch the working tree or index, and does not update any
> > +references.  However, the output of this command is meant to be used
>
> Small suggestion here:
>
> Takes a range of commits, and replays them onto a new location.  Does
> neither touch the working tree nor index, and does not update any
> references.

I am not a native speaker, so I am not sure what's best here. I find
your suggestion a bit less clear though, so until a native speaker
agrees with it or maybe finds something even better, I prefer to leave
it as-is.

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v3 12/15] replay: disallow revision specific options and pathspecs
  2023-07-25 21:16       ` Junio C Hamano
@ 2023-09-07  8:33         ` Christian Couder
  0 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-09-07  8:33 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, Patrick Steinhardt, Johannes Schindelin, Elijah Newren,
	John Cai, Derrick Stolee, Phillip Wood, Felipe Contreras,
	Calvin Wan, Christian Couder

On Tue, Jul 25, 2023 at 11:16 PM Junio C Hamano <gitster@pobox.com> wrote:
>
> Christian Couder <christian.couder@gmail.com> writes:
>
> > +     /*
> > +      * Reject any pathspec. (They are allowed and eaten by
> > +      * setup_revisions() above.) In the future we might accept
> > +      * them, after adding related tests and doc though.
> > +      */
> > +     if (revs.prune_data.nr) {
> > +             error(_("invalid pathspec: %s"), revs.prune_data.items[0].match);
>
> This made me waste a few minutes wondering if and how I misspelt my
> pathspec elements.  If we mean "no pathspec is allowed", we should
> say so instead.

Yeah, right. I have changed this to:

            error(_("no pathspec is allowed: '%s'"),
revs.prune_data.items[0].match);

> > +             usage_with_options(replay_usage, replay_options);
> > +     }

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v3 13/15] replay: add --advance or 'cherry-pick' mode
  2023-06-22 10:05       ` Toon Claes
@ 2023-09-07  8:35         ` Christian Couder
  0 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-09-07  8:35 UTC (permalink / raw)
  To: Toon Claes
  Cc: git, Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

On Thu, Jun 22, 2023 at 12:09 PM Toon Claes <toon@iotcl.com> wrote:
>
>
> Christian Couder <christian.couder@gmail.com> writes:
>
> > +     /*
> > +      * When the user specifies e.g.
> > +      *   git replay origin/main..mybranch
> > +      *   git replay ^origin/next mybranch1 mybranch2
>
> When I'm trying these, I'm getting the error:
>     error: option --onto or --advance is mandatory
>
> In what situation can I omit both --onto and --advance?

It was possible with version 1 of this series as one of the patches
allowed the command to guess the base:

https://lore.kernel.org/git/20230407072415.1360068-13-christian.couder@gmail.com/

so --onto wasn't needed to specify it.

Comments on that patch said that it might be better to focus on a
plumbing command first and for that the patch wasn't needed, so I
removed it in version 2.

> > +static void determine_replay_mode(struct rev_cmdline_info *cmd_info,
> > +                               const char *onto_name,
> > +                               const char **advance_name,
> > +                               struct commit **onto,
>
> Would it make sense to call this target?

This is more the new base we are rebasing target branches onto. So if
we want to change the name, `--base` or `--newbase` would make more
sense. `git rebase` already has `--onto` though, so, if we want to be
consistent with it, we should keep `--onto` for this.

> > +                               struct strset **update_refs)
> > +{
> > +     struct ref_info rinfo;
> > +
> > +     get_ref_information(cmd_info, &rinfo);
> > +     if (!rinfo.positive_refexprs)
> > +             die(_("need some commits to replay"));
> > +     if (onto_name && *advance_name)
> > +             die(_("--onto and --advance are incompatible"));
>
> Do we actually need to disallow this? I mean from git-replay's point of
> view, there's no technical limitation why can cannot support both modes
> at once. The update-ref commands in the output will update both target
> and source branches, but it's not up to us whether that's desired.

I am not sure what you call "target" and "source" branches. Anyway
here is in simple terms the way the command works:

  1) it takes either `--onto <newbase>` or `--advance <branch>` and
then one or more <revision-range>,
  2) it replays all the commits in the <revision-range> onto either
<newbase> or <branch>,
  3) in case of `--advance`, it outputs a single command for `git
update-ref --stdin` to advance <branch> to the last commit that was
just replayed,
  4) in case of `--onto`, it outputs a number of commands for `git
update-ref --stdin` to update the branches in <revision-range> to
where the tip commits of these branches have been replayed.

So `--advance` is like a cherry-pick, and `--onto` is like a rebase.

It would be possible to do both a rebase onto a branch and a
cherry-pick of the rebased commits onto that branch, but this is not
very common and you can achieve the same result by just rebasing and
then using `git reset` or `git update-ref` to make the branch point to
the result of the rebase. So I don't see the point of complicating the
command at this point.

> > +     else if (onto_name) {
>
> No need to 'else' here IMHO.
>
> > +             *onto = peel_committish(onto_name);
> > +             if (rinfo.positive_refexprs <
> > +                 strset_get_size(&rinfo.positive_refs))
> > +                     die(_("all positive revisions given must be references"));
>
> I tested this locally with the following command:
>
> $ git replay --onto main OID..OID
>
> This command didn't give any errors, neither did it return any
> update-ref lines. I would have expected to hit this die().

Yeah, this might be unexpected.

I tested it too and 'rinfo.positive_refexprs' is 1 while
'strset_get_size(&rinfo.positive_refs)' is 0 with such a command.

The result does not look wrong though. Above that code there is:

    if (!rinfo.positive_refexprs)
        die(_("need some commits to replay"));

so it looks like there is at least a check that the revision range
passed to the command contains positive commits.

It might be possible that users prefer a command that outputs nothing
when there is nothing to replay instead of erroring out.

> > +     } else if (*advance_name) {
> > +             struct object_id oid;
> > +             char *fullname = NULL;
> > +
> > +             *onto = peel_committish(*advance_name);
> > +             if (repo_dwim_ref(the_repository, *advance_name, strlen(*advance_name),
> > +                          &oid, &fullname, 0) == 1) {
> > +                     *advance_name = fullname;
> > +             } else {
> > +                     die(_("argument to --advance must be a reference"));
> > +             }
> > +             if (rinfo.positive_refexprs > 1)
> > +                     die(_("cannot advance target with multiple source branches because ordering would be ill-defined"));
>
> The sources aren't always branches, so I suggest something like:
>
> +                       die(_("cannot advance target with multiple sources because ordering would be ill-defined"));

Yeah, that looks reasonable. I have made this change in version 4 I
will send very soon.

> > +     determine_replay_mode(&revs.cmdline, onto_name, &advance_name,
> > +                           &onto, &update_refs);
> > +
> > +     if (!onto) /* FIXME: Should handle replaying down to root commit */
> > +             die("Replaying down to root commit is not supported yet!");
>
> When I was testing locally I tried the following:
>
> $ git replay --onto main feature
>
> I was expecting this command to find the common ancestor automatically,
> but instead I got this error. I'm fine if for now the command does not
> determine the common ancestor yet, but I think we should provide a
> better error for this scenario.

I agree that it isn't very user friendly. We could indeed try to find
if there is a common ancestor, and, if that's the case, suggest
another way to call the command. This is a plumbing command in its
early stage though for now. So I guess it's Ok to postpone working on
nicer error messages.

> > +test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
> > +     git -C bare replay --advance main topic1..topic2 >result-bare &&
> > +     test_cmp expect result-bare
> > +'
> > +
> >  test_done
>
> Shall we add a test case when providing both --onto and --advance? And
> one that omits both?

Ok, I have made this change in version 4.

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v3 13/15] replay: add --advance or 'cherry-pick' mode
  2023-07-25 21:41       ` Junio C Hamano
@ 2023-09-07  8:35         ` Christian Couder
  0 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-09-07  8:35 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, Patrick Steinhardt, Johannes Schindelin, Elijah Newren,
	John Cai, Derrick Stolee, Phillip Wood, Felipe Contreras,
	Calvin Wan, Christian Couder

On Tue, Jul 25, 2023 at 11:41 PM Junio C Hamano <gitster@pobox.com> wrote:
>
> Christian Couder <christian.couder@gmail.com> writes:
>
> > There is already a 'rebase' mode with `--onto`. Let's add an 'advance' or
> > 'cherry-pick' mode with `--advance`. This new mode will make the target
> > branch advance as we replay commits onto it.
>
> If I say
>
>         $ git replay --(advance|onto) xyzzy frotz..nitfol yomin
>
> where the topology of the cherry-picked range look like this
>
>                       x---x---Y yomin
>                      /
>             E---F---x----x----N nitfol
>            /  frotz
>           /
>          X xyzzy
>
> after transplanting the commits, we would get something like
>
>                       x---x---Y yomin
>                      /
>             E---F---x----x----N nitfol
>            /  frotz
>           /
>          X---x----x----N'
>               \
>                x---x---Y'
>
> Now, if this was done with --onto, nitfol and yomin would point at
> N' and Y', but with --advance, where would xyzzy go?
>
> Yes, my point is that without --advance, there always is a
> reasonable set of branch tips that will be moved, but with
> "--advance", you cannot guarantee that you have any reasonable
> answer to that question.
>
> The answer could be "when there is no single 'tip of the new
> history', the command with '--advance' errors out", but whatever
> behaviour we choose, it should be documented.

Ok, I have improved the commit message by adding the following:

"The replayed commits should have a single tip, so that it's clear where
the target branch should be advanced. If they have more than one tip,
this new mode will error out."

I have also updated the doc for <revision-range> like this:

"<revision-range>::
    Range of commits to replay. More than one <revision-range> can
    be passed, but in `--advance <branch>` mode, they should have
    a single tip, so that it's clear where <branch> should point
    to. See "Specifying Ranges" in linkgit:git-rev-parse."

> > +--advance <branch>::
> > +     Starting point at which to create the new commits; must be a
> > +     branch name.
> > ++
> > +When `--advance` is specified, the update-ref command(s) in the output
> > +will update the branch passed as an argument to `--advance` to point at
> > +the new commits (in other words, this mimics a cherry-pick operation).
>
> This part is not giving much useful information to determine the
> answer (which might be fine, as long as the answer can easily be
> found in some other parts of this document, but I would have
> expected everything necessary would be found here or one item before
> this one about --onto).

The doc about <revision-range> is just after the above, so I think the
above change in the <revision-range> doc is Ok and enough here.

> > @@ -47,7 +55,10 @@ input to `git update-ref --stdin`.  It is basically of the form:
> >       update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
> >       update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
> >
> > -where the number of refs updated depend on the arguments passed.
> > +where the number of refs updated depend on the arguments passed.  When
> > +using `--advance`, the number of refs updated is always one, but for
> > +`--onto`, it can be one or more (rebasing multiple branches
> > +simultaneously is supported).
>
> "dependS on the arguments passed" is not incorrect per-se, in the
> sense that if you "replay --onto X E..N" (in the above picture), I
> think you'll move F and N (two), while "F..N" will only move N
> (one).  But the important thing to convey is how many branches had
> their tips in the replayed range, no?  "depends on the shape of the
> history being replayed" would be a more correct thing to say for the
> "--onto" mode.  For "--advance", presumably you would require to
> have a single positive endpoint [*], so "depends on the arguments"
> is still not wrong per-se, because "when --advance is part of the
> arguments, the number becomes one".

Yeah, I agree.

>         Side note: even then, I suspect that
>
>                 git replay --advance X E..F N
>
>         should be allowed, simply because there is only one sensible
>         interpretation.  You'll end up with a single strand of
>         pearls F--x--x--N transplanted on top of X, and the range
>         happens to contain F and N but it is obvious the end result
>         should advance xyzzy to N because F fast-forwards to N.
>
> I'd say "where the number of ..." and everything after these sample
> "update" lines should be removed,

I am not sure it's a good thing to remove that, as I think repeating
how things work in the context of an example output can help people
understand. I have updated these sentences to the following:

"where the number of refs updated depends on the arguments passed and
the shape of the history being replayed.  When using `--advance`, the
number of refs updated is always one, but for `--onto`, it can be one
or more (rebasing multiple branches simultaneously is supported)."

> and instead we should add a few
> words to the main description of the options, e.g.  for "--onto"
>
> > +When `--onto` is specified, the update-ref command(s) in the output will
> > +update the branch(es) in the revision range to point at the new
> > +commits (in other words, this mimics a rebase operation).
>
> we could update the above to something like
>
>     ... will update the branches in the revision range to point at the
>     new commits, similar to the way how "rebase --update-refs" updates
>     multiple branches in the affected range.

Yeah, I agree. In the version 4 I will send soon, have updated the above to:

"When `--onto` is specified, the update-ref command(s) in the output will
update the branch(es) in the revision range to point at the new
commits, similar to the way how `git rebase --update-refs` updates

multiple branches in the affected range."

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v3 14/15] replay: add --contained to rebase contained branches
  2023-06-22 10:10       ` Toon Claes
@ 2023-09-07  8:37         ` Christian Couder
  0 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-09-07  8:37 UTC (permalink / raw)
  To: Toon Claes
  Cc: git, Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood,
	Felipe Contreras, Calvin Wan, Christian Couder

On Thu, Jun 22, 2023 at 12:13 PM Toon Claes <toon@iotcl.com> wrote:
>
>
> Christian Couder <christian.couder@gmail.com> writes:
>
> > diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
> > index 439b2f92e7..6fcaa44ef2 100644
> > --- a/Documentation/git-replay.txt
> > +++ b/Documentation/git-replay.txt
> > @@ -9,7 +9,7 @@ git-replay - Replay commits on a different base, without touching working tree
> >  SYNOPSIS
> >  --------
> >  [verse]
> > -'git replay' (--onto <newbase> | --advance <branch>) <revision-range>...
> > +'git replay' ([--contained] --onto <newbase> | --advance <branch>)
> > <revision-range>...
>
> I'm not sure we need this, or at least not right now.
> I've been testing with a repo having:
>
> * a13d9c4 (another-feature) yet another commit
> * c7afc2e (HEAD -> feature) third commit
> * e95cecc second commit
> * f141e77 first commit
> | * 7bb62ac (main) later commit
> | * 506cb0a another commit
> |/
> * e7acac6 initial commit
>
> I tried both commands:
>
> $ git replay --onto main main..feature main..another-feature
> $ git replay --onto main --contained main..another-feature
>
> and they both give the same result (especially with the commit following
> up this one). What is the upside of having this --contained option?

This is expected. The thing is that:

$ git replay --onto main main..another-feature

will only output something to update 'another-feature'

while:

$ git replay --onto main --contained main..another-feature

will output something to update 'another-feature' and also something
to update 'feature'.

So when you use --contained you don't need to first find the other
branches like 'feature' that point to commits between 'main' and
'another-feature', as --contained will find them for you.

> Maybe it's better to defer this patch to a separate series.

I am not sure why you are proposing this. It's true that there are
other means to achieve the same thing as --contained, but that doesn't
mean that it cannot be useful already. If there were things that
needed to be more polished in this feature, then maybe leaving it for
a separate series later might allow this series to graduate while
--contained is polished, but I don't think we are in this case.

> And another question, in git-rebase(1) a similar option is called
> --update-refs. Would you consider reusing that name here is a good idea
> for consistency?

`git replay` outputs commands that can be passed to `git update-ref
--stdin`, but for now it doesn't run that command itself. So no refs
are actually updated. If we ever add an option for git replay to also
update the refs, it would have a name probably quite similar to
--update-refs so it would be unfortunate if --update-refs is already
used for something else.

--contained also tells about the fact that the branches affected by
the option are "contained" in the revision range that is passed, which
is nice.

In short I think it's just unfortunate that git rebase already uses
--update-refs for the "contained" branches, as it would be likely to
confuse people if we would use it here for that.

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH 11/14] replay: use standard revision ranges
  2023-09-03 15:47                         ` Johannes Schindelin
@ 2023-09-07  8:39                           ` Christian Couder
  2023-09-07 10:22                             ` Johannes Schindelin
  0 siblings, 1 reply; 208+ messages in thread
From: Christian Couder @ 2023-09-07  8:39 UTC (permalink / raw)
  To: Johannes Schindelin
  Cc: Elijah Newren, Derrick Stolee, git, Junio C Hamano,
	Patrick Steinhardt, John Cai, Christian Couder

Hi Dscho,

On Sun, Sep 3, 2023 at 5:47 PM Johannes Schindelin
<Johannes.Schindelin@gmx.de> wrote:
>
> Hi Elijah & Stolee,
>
> On Sat, 29 Apr 2023, Elijah Newren wrote:
>
> > On Mon, Apr 24, 2023 at 8:23 AM Derrick Stolee <derrickstolee@github.com> wrote:

> > > Basically, I'm not super thrilled about exposing options that are
> > > unlikely to be valuable to users and instead are more likely to cause
> > > confusion due to changes that won't successfully apply.
> >
> > Oh, I got thrown by the "right now" portion of your comment; I
> > couldn't see how time or future changes would affect anything to make
> > it less (or more) confusing for users.
> >
> > Quick clarification, though: while you correctly point out the type of
> > confusion the user would experience without my overriding, my
> > overriding of rev.reverse (after setup_revisions() returns, not before
> > it is called) precludes that experience.  The override means none of
> > the above happens, and they would instead just wonder why their option
> > is being ignored.
>
> FWIW here is my view on the matter: `git replay`, at least in its current
> incarnation, is a really low-level tool. As such, I actually do not want
> to worry much about protecting users from nonsensical invocations.
>
> In that light, I would like to see that code rejecting all revision
> options except `--diff-algorithm` be dropped. Should we ever decide to add
> a non-low-level mode to `git replay`, we can easily add some user-friendly
> sanity check of the options then, and only for that non-low-level code.
> For now, I feel that it's just complicating things, and `git replay` is in
> the experimental phase anyway.

I would be Ok with removing the patch (called "replay: disallow
revision specific options and pathspecs")
that rejects all revision options and pathspecs if there is a
consensus for that. It might not simplify things too much if there is
still an exception for `--diff-algorithm` though. Also it's not clear
if you are Ok with allowing pathspecs or not.

The idea with disallowing all of them was to later add back those that
make sense along with tests and maybe docs to explain them in the
context of this command. It was not to disallow them permanently. So I
would think the best path forward would be a patch series on top of
this one that would revert the patch disallowing these options and
maybe pathspecs, and instead allow most of them and document and test
things a bit.

> And further, I would even like to see that `--reverse` override go, and
> turn it into `revs.reverse = !revs.reverse` instead. (And yes, I can
> easily think of instances where I would have wanted to reverse a series of
> patches...).

I think this might deserve docs and tests too, so it might want to be
part of a separate patch series once the existing one has graduated.

At this point I don't think it's worth delaying this patch series for
relatively small issues like this. There are many different ways this
new command can be polished and improved. The important thing is that
it looks like we all agree that the new command makes sense and should
have roughly the basic set of features that Elijah originally
implemented, so let's go with this, and then we can improve and
iterate on top of this.

^ permalink raw reply	[flat|nested] 208+ messages in thread

* [PATCH v4 00/15] Introduce new `git replay` command
  2023-06-02 10:25   ` [PATCH v3 " Christian Couder
                       ` (15 preceding siblings ...)
  2023-06-03  1:42     ` [PATCH v3 00/15] Introduce new `git replay` command Junio C Hamano
@ 2023-09-07  9:25     ` Christian Couder
  2023-09-07  9:25       ` [PATCH v4 01/15] t6429: remove switching aspects of fast-rebase Christian Couder
                         ` (16 more replies)
  16 siblings, 17 replies; 208+ messages in thread
From: Christian Couder @ 2023-09-07  9:25 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Christian Couder

# Intro

`git replay` has initially been developed entirely by Elijah Newren
mostly last year (2022) at:

https://github.com/newren/git/commits/replay

I took over this year to polish and upstream it as GitLab is
interested in replacing libgit2, and for that purpose needs a command
to do server side (so without using a worktree) rebases, cherry-picks
and reverts.

I reduced the number of commits and features in this first patch
series, compared to what Elijah already developed. Especially I
stopped short of replaying merge commits and replaying
interactively. These and other features might be upstreamed in the
future after this patch series has graduated.

The focus in this series is to make it a good plumbing command that
can already be used server side and that replaces the "fast-rebase"
test-tool command. So things to make it easier to use on the command
line, and more advanced features (like replaying merges) are left out.

It looks like GitHub has actually already been using version 3 of this
patch series in production with good results. See:

https://github.blog/2023-07-27-scaling-merge-ort-across-github/
https://lore.kernel.org/git/304f2a49-5e05-7655-9f87-2011606df5db@gmx.de/

# Content of this cover letter

The "Quick Overview" and "Reasons for diverging from cherry-pick &
rebase" sections just below are describing the purpose of the new
command in the big scheme of things. They are taken from Elijah's
design notes
(https://github.com/newren/git/blob/replay/replay-design-notes.txt)
and describe what we want this command to become and the reasons for
that, not what the command is after only this patch series. Also these
design notes were written at least one year ago, so parts of those 2
sections are not true anymore. I have added Phillip Wood's or Felipe
Contreras' notes (thanks to them) where that's the case, but some now
flawed parts may have missed.

After these two sections, starting with the "Important limitations"
section, you will find sections describing what is actually in this
patch series.

More interesting material is available in Elijah's design notes like
an "Intro via examples"
(https://github.com/newren/git/blob/replay/replay-design-notes.txt#L37-L132),
a discussion about "Preserving topology, replaying merges"
(https://github.com/newren/git/blob/replay/replay-design-notes.txt#L264-L341)
and a "Current status" section describing Elijah's work
(https://github.com/newren/git/blob/replay/replay-design-notes.txt#L344-L392)
before I started working on upstreaming it.

I have not included this material here though, as the documentation
added by this patch series for the `git replay` command already
includes an "EXAMPLES" section, and other sections of Elijah's design
notes might not be interesting for now. Also this cover letter is
already pretty long.  But reviewers can refer to the links above if
they think it can help.

# Quick Overview (from Elijah's design notes)

`git replay`, at a basic level, can perhaps be thought of as a
"default-to-dry-run rebase" -- meaning no updates to the working tree,
or to the index, or to any references.  However, it differs from
rebase in that it:

  * Works for branches that aren't checked out

  * Works in a bare repository

  * Can replay multiple branches simultaneously (with or without common
    history in the range being replayed)

  * Preserves relative topology by default (merges are replayed too in
    Elijah's original work, not in this series)

  * Focuses on performance

  * Has several altered defaults as a result of the above

I sometimes think of `git replay` as "fast-replay", a patch-based
analogue to the snapshot-based fast-export & fast-import tools.

# Reasons for diverging from cherry-pick & rebase (from Elijah's
  design notes)

There are multiple reasons to diverge from the defaults in cherry-pick and
rebase.

* Server side needs

  * Both cherry-pick and rebase, via the sequencer, are heavily tied
    to updating the working tree, index, some refs, and a lot of
    control files with every commit replayed, and invoke a mess of
    hooks[1] that might be hard to avoid for backward compatibility
    reasons (at least, that's been brought up a few times on the
    list).

  * cherry-pick and rebase both fork various subprocesses
    unnecessarily, but somewhat intrinsically in part to ensure the
    same hooks are called that old scripted implementations would have
    called.

    Note: since 356ee4659bb (sequencer: try to commit without forking
    'git commit', 2017-11-24) cherry-pick and rebase do not fork
    subprocesses other than hooks for the cases covered by this patch
    series (i.e. they do not fork "git commit" for simple picks).

  * "Dry run" behavior, where there are no updates to worktree, index,
    or even refs might be important.

  * Should not assume users only want to operate on HEAD (see next
    section)

* Decapitate HEAD-centric assumptions

  * cherry-pick forces commits to be played on top of HEAD;
    inflexible.

  * rebase assumes the range of commits to be replayed is
    upstream..HEAD by default, though it allows one to replay
    upstream..otherbranch -- but it still forcibly and needlessly
    checks out 'otherbranch' before starting to replay things.

    Note: since 767a9c417eb (rebase -i: stop checking out the tip of
    the branch to rebase, 2020-01-24) it's not true that rebase
    forcibly and needlessly checks out 'otherbranch'.

  * Assuming HEAD is involved severely limits replaying multiple
    (possibly divergent) branches.

    Note: since 89fc0b53fdb (rebase: update refs from 'update-ref'
    commands, 2022-07-19) the sequencer can update multiple
    branches. The issue with divergent branch is with command line
    arguments and the todo list generation rather than the
    capabilities of the sequencer.

  * Once you stop assuming HEAD has a certain meaning, there's not
    much reason to have two separate commands anymore (except for the
    funny extra not-necessarily-compatible options both have gained
    over time).

  * (Micro issue: Assuming HEAD is involved also makes it harder for
    new users to learn what rebase means and does; it makes command
    lines hard to parse.  Not sure I want to harp on this too much, as
    I have a suspicion I might be creating a tool for experts with
    complicated use cases, but it's a minor quibble.)

* Performance

  * jj is slaughtering us on rebase speed[2].  I would like us to become
    competitive.  (I dropped a few comments in the link at [2] about why
    git is currently so bad.)

  * From [3], there was a simple 4-patch series in linux.git that took
    53 seconds to rebase.  Switching to ort dropped it to 16 seconds.
    While that sounds great, only 11 *milliseconds* were needed to do
    the actual merges.  That means almost *all* the time (>99%) was
    overhead!  Big offenders:

    * --reapply-cherry-picks should be the default

    * can_fast_forward() should be ripped out, and perhaps other extraneous
      revision walks

      Note: d42c9ffa0f (rebase: factor out branch_base calculation,
      2022-10-17) might already deal with that (according to Felipe
      Contreras).

    * avoid updating working tree, index, refs, reflogs, and control
      structures except when needed (e.g. hitting a conflict, or operation
      finished)

  * Other performance ideas (mostly for future work, not in this
    series)

    * single-file control structures instead of directory of files
      (when doing interactive things which is in Elijah's original
      work, but not in this series)

    * avoid forking subprocesses unless explicitly requested (e.g.
      --exec, --strategy, --run-hooks).  For example, definitely do not
      invoke `git commit` or `git merge`.

    * Sanitize hooks:

      * dispense with all per-commit hooks for sure (pre-commit,
        post-commit, post-checkout).

      * pre-rebase also seems to assume exactly 1 ref is written, and
        invoking it repeatedly would be stupid.  Plus, it's specific
        to "rebase".  So...ignore?  (Stolee's --ref-update option for
        rebase probably broke the pre-rebase assumptions already...)

      * post-rewrite hook might make sense, but fast-import got
        exempted, and I think of replay like a patch-based analogue
        to the snapshot-based fast-import.

    * When not running server side, resolve conflicts in a sparse-cone
      sparse-index worktree to reduce number of files written to a
      working tree.  (See below as well.)

    * [High risk of possible premature optimization] Avoid large
      numbers of newly created loose objects, when replaying large
      numbers of commits.  Two possibilities: (1) Consider using
      tmp-objdir and pack objects from the tmp-objdir at end of
      exercise, (2) Lift code from git-fast-import to immediately
      stuff new objects into a pack?

* Multiple branches and non-checked out branches

  * The ability to operate on non-checked out branches also implies
    that we should generally be able to replay when in a dirty working
    tree (exception being when we expect to update HEAD and any of the
    dirty files is one that needs to be updated by the replay).

  * Also, if we are operating locally on a non-checked out branch and
    hit a conflict, we should have a way to resolve the conflict
    without messing with the user's work on their current
    branch. (This is not is this patch series though.)

    * Idea: new worktree with sparse cone + sparse index checkout,
      containing only files in the root directory, and whatever is
      necessary to get the conflicts

    * Companion to above idea: control structures should be written to
      $GIT_COMMON_DIR/replay-${worktree}, so users can have multiple
      replay sessions, and so we know which worktrees are associated
      with which replay operations.

  - [1] https://lore.kernel.org/git/pull.749.v3.git.git.1586044818132.gitgitgadget@gmail.com/
  - [2] https://github.com/martinvonz/jj/discussions/49
  - [3] https://lore.kernel.org/git/CABPp-BE48=97k_3tnNqXPjSEfA163F8hoE+HY0Zvz1SWB2B8EA@mail.gmail.com/

# Important limitations

* The code exits with code 1 if there are any conflict. No
  resumability. No nice output. No interactivity. No special exit code
  depending on the reason.

* When a commit becomes empty as it is replayed, it is still replayed
  as an empty commit, instead of being dropped.

* No replaying merges, nor root commits. Only regular commits.

* Signed commits are not properly handled. It's not clear what to do
  to such commits when replaying on the server side.

* Notes associated with replayed commits are not updated nor
  duplicated. (Thanks to Phillip Wood for noticing.)

# Commit overview

* 1/15 t6429: remove switching aspects of fast-rebase

    New preparatory commit to make it easier to later replace the
    fast-rebase test-tool by `git replay` without breaking existing
    tests.

* 2/15 replay: introduce new builtin

     This creates a minimal `git replay` command by moving the code
     from the `fast-rebase` test helper from `t/helper/` into
     `builtin/` and doing some renames and a few other needed changes.
     (In v3 some `#include ...` were changed to deal with upstream
     changes in this area as suggested by Junio.)

* - 3/15 replay: start using parse_options API
  - 4/15 replay: die() instead of failing assert()
  - 5/15 replay: introduce pick_regular_commit()
  - 6/15 replay: don't simplify history
  - 7/15 replay: add an important FIXME comment about gpg signing
  - 8/15 replay: remove progress and info output
  - 9/15 replay: remove HEAD related sanity check

     These slowly change the command to make it behave more like
     regular commands and to start cleaning up its output.

* 10/15 replay: make it a minimal server side command

     After the cleaning up in previous commits, it's now time to
     radically change the way it works by stopping it to do ref
     updates, to update the index and worktree, to consider HEAD as
     special. Instead just make it output commands that should be
     passed to `git update-ref --stdin`.

* 11/15 replay: use standard revision ranges

     Start addind new interesting features and also documentation and
     tests, as the interface of the command is cristalizing into its
     final form.

* 12/15 replay: disallow revision specific options and pathspecs

     For now disallow revision specific options and pathspecs that are
     allowed and eaten by setup_revisions(), as it's not clear if all
     of these extra features are really needed, and anyway they would
     require tests and doc. So we leave them for future improvements.
     (In v3 a typo was fixed in the commit message and a code comment
     has been improved as suggested by Elijah.)

* - 13/15 replay: add --advance or 'cherry-pick' mode
  - 14/15 replay: add --contained to rebase contained branches

    Add new option and features to the command. (In v3, in commit
    14/15, the synopsys of the command was improved and a sentence
    about git rebase in its doc was removed as suggested by Elijah.)

* 15/15 replay: stop assuming replayed branches do not diverge

      This adds another interesting feature, as well as related
      documentation and tests. (In v3 a typo in the commit message was
      fixed as suggested by Elijah.)

# Notes about `fast-rebase`, tests and documentation

The `fast-rebase` test-tool helper was developed by Elijah to
experiment with a rebasing tool that would be developed from scratch
based on his merge-ort work, could be used to test that merge-ort
work, and would not have the speed and interface limitations of `git
rebase` or `git cherry-pick`.

This `fast-rebase` helper was used before this series in:

t6429-merge-sequence-rename-caching.sh

So when `git replay` is created from `fast-rebase` in patch 2/15, the
t6429 test script is also converted to use `git replay`. This ensures
that `git replay` doesn't break too badly during the first 10 patches
in this patch series.

Tests and documentation are introduced specifically for `git replay`
only in 11/15 and later patches as it doesn't make much sense to
document and test behavior that we know is going to change soon. So
it's only when the command is crystalizing towards its final form that
we start documenting and testing it.

# Possibly controversial issues 

* bare or not bare: this series works towards a plumbing command with
  the end goal of it being usable and used first on bare repos,
  contrary to existing commands like `git rebase` and `git
  cherry-pick`. The tests check that the command works on both bare
  and non-bare repo though.

* exit status: a successful, non-conflicted replay exits with code
  0. When the replay has conflicts, the exit status is 1. If the
  replay is not able to complete (or start) due to some kind of error,
  the exit status is something other than 0 or 1. There are a few
  tests checking that. It has been suggested in an internal review
  that conflicts might want to get a more specific error code as an
  error code of 1 might be quite easy to return by accident. It
  doesn't seem to me from their docs (which might want to be improved,
  I didn't look at the code) that other commands like `git merge` and
  `git rebase` exit with a special error code in case of conflict.

* make worktree and index changes optional: commit 10/15 stops
  updating the index and worktree, but it might be better especially
  for cli users to make that optional. The issue is that this would
  make the command more complex while we are developing a number of
  important features so that the command can be used on bare repos. It
  seems that this should rather be done in an iterative improvement
  after the important features have landed.

* when and where to add tests and docs: although t6429 has tests that
  are changed to use the new command instead of the fast-rebase
  test-tool command as soon as the former is introduced, there is no
  specific test script and no doc for the new command until commit
  11/15 when standard revision ranges are used. This is done to avoid
  churn in tests and docs while the final form of the command hasn't
  crystalized enough. Adding tests and doc at this point makes this
  commit quite big and possibly more difficult to review than if they
  were in separate commits though. On the other hand when tests and
  docs are added in specific commits some reviewers say it would be
  better to introduce them when the related changes are made.

* --advance and --contained: these two advanced options might not
  belong to this first series and could perhaps be added in a followup
  series in separate commits. On the other hand the code for
  --contained seems involved with the code of --advance and it's nice
  to see soon that git replay can indeed do cherry-picking and rebase
  many refs at once, and this way fullfil these parts of its promise.

* replaying diverging branches: 15/15 the last patch in the series,
  which allow replaying diverging branches, can be seen as a
  fundamental fix or alternatively as adding an interesting
  feature. So it's debatable if it should be in its own patch along
  with its own tests as in this series, or if it should be merged into
  a previous patch and which one.

* only 2 patches: this patch series can be seen from a high level
  point of view as 1) introducing the new `git replay` command, and 2)
  using `git replay` to replace, and get rid of, the fast-rebase
  test-tool command. The fact that not much of the original
  fast-rebase code and interface is left would agree with that point
  of view. On the other hand, fast-rebase can also be seen as a first
  iteration towards `git replay`. So it can also make sense to see how
  `git replay` evolved from it.

# Changes between v3 and v4

Thanks to Toon, Junio and Dscho for their suggestions on the previous
version! The very few and minor changes compared to v3 are:

* The patch series was rebased onto master at d814540bb7 (The fifth
  batch, 2023-09-01). This is to fix a few header related conflicts as
  can be seen in the range-diff.

* In patch 10/15 (replay: make it a minimal server side command) a /*
  Cleanup */ code comment has been removed as suggested by Toon.

* In patch 11/15 (replay: use standard revision ranges) the git-replay
  documentation related to --onto has been improved to better explain
  which branches will be updated by the update-ref command(s) in the
  output as suggested by Junio.

* In patch 12/15 (replay: disallow revision specific options and
  pathspecs) an error message has been improved as suggested by Junio.

* In patch 13/15 (replay: add --advance or 'cherry-pick' mode) the
  commit message and the git-replay documentation have been improved
  to better explain that --advance only works when the revision range
  passed has a single tip as suggested by Junio.

* Also in patch 13/15 (replay: add --advance or 'cherry-pick' mode) an
  error message has been improved, and a few tests have been added to
  check that `git replay` fails when it's passed both --advance and
  --onto and when it's passed none of these options, as suggested by
  Toon.

# Range-diff between v3 and v4

 1:  51fa1c7aea =  1:  1eaca9b788 t6429: remove switching aspects of fast-rebase
 2:  19f8cf1b2e !  2:  5ac4beb1ae replay: introduce new builtin
    @@ t/helper/test-fast-rebase.c => builtin/replay.c
      
      #define USE_THE_INDEX_VARIABLE
     -#include "test-tool.h"
    --#include "cache.h"
     +#include "git-compat-util.h"
     +
     +#include "builtin.h"
 3:  295e876db6 !  3:  299381aa9b replay: start using parse_options API
    @@ builtin/replay.c
      #include "lockfile.h"
      #include "merge-ort.h"
      #include "object-name.h"
    +-#include "read-cache-ll.h"
     +#include "parse-options.h"
      #include "refs.h"
      #include "revision.h"
 4:  6ece7d3751 =  4:  3b825c9be0 replay: die() instead of failing assert()
 5:  9b4bc659fb =  5:  b1e890745d replay: introduce pick_regular_commit()
 6:  9ab68d50ab =  6:  ec51351889 replay: don't simplify history
 7:  37e93faafa =  7:  cd4ed07d2d replay: add an important FIXME comment about gpg signing
 8:  03036781ed =  8:  e45a55917c replay: remove progress and info output
 9:  4ea289952e =  9:  0587a76cbb replay: remove HEAD related sanity check
10:  fba98eda07 ! 10:  d10368e87a replay: make it a minimal server side command
    @@ builtin/replay.c
     -#include "commit.h"
      #include "environment.h"
     -#include "gettext.h"
    +-#include "hash.h"
      #include "hex.h"
      #include "lockfile.h"
      #include "merge-ort.h"
    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
     +          }
        }
      
    -+  /* Cleanup */
        merge_finalize(&merge_opt, &result);
     +  ret = result.clean;
      
11:  03f9f20f6e ! 11:  4e09572c43 replay: use standard revision ranges
    @@ Documentation/git-replay.txt (new)
     +  Starting point at which to create the new commits.  May be any
     +  valid commit, and not just an existing branch name.
     ++
    -+The update-ref commands in the output will update the branch(es)
    -+in the revision range to point at the new commits (in other
    -+words, this mimics a rebase operation).
    ++The update-ref command(s) in the output will update the branch(es) in
    ++the revision range to point at the new commits, similar to the way how
    ++`git rebase --update-refs` updates multiple branches in the affected
    ++range.
     +
     +<revision-range>::
     +  Range of commits to replay; see "Specifying Ranges" in
    @@ Documentation/git-replay.txt (new)
     +  update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
     +  update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
     +
    -+where the number of refs updated depend on the arguments passed.
    ++where the number of refs updated depends on the arguments passed and
    ++the shape of the history being replayed.
     +
     +EXIT STATUS
     +-----------
12:  e651250ac7 ! 12:  64b803f1cf replay: disallow revision specific options and pathspecs
    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
     +   * them, after adding related tests and doc though.
     +   */
     +  if (revs.prune_data.nr) {
    -+          error(_("invalid pathspec: %s"), revs.prune_data.items[0].match);
    ++          error(_("no pathspec is allowed: '%s'"), revs.prune_data.items[0].match);
     +          usage_with_options(replay_usage, replay_options);
     +  }
     +
13:  56e5416dad ! 13:  04f27d81ab replay: add --advance or 'cherry-pick' mode
    @@ Commit message
         'cherry-pick' mode with `--advance`. This new mode will make the target
         branch advance as we replay commits onto it.
     
    +    The replayed commits should have a single tip, so that it's clear where
    +    the target branch should be advanced. If they have more than one tip,
    +    this new mode will error out.
    +
         Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
         Signed-off-by: Elijah Newren <newren@gmail.com>
         Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
    @@ Documentation/git-replay.txt: OPTIONS
        Starting point at which to create the new commits.  May be any
        valid commit, and not just an existing branch name.
      +
    --The update-ref commands in the output will update the branch(es)
    --in the revision range to point at the new commits (in other
    --words, this mimics a rebase operation).
    +-The update-ref command(s) in the output will update the branch(es) in
    +-the revision range to point at the new commits, similar to the way how
    +-`git rebase --update-refs` updates multiple branches in the affected
    +-range.
     +When `--onto` is specified, the update-ref command(s) in the output will
     +update the branch(es) in the revision range to point at the new
    -+commits (in other words, this mimics a rebase operation).
    ++commits, similar to the way how `git rebase --update-refs` updates
    ++multiple branches in the affected range.
     +
     +--advance <branch>::
     +  Starting point at which to create the new commits; must be a
    @@ Documentation/git-replay.txt: OPTIONS
     +the new commits (in other words, this mimics a cherry-pick operation).
      
      <revision-range>::
    -   Range of commits to replay; see "Specifying Ranges" in
    +-  Range of commits to replay; see "Specifying Ranges" in
    +-  linkgit:git-rev-parse.
    ++  Range of commits to replay. More than one <revision-range> can
    ++  be passed, but in `--advance <branch>` mode, they should have
    ++  a single tip, so that it's clear where <branch> should point
    ++  to. See "Specifying Ranges" in linkgit:git-rev-parse.
    + 
    + OUTPUT
    + ------
     @@ Documentation/git-replay.txt: input to `git update-ref --stdin`.  It is basically of the form:
    -   update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
        update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
      
    --where the number of refs updated depend on the arguments passed.
    -+where the number of refs updated depend on the arguments passed.  When
    -+using `--advance`, the number of refs updated is always one, but for
    -+`--onto`, it can be one or more (rebasing multiple branches
    -+simultaneously is supported).
    + where the number of refs updated depends on the arguments passed and
    +-the shape of the history being replayed.
    ++the shape of the history being replayed.  When using `--advance`, the
    ++number of refs updated is always one, but for `--onto`, it can be one
    ++or more (rebasing multiple branches simultaneously is supported).
      
      EXIT STATUS
      -----------
    @@ builtin/replay.c: static struct commit *create_commit(struct tree *tree,
     +                  die(_("argument to --advance must be a reference"));
     +          }
     +          if (rinfo.positive_refexprs > 1)
    -+                  die(_("cannot advance target with multiple source branches because ordering would be ill-defined"));
    ++                  die(_("cannot advance target with multiple sources because ordering would be ill-defined"));
     +  } else {
     +          int positive_refs_complete = (
     +                  rinfo.positive_refexprs ==
    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
     +                 oid_to_hex(&onto->object.oid));
     +  }
     +
    -   /* Cleanup */
        merge_finalize(&merge_opt, &result);
        ret = result.clean;
      
    @@ t/t3650-replay-basics.sh: test_expect_success 'using replay on bare repo with di
     +  git -C bare replay --advance main topic1..topic2 >result-bare &&
     +  test_cmp expect result-bare
     +'
    ++
    ++test_expect_success 'replay on bare repo fails with both --advance and --onto' '
    ++  test_must_fail git -C bare replay --advance main --onto main topic1..topic2 >result-bare
    ++'
    ++
    ++test_expect_success 'replay fails when both --advance and --onto are omitted' '
    ++  test_must_fail git replay topic1..topic2 >result
    ++'
     +
      test_done
14:  2cc17dfdc7 ! 14:  9ed0919ad5 replay: add --contained to rebase contained branches
    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
                                       oid_to_hex(&last_commit->object.oid),
     
      ## t/t3650-replay-basics.sh ##
    -@@ t/t3650-replay-basics.sh: test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
    -   test_cmp expect result-bare
    +@@ t/t3650-replay-basics.sh: test_expect_success 'replay fails when both --advance and --onto are omitted' '
    +   test_must_fail git replay topic1..topic2 >result
      '
      
     +test_expect_success 'using replay to also rebase a contained branch' '
15:  a6d88fc8f0 ! 15:  cf8c984877 replay: stop assuming replayed branches do not diverge
    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
                if (advance_name)
                        continue;
     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix)
    +   }
      
    -   /* Cleanup */
        merge_finalize(&merge_opt, &result);
     -  ret = result.clean;
     -


Christian Couder (1):
  replay: disallow revision specific options and pathspecs

Elijah Newren (14):
  t6429: remove switching aspects of fast-rebase
  replay: introduce new builtin
  replay: start using parse_options API
  replay: die() instead of failing assert()
  replay: introduce pick_regular_commit()
  replay: don't simplify history
  replay: add an important FIXME comment about gpg signing
  replay: remove progress and info output
  replay: remove HEAD related sanity check
  replay: make it a minimal server side command
  replay: use standard revision ranges
  replay: add --advance or 'cherry-pick' mode
  replay: add --contained to rebase contained branches
  replay: stop assuming replayed branches do not diverge

 .gitignore                               |   1 +
 Documentation/git-replay.txt             | 124 +++++++
 Makefile                                 |   2 +-
 builtin.h                                |   1 +
 builtin/replay.c                         | 431 +++++++++++++++++++++++
 command-list.txt                         |   1 +
 git.c                                    |   1 +
 t/helper/test-fast-rebase.c              | 241 -------------
 t/helper/test-tool.c                     |   1 -
 t/helper/test-tool.h                     |   1 -
 t/t3650-replay-basics.sh                 | 214 +++++++++++
 t/t6429-merge-sequence-rename-caching.sh |  45 +--
 12 files changed, 799 insertions(+), 264 deletions(-)
 create mode 100644 Documentation/git-replay.txt
 create mode 100644 builtin/replay.c
 delete mode 100644 t/helper/test-fast-rebase.c
 create mode 100755 t/t3650-replay-basics.sh

-- 
2.42.0.126.gcf8c984877


^ permalink raw reply	[flat|nested] 208+ messages in thread

* [PATCH v4 01/15] t6429: remove switching aspects of fast-rebase
  2023-09-07  9:25     ` [PATCH v4 " Christian Couder
@ 2023-09-07  9:25       ` Christian Couder
  2023-09-07  9:25       ` [PATCH v4 02/15] replay: introduce new builtin Christian Couder
                         ` (15 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-09-07  9:25 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Christian Couder

From: Elijah Newren <newren@gmail.com>

At the time t6429 was written, merge-ort was still under development,
did not have quite as many tests, and certainly was not widely deployed.
Since t6429 was exercising some codepaths just a little differently, we
thought having them also test the "merge_switch_to_result()" bits of
merge-ort was useful even though they weren't intrinsic to the real
point of these tests.

However, the value provided by doing extra testing of the
"merge_switch_to_result()" bits has decreased a bit over time, and it's
actively making it harder to refactor `test-tool fast-rebase` into `git
replay`, which we are going to do in following commits.  Dispense with
these bits.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 t/helper/test-fast-rebase.c              | 9 +--------
 t/t6429-merge-sequence-rename-caching.sh | 9 +++++++--
 2 files changed, 8 insertions(+), 10 deletions(-)

diff --git a/t/helper/test-fast-rebase.c b/t/helper/test-fast-rebase.c
index cac20a72b3..2bfab66b1b 100644
--- a/t/helper/test-fast-rebase.c
+++ b/t/helper/test-fast-rebase.c
@@ -194,7 +194,7 @@ int cmd__fast_rebase(int argc, const char **argv)
 		last_commit = create_commit(result.tree, commit, last_commit);
 	}
 
-	merge_switch_to_result(&merge_opt, head_tree, &result, 1, !result.clean);
+	merge_finalize(&merge_opt, &result);
 
 	if (result.clean < 0)
 		exit(128);
@@ -213,9 +213,6 @@ int cmd__fast_rebase(int argc, const char **argv)
 		}
 		if (create_symref("HEAD", branch_name.buf, reflog_msg.buf) < 0)
 			die(_("unable to update HEAD"));
-
-		prime_cache_tree(the_repository, the_repository->index,
-				 result.tree);
 	} else {
 		fprintf(stderr, "\nAborting: Hit a conflict.\n");
 		strbuf_addf(&reflog_msg, "rebase progress up to %s",
@@ -228,10 +225,6 @@ int cmd__fast_rebase(int argc, const char **argv)
 			die("Failed to update %s", argv[4]);
 		}
 	}
-	if (write_locked_index(&the_index, &lock,
-			       COMMIT_LOCK | SKIP_IF_UNCHANGED))
-		die(_("unable to write %s"), get_index_file());
-
 	ret = (result.clean == 0);
 cleanup:
 	strbuf_release(&reflog_msg);
diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh
index d02fa16614..75d3fd2dba 100755
--- a/t/t6429-merge-sequence-rename-caching.sh
+++ b/t/t6429-merge-sequence-rename-caching.sh
@@ -72,6 +72,7 @@ test_expect_success 'caching renames does not preclude finding new ones' '
 		git switch upstream &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream~1..topic
 
 		git ls-files >tracked-files &&
@@ -200,6 +201,7 @@ test_expect_success 'rename same file identically, then reintroduce it' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream~1..topic &&
 
 		git ls-files >tracked &&
@@ -277,6 +279,7 @@ test_expect_success 'rename same file identically, then add file to old dir' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream~1..topic &&
 
 		git ls-files >tracked &&
@@ -356,8 +359,6 @@ test_expect_success 'cached dir rename does not prevent noticing later conflict'
 		test_must_fail test-tool fast-rebase --onto HEAD upstream~1 topic >output &&
 		#git cherry-pick upstream..topic &&
 
-		grep CONFLICT..rename/rename output &&
-
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls
 	)
@@ -456,6 +457,7 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
@@ -522,6 +524,7 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
@@ -624,6 +627,7 @@ test_expect_success 'caching renames only on upstream side, part 1' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
@@ -682,6 +686,7 @@ test_expect_success 'caching renames only on upstream side, part 2' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
-- 
2.42.0.126.gcf8c984877


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v4 02/15] replay: introduce new builtin
  2023-09-07  9:25     ` [PATCH v4 " Christian Couder
  2023-09-07  9:25       ` [PATCH v4 01/15] t6429: remove switching aspects of fast-rebase Christian Couder
@ 2023-09-07  9:25       ` Christian Couder
  2023-09-07 10:23         ` Johannes Schindelin
  2023-09-07  9:25       ` [PATCH v4 03/15] replay: start using parse_options API Christian Couder
                         ` (14 subsequent siblings)
  16 siblings, 1 reply; 208+ messages in thread
From: Christian Couder @ 2023-09-07  9:25 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Christian Couder

From: Elijah Newren <newren@gmail.com>

For now, this is just a rename from `t/helper/test-fast-rebase.c` into
`builtin/replay.c` with minimal changes to make it build appropriately.

Subsequent commits will flesh out its capabilities and make it a more
standard regular builtin.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 .gitignore                                    |  1 +
 Makefile                                      |  2 +-
 builtin.h                                     |  1 +
 .../test-fast-rebase.c => builtin/replay.c    | 27 ++++++-------------
 command-list.txt                              |  1 +
 git.c                                         |  1 +
 t/helper/test-tool.c                          |  1 -
 t/helper/test-tool.h                          |  1 -
 t/t6429-merge-sequence-rename-caching.sh      | 27 +++++++------------
 9 files changed, 22 insertions(+), 40 deletions(-)
 rename t/helper/test-fast-rebase.c => builtin/replay.c (89%)

diff --git a/.gitignore b/.gitignore
index 5e56e471b3..612c0f6a0f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -135,6 +135,7 @@
 /git-remote-ext
 /git-repack
 /git-replace
+/git-replay
 /git-request-pull
 /git-rerere
 /git-reset
diff --git a/Makefile b/Makefile
index 5776309365..05a504dc28 100644
--- a/Makefile
+++ b/Makefile
@@ -799,7 +799,6 @@ TEST_BUILTINS_OBJS += test-dump-split-index.o
 TEST_BUILTINS_OBJS += test-dump-untracked-cache.o
 TEST_BUILTINS_OBJS += test-env-helper.o
 TEST_BUILTINS_OBJS += test-example-decorate.o
-TEST_BUILTINS_OBJS += test-fast-rebase.o
 TEST_BUILTINS_OBJS += test-fsmonitor-client.o
 TEST_BUILTINS_OBJS += test-genrandom.o
 TEST_BUILTINS_OBJS += test-genzeros.o
@@ -1287,6 +1286,7 @@ BUILTIN_OBJS += builtin/remote-fd.o
 BUILTIN_OBJS += builtin/remote.o
 BUILTIN_OBJS += builtin/repack.o
 BUILTIN_OBJS += builtin/replace.o
+BUILTIN_OBJS += builtin/replay.o
 BUILTIN_OBJS += builtin/rerere.o
 BUILTIN_OBJS += builtin/reset.o
 BUILTIN_OBJS += builtin/rev-list.o
diff --git a/builtin.h b/builtin.h
index d560baa661..28280636da 100644
--- a/builtin.h
+++ b/builtin.h
@@ -211,6 +211,7 @@ int cmd_remote(int argc, const char **argv, const char *prefix);
 int cmd_remote_ext(int argc, const char **argv, const char *prefix);
 int cmd_remote_fd(int argc, const char **argv, const char *prefix);
 int cmd_repack(int argc, const char **argv, const char *prefix);
+int cmd_replay(int argc, const char **argv, const char *prefix);
 int cmd_rerere(int argc, const char **argv, const char *prefix);
 int cmd_reset(int argc, const char **argv, const char *prefix);
 int cmd_restore(int argc, const char **argv, const char *prefix);
diff --git a/t/helper/test-fast-rebase.c b/builtin/replay.c
similarity index 89%
rename from t/helper/test-fast-rebase.c
rename to builtin/replay.c
index 2bfab66b1b..e102749ab6 100644
--- a/t/helper/test-fast-rebase.c
+++ b/builtin/replay.c
@@ -1,17 +1,11 @@
 /*
- * "git fast-rebase" builtin command
- *
- * FAST: Forking Any Subprocesses (is) Taboo
- *
- * This is meant SOLELY as a demo of what is possible.  sequencer.c and
- * rebase.c should be refactored to use the ideas here, rather than attempting
- * to extend this file to replace those (unless Phillip or Dscho say that
- * refactoring is too hard and we need a clean slate, but I'm guessing that
- * refactoring is the better route).
+ * "git replay" builtin command
  */
 
 #define USE_THE_INDEX_VARIABLE
-#include "test-tool.h"
+#include "git-compat-util.h"
+
+#include "builtin.h"
 #include "cache-tree.h"
 #include "commit.h"
 #include "environment.h"
@@ -27,7 +21,8 @@
 #include "sequencer.h"
 #include "setup.h"
 #include "strvec.h"
-#include "tree.h"
+#include <oidset.h>
+#include <tree.h>
 
 static const char *short_commit_name(struct commit *commit)
 {
@@ -94,7 +89,7 @@ static struct commit *create_commit(struct tree *tree,
 	return (struct commit *)obj;
 }
 
-int cmd__fast_rebase(int argc, const char **argv)
+int cmd_replay(int argc, const char **argv, const char *prefix)
 {
 	struct commit *onto;
 	struct commit *last_commit = NULL, *last_picked_commit = NULL;
@@ -110,12 +105,6 @@ int cmd__fast_rebase(int argc, const char **argv)
 	struct strbuf branch_name = STRBUF_INIT;
 	int ret = 0;
 
-	/*
-	 * test-tool stuff doesn't set up the git directory by default; need to
-	 * do that manually.
-	 */
-	setup_git_directory();
-
 	if (argc == 2 && !strcmp(argv[1], "-h")) {
 		printf("Sorry, I am not a psychiatrist; I can not give you the help you need.  Oh, you meant usage...\n");
 		exit(129);
@@ -136,7 +125,7 @@ int cmd__fast_rebase(int argc, const char **argv)
 	if (repo_read_index(the_repository) < 0)
 		BUG("Could not read index");
 
-	repo_init_revisions(the_repository, &revs, NULL);
+	repo_init_revisions(the_repository, &revs, prefix);
 	revs.verbose_header = 1;
 	revs.max_parents = 1;
 	revs.cherry_mark = 1;
diff --git a/command-list.txt b/command-list.txt
index 54b2a50f5f..d74836ab21 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -160,6 +160,7 @@ git-reflog                              ancillarymanipulators           complete
 git-remote                              ancillarymanipulators           complete
 git-repack                              ancillarymanipulators           complete
 git-replace                             ancillarymanipulators           complete
+git-replay                              mainporcelain           history
 git-request-pull                        foreignscminterface             complete
 git-rerere                              ancillaryinterrogators
 git-reset                               mainporcelain           history
diff --git a/git.c b/git.c
index c67e44dd82..7068a184b0 100644
--- a/git.c
+++ b/git.c
@@ -594,6 +594,7 @@ static struct cmd_struct commands[] = {
 	{ "remote-fd", cmd_remote_fd, NO_PARSEOPT },
 	{ "repack", cmd_repack, RUN_SETUP },
 	{ "replace", cmd_replace, RUN_SETUP },
+	{ "replay", cmd_replay, RUN_SETUP },
 	{ "rerere", cmd_rerere, RUN_SETUP },
 	{ "reset", cmd_reset, RUN_SETUP },
 	{ "restore", cmd_restore, RUN_SETUP | NEED_WORK_TREE },
diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
index abe8a785eb..9ca1586de7 100644
--- a/t/helper/test-tool.c
+++ b/t/helper/test-tool.c
@@ -30,7 +30,6 @@ static struct test_cmd cmds[] = {
 	{ "dump-untracked-cache", cmd__dump_untracked_cache },
 	{ "env-helper", cmd__env_helper },
 	{ "example-decorate", cmd__example_decorate },
-	{ "fast-rebase", cmd__fast_rebase },
 	{ "fsmonitor-client", cmd__fsmonitor_client },
 	{ "genrandom", cmd__genrandom },
 	{ "genzeros", cmd__genzeros },
diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h
index ea2672436c..a03bbfc6b2 100644
--- a/t/helper/test-tool.h
+++ b/t/helper/test-tool.h
@@ -24,7 +24,6 @@ int cmd__dump_untracked_cache(int argc, const char **argv);
 int cmd__dump_reftable(int argc, const char **argv);
 int cmd__env_helper(int argc, const char **argv);
 int cmd__example_decorate(int argc, const char **argv);
-int cmd__fast_rebase(int argc, const char **argv);
 int cmd__fsmonitor_client(int argc, const char **argv);
 int cmd__genrandom(int argc, const char **argv);
 int cmd__genzeros(int argc, const char **argv);
diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh
index 75d3fd2dba..7670b72008 100755
--- a/t/t6429-merge-sequence-rename-caching.sh
+++ b/t/t6429-merge-sequence-rename-caching.sh
@@ -71,9 +71,8 @@ test_expect_success 'caching renames does not preclude finding new ones' '
 
 		git switch upstream &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream~1..topic
 
 		git ls-files >tracked-files &&
 		test_line_count = 2 tracked-files &&
@@ -141,8 +140,7 @@ test_expect_success 'cherry-pick both a commit and its immediate revert' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
-		#git cherry-pick upstream~1..topic &&
+		git replay --onto HEAD upstream~1 topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 1 calls
@@ -200,9 +198,8 @@ test_expect_success 'rename same file identically, then reintroduce it' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream~1..topic &&
 
 		git ls-files >tracked &&
 		test_line_count = 2 tracked &&
@@ -278,9 +275,8 @@ test_expect_success 'rename same file identically, then add file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream~1..topic &&
 
 		git ls-files >tracked &&
 		test_line_count = 4 tracked &&
@@ -356,8 +352,7 @@ test_expect_success 'cached dir rename does not prevent noticing later conflict'
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test_must_fail test-tool fast-rebase --onto HEAD upstream~1 topic >output &&
-		#git cherry-pick upstream..topic &&
+		test_must_fail git replay --onto HEAD upstream~1 topic >output &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls
@@ -456,9 +451,8 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls &&
@@ -523,9 +517,8 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 3 calls &&
@@ -626,9 +619,8 @@ test_expect_success 'caching renames only on upstream side, part 1' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 1 calls &&
@@ -685,9 +677,8 @@ test_expect_success 'caching renames only on upstream side, part 2' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls &&
-- 
2.42.0.126.gcf8c984877


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v4 03/15] replay: start using parse_options API
  2023-09-07  9:25     ` [PATCH v4 " Christian Couder
  2023-09-07  9:25       ` [PATCH v4 01/15] t6429: remove switching aspects of fast-rebase Christian Couder
  2023-09-07  9:25       ` [PATCH v4 02/15] replay: introduce new builtin Christian Couder
@ 2023-09-07  9:25       ` Christian Couder
  2023-09-07  9:25       ` [PATCH v4 04/15] replay: die() instead of failing assert() Christian Couder
                         ` (13 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-09-07  9:25 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Christian Couder

From: Elijah Newren <newren@gmail.com>

Instead of manually parsing arguments, let's start using the parse_options
API. This way this new builtin will look more standard, and in some
upcoming commits will more easily be able to handle more command line
options.

Note that we plan to later use standard revision ranges instead of
hardcoded "<oldbase> <branch>" arguments. When we will use standard
revision ranges, it will be easier to check if there are no spurious
arguments if we keep ARGV[0], so let's call parse_options() with
PARSE_OPT_KEEP_ARGV0 even if we don't need ARGV[0] right now to avoid
some useless code churn.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 45 ++++++++++++++++++++++++++++++++-------------
 1 file changed, 32 insertions(+), 13 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index e102749ab6..d6dec7c866 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -15,7 +15,7 @@
 #include "lockfile.h"
 #include "merge-ort.h"
 #include "object-name.h"
-#include "read-cache-ll.h"
+#include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
 #include "sequencer.h"
@@ -92,6 +92,7 @@ static struct commit *create_commit(struct tree *tree,
 int cmd_replay(int argc, const char **argv, const char *prefix)
 {
 	struct commit *onto;
+	const char *onto_name = NULL;
 	struct commit *last_commit = NULL, *last_picked_commit = NULL;
 	struct object_id head;
 	struct lock_file lock = LOCK_INIT;
@@ -105,16 +106,32 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct strbuf branch_name = STRBUF_INIT;
 	int ret = 0;
 
-	if (argc == 2 && !strcmp(argv[1], "-h")) {
-		printf("Sorry, I am not a psychiatrist; I can not give you the help you need.  Oh, you meant usage...\n");
-		exit(129);
+	const char * const replay_usage[] = {
+		N_("git replay --onto <newbase> <oldbase> <branch>"),
+		NULL
+	};
+	struct option replay_options[] = {
+		OPT_STRING(0, "onto", &onto_name,
+			   N_("revision"),
+			   N_("replay onto given commit")),
+		OPT_END()
+	};
+
+	argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
+			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
+
+	if (!onto_name) {
+		error(_("option --onto is mandatory"));
+		usage_with_options(replay_usage, replay_options);
 	}
 
-	if (argc != 5 || strcmp(argv[1], "--onto"))
-		die("usage: read the code, figure out how to use it, then do so");
+	if (argc != 3) {
+		error(_("bad number of arguments"));
+		usage_with_options(replay_usage, replay_options);
+	}
 
-	onto = peel_committish(argv[2]);
-	strbuf_addf(&branch_name, "refs/heads/%s", argv[4]);
+	onto = peel_committish(onto_name);
+	strbuf_addf(&branch_name, "refs/heads/%s", argv[2]);
 
 	/* Sanity check */
 	if (repo_get_oid(the_repository, "HEAD", &head))
@@ -126,6 +143,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		BUG("Could not read index");
 
 	repo_init_revisions(the_repository, &revs, prefix);
+
 	revs.verbose_header = 1;
 	revs.max_parents = 1;
 	revs.cherry_mark = 1;
@@ -134,7 +152,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	revs.right_only = 1;
 	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
 	revs.topo_order = 1;
-	strvec_pushl(&rev_walk_args, "", argv[4], "--not", argv[3], NULL);
+
+	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
 
 	if (setup_revisions(rev_walk_args.nr, rev_walk_args.v, &revs, NULL) > 1) {
 		ret = error(_("unhandled options"));
@@ -197,8 +216,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			       &last_commit->object.oid,
 			       &last_picked_commit->object.oid,
 			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
-			error(_("could not update %s"), argv[4]);
-			die("Failed to update %s", argv[4]);
+			error(_("could not update %s"), argv[2]);
+			die("Failed to update %s", argv[2]);
 		}
 		if (create_symref("HEAD", branch_name.buf, reflog_msg.buf) < 0)
 			die(_("unable to update HEAD"));
@@ -210,8 +229,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			       &last_commit->object.oid,
 			       &head,
 			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
-			error(_("could not update %s"), argv[4]);
-			die("Failed to update %s", argv[4]);
+			error(_("could not update %s"), argv[2]);
+			die("Failed to update %s", argv[2]);
 		}
 	}
 	ret = (result.clean == 0);
-- 
2.42.0.126.gcf8c984877


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v4 04/15] replay: die() instead of failing assert()
  2023-09-07  9:25     ` [PATCH v4 " Christian Couder
                         ` (2 preceding siblings ...)
  2023-09-07  9:25       ` [PATCH v4 03/15] replay: start using parse_options API Christian Couder
@ 2023-09-07  9:25       ` Christian Couder
  2023-09-07  9:25       ` [PATCH v4 05/15] replay: introduce pick_regular_commit() Christian Couder
                         ` (12 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-09-07  9:25 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Christian Couder

From: Elijah Newren <newren@gmail.com>

It's not a good idea for regular Git commands to use an assert() to
check for things that could happen but are not supported.

Let's die() with an explanation of the issue instead.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index d6dec7c866..f3fdbe48c9 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -179,7 +179,12 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 
 		fprintf(stderr, "Rebasing %s...\r",
 			oid_to_hex(&commit->object.oid));
-		assert(commit->parents && !commit->parents->next);
+
+		if (!commit->parents)
+			die(_("replaying down to root commit is not supported yet!"));
+		if (commit->parents->next)
+			die(_("replaying merge commits is not supported yet!"));
+
 		base = commit->parents->item;
 
 		next_tree = repo_get_commit_tree(the_repository, commit);
-- 
2.42.0.126.gcf8c984877


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v4 05/15] replay: introduce pick_regular_commit()
  2023-09-07  9:25     ` [PATCH v4 " Christian Couder
                         ` (3 preceding siblings ...)
  2023-09-07  9:25       ` [PATCH v4 04/15] replay: die() instead of failing assert() Christian Couder
@ 2023-09-07  9:25       ` Christian Couder
  2023-09-07  9:25       ` [PATCH v4 06/15] replay: don't simplify history Christian Couder
                         ` (11 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-09-07  9:25 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Christian Couder

From: Elijah Newren <newren@gmail.com>

Let's refactor the code to handle a regular commit (a commit that is
neither a root commit nor a merge commit) into a single function instead
of keeping it inside cmd_replay().

This is good for separation of concerns, and this will help further work
in the future to replay merge commits.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 54 ++++++++++++++++++++++++++++++------------------
 1 file changed, 34 insertions(+), 20 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index f3fdbe48c9..c66888679b 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -89,6 +89,35 @@ static struct commit *create_commit(struct tree *tree,
 	return (struct commit *)obj;
 }
 
+static struct commit *pick_regular_commit(struct commit *pickme,
+					  struct commit *last_commit,
+					  struct merge_options *merge_opt,
+					  struct merge_result *result)
+{
+	struct commit *base;
+	struct tree *pickme_tree, *base_tree;
+
+	base = pickme->parents->item;
+
+	pickme_tree = repo_get_commit_tree(the_repository, pickme);
+	base_tree = repo_get_commit_tree(the_repository, base);
+
+	merge_opt->branch2 = short_commit_name(pickme);
+	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
+
+	merge_incore_nonrecursive(merge_opt,
+				  base_tree,
+				  result->tree,
+				  pickme_tree,
+				  result);
+
+	free((char*)merge_opt->ancestor);
+	merge_opt->ancestor = NULL;
+	if (!result->clean)
+		return NULL;
+	return create_commit(result->tree, pickme, last_commit);
+}
+
 int cmd_replay(int argc, const char **argv, const char *prefix)
 {
 	struct commit *onto;
@@ -100,7 +129,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct rev_info revs;
 	struct commit *commit;
 	struct merge_options merge_opt;
-	struct tree *next_tree, *base_tree, *head_tree;
+	struct tree *head_tree;
 	struct merge_result result;
 	struct strbuf reflog_msg = STRBUF_INIT;
 	struct strbuf branch_name = STRBUF_INIT;
@@ -175,7 +204,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	result.tree = head_tree;
 	last_commit = onto;
 	while ((commit = get_revision(&revs))) {
-		struct commit *base;
+		struct commit *pick;
 
 		fprintf(stderr, "Rebasing %s...\r",
 			oid_to_hex(&commit->object.oid));
@@ -185,26 +214,11 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		if (commit->parents->next)
 			die(_("replaying merge commits is not supported yet!"));
 
-		base = commit->parents->item;
-
-		next_tree = repo_get_commit_tree(the_repository, commit);
-		base_tree = repo_get_commit_tree(the_repository, base);
-
-		merge_opt.branch2 = short_commit_name(commit);
-		merge_opt.ancestor = xstrfmt("parent of %s", merge_opt.branch2);
-
-		merge_incore_nonrecursive(&merge_opt,
-					  base_tree,
-					  result.tree,
-					  next_tree,
-					  &result);
-
-		free((char*)merge_opt.ancestor);
-		merge_opt.ancestor = NULL;
-		if (!result.clean)
+		pick = pick_regular_commit(commit, last_commit, &merge_opt, &result);
+		if (!pick)
 			break;
+		last_commit = pick;
 		last_picked_commit = commit;
-		last_commit = create_commit(result.tree, commit, last_commit);
 	}
 
 	merge_finalize(&merge_opt, &result);
-- 
2.42.0.126.gcf8c984877


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v4 06/15] replay: don't simplify history
  2023-09-07  9:25     ` [PATCH v4 " Christian Couder
                         ` (4 preceding siblings ...)
  2023-09-07  9:25       ` [PATCH v4 05/15] replay: introduce pick_regular_commit() Christian Couder
@ 2023-09-07  9:25       ` Christian Couder
  2023-09-07 10:23         ` Johannes Schindelin
  2023-09-07  9:25       ` [PATCH v4 07/15] replay: add an important FIXME comment about gpg signing Christian Couder
                         ` (10 subsequent siblings)
  16 siblings, 1 reply; 208+ messages in thread
From: Christian Couder @ 2023-09-07  9:25 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Christian Couder

From: Elijah Newren <newren@gmail.com>

Let's set the rev walking options we need after calling
setup_revisions() instead of before. This makes it clearer which options
we need.

Also we don't want history simplification, as we want to deal with all
the commits in the affected range.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 15 ++++++---------
 1 file changed, 6 insertions(+), 9 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index c66888679b..4b1e501595 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -173,15 +173,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 
 	repo_init_revisions(the_repository, &revs, prefix);
 
-	revs.verbose_header = 1;
-	revs.max_parents = 1;
-	revs.cherry_mark = 1;
-	revs.limited = 1;
-	revs.reverse = 1;
-	revs.right_only = 1;
-	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
-	revs.topo_order = 1;
-
 	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
 
 	if (setup_revisions(rev_walk_args.nr, rev_walk_args.v, &revs, NULL) > 1) {
@@ -189,6 +180,12 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		goto cleanup;
 	}
 
+	/* requirements/overrides for revs */
+	revs.reverse = 1;
+	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
+	revs.topo_order = 1;
+	revs.simplify_history = 0;
+
 	strvec_clear(&rev_walk_args);
 
 	if (prepare_revision_walk(&revs) < 0) {
-- 
2.42.0.126.gcf8c984877


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v4 07/15] replay: add an important FIXME comment about gpg signing
  2023-09-07  9:25     ` [PATCH v4 " Christian Couder
                         ` (5 preceding siblings ...)
  2023-09-07  9:25       ` [PATCH v4 06/15] replay: don't simplify history Christian Couder
@ 2023-09-07  9:25       ` Christian Couder
  2023-09-07  9:25       ` [PATCH v4 08/15] replay: remove progress and info output Christian Couder
                         ` (9 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-09-07  9:25 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Christian Couder

From: Elijah Newren <newren@gmail.com>

We want to be able to handle signed commits in some way in the future,
but we are not ready to do it now. So for the time being let's just add
a FIXME comment to remind us about it.

These are different ways we could handle them:

  - in case of a cli user and if there was an interactive mode, we could
    perhaps ask if the user wants to sign again
  - we could add an option to just fail if there are signed commits

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 4b1e501595..47d695df93 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -62,7 +62,7 @@ static struct commit *create_commit(struct tree *tree,
 	struct object *obj;
 	struct commit_list *parents = NULL;
 	char *author;
-	char *sign_commit = NULL;
+	char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
 	struct commit_extra_header *extra;
 	struct strbuf msg = STRBUF_INIT;
 	const char *out_enc = get_commit_output_encoding();
-- 
2.42.0.126.gcf8c984877


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v4 08/15] replay: remove progress and info output
  2023-09-07  9:25     ` [PATCH v4 " Christian Couder
                         ` (6 preceding siblings ...)
  2023-09-07  9:25       ` [PATCH v4 07/15] replay: add an important FIXME comment about gpg signing Christian Couder
@ 2023-09-07  9:25       ` Christian Couder
  2023-09-07  9:25       ` [PATCH v4 09/15] replay: remove HEAD related sanity check Christian Couder
                         ` (8 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-09-07  9:25 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Christian Couder

From: Elijah Newren <newren@gmail.com>

The replay command will be changed in a follow up commit, so that it
will not update refs directly, but instead it will print on stdout a
list of commands that can be consumed by `git update-ref --stdin`.

We don't want this output to be polluted by its current low value
output, so let's just remove the latter.

In the future, when the command gets an option to update refs by
itself, it will make a lot of sense to display a progress meter, but
we are not there yet.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 7 +------
 1 file changed, 1 insertion(+), 6 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 47d695df93..b5c854c686 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -195,7 +195,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
-	merge_opt.show_rename_progress = 1;
+	merge_opt.show_rename_progress = 0;
 	merge_opt.branch1 = "HEAD";
 	head_tree = repo_get_commit_tree(the_repository, onto);
 	result.tree = head_tree;
@@ -203,9 +203,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	while ((commit = get_revision(&revs))) {
 		struct commit *pick;
 
-		fprintf(stderr, "Rebasing %s...\r",
-			oid_to_hex(&commit->object.oid));
-
 		if (!commit->parents)
 			die(_("replaying down to root commit is not supported yet!"));
 		if (commit->parents->next)
@@ -224,7 +221,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		exit(128);
 
 	if (result.clean) {
-		fprintf(stderr, "\nDone.\n");
 		strbuf_addf(&reflog_msg, "finish rebase %s onto %s",
 			    oid_to_hex(&last_picked_commit->object.oid),
 			    oid_to_hex(&last_commit->object.oid));
@@ -238,7 +234,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		if (create_symref("HEAD", branch_name.buf, reflog_msg.buf) < 0)
 			die(_("unable to update HEAD"));
 	} else {
-		fprintf(stderr, "\nAborting: Hit a conflict.\n");
 		strbuf_addf(&reflog_msg, "rebase progress up to %s",
 			    oid_to_hex(&last_picked_commit->object.oid));
 		if (update_ref(reflog_msg.buf, "HEAD",
-- 
2.42.0.126.gcf8c984877


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v4 09/15] replay: remove HEAD related sanity check
  2023-09-07  9:25     ` [PATCH v4 " Christian Couder
                         ` (7 preceding siblings ...)
  2023-09-07  9:25       ` [PATCH v4 08/15] replay: remove progress and info output Christian Couder
@ 2023-09-07  9:25       ` Christian Couder
  2023-09-07  9:25       ` [PATCH v4 10/15] replay: make it a minimal server side command Christian Couder
                         ` (7 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-09-07  9:25 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Christian Couder

From: Elijah Newren <newren@gmail.com>

We want replay to be a command that can be used on the server side on
any branch, not just the current one, so we are going to stop updating
HEAD in a future commit.

A "sanity check" that makes sure we are replaying the current branch
doesn't make sense anymore. Let's remove it.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 8 +-------
 1 file changed, 1 insertion(+), 7 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index b5c854c686..a2636fbdcc 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -123,7 +123,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct commit *onto;
 	const char *onto_name = NULL;
 	struct commit *last_commit = NULL, *last_picked_commit = NULL;
-	struct object_id head;
 	struct lock_file lock = LOCK_INIT;
 	struct strvec rev_walk_args = STRVEC_INIT;
 	struct rev_info revs;
@@ -162,11 +161,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	onto = peel_committish(onto_name);
 	strbuf_addf(&branch_name, "refs/heads/%s", argv[2]);
 
-	/* Sanity check */
-	if (repo_get_oid(the_repository, "HEAD", &head))
-		die(_("Cannot read HEAD"));
-	assert(oideq(&onto->object.oid, &head));
-
 	repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
 	if (repo_read_index(the_repository) < 0)
 		BUG("Could not read index");
@@ -238,7 +232,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			    oid_to_hex(&last_picked_commit->object.oid));
 		if (update_ref(reflog_msg.buf, "HEAD",
 			       &last_commit->object.oid,
-			       &head,
+			       &onto->object.oid,
 			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
 			error(_("could not update %s"), argv[2]);
 			die("Failed to update %s", argv[2]);
-- 
2.42.0.126.gcf8c984877


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v4 10/15] replay: make it a minimal server side command
  2023-09-07  9:25     ` [PATCH v4 " Christian Couder
                         ` (8 preceding siblings ...)
  2023-09-07  9:25       ` [PATCH v4 09/15] replay: remove HEAD related sanity check Christian Couder
@ 2023-09-07  9:25       ` Christian Couder
  2023-09-07  9:25       ` [PATCH v4 11/15] replay: use standard revision ranges Christian Couder
                         ` (6 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-09-07  9:25 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Christian Couder

From: Elijah Newren <newren@gmail.com>

We want this command to be a minimal command that just does server side
picking of commits, displaying the results on stdout for higher level
scripts to consume.

So let's simplify it:
  * remove the worktree and index reading/writing,
  * remove the ref (and reflog) updating,
  * remove the assumptions tying us to HEAD, since (a) this is not a
    rebase and (b) we want to be able to pick commits in a bare repo,
    i.e. to/from branches that are not checked out and not the main
    branch,
  * remove unneeded includes,
  * handle rebasing multiple branches by printing on stdout the update
    ref commands that should be performed.

The output can be piped into `git update-ref --stdin` for the ref
updates to happen.

In the future to make it easier for users to use this command
directly maybe an option can be added to automatically pipe its output
into `git update-ref`.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c                         | 78 ++++++++----------------
 t/t6429-merge-sequence-rename-caching.sh | 39 +++++++-----
 2 files changed, 50 insertions(+), 67 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index a2636fbdcc..e45cd59da1 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -6,11 +6,7 @@
 #include "git-compat-util.h"
 
 #include "builtin.h"
-#include "cache-tree.h"
-#include "commit.h"
 #include "environment.h"
-#include "gettext.h"
-#include "hash.h"
 #include "hex.h"
 #include "lockfile.h"
 #include "merge-ort.h"
@@ -18,8 +14,6 @@
 #include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
-#include "sequencer.h"
-#include "setup.h"
 #include "strvec.h"
 #include <oidset.h>
 #include <tree.h>
@@ -102,6 +96,7 @@ static struct commit *pick_regular_commit(struct commit *pickme,
 	pickme_tree = repo_get_commit_tree(the_repository, pickme);
 	base_tree = repo_get_commit_tree(the_repository, base);
 
+	merge_opt->branch1 = short_commit_name(last_commit);
 	merge_opt->branch2 = short_commit_name(pickme);
 	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
 
@@ -122,15 +117,12 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 {
 	struct commit *onto;
 	const char *onto_name = NULL;
-	struct commit *last_commit = NULL, *last_picked_commit = NULL;
-	struct lock_file lock = LOCK_INIT;
+	struct commit *last_commit = NULL;
 	struct strvec rev_walk_args = STRVEC_INIT;
 	struct rev_info revs;
 	struct commit *commit;
 	struct merge_options merge_opt;
-	struct tree *head_tree;
 	struct merge_result result;
-	struct strbuf reflog_msg = STRBUF_INIT;
 	struct strbuf branch_name = STRBUF_INIT;
 	int ret = 0;
 
@@ -161,10 +153,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	onto = peel_committish(onto_name);
 	strbuf_addf(&branch_name, "refs/heads/%s", argv[2]);
 
-	repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
-	if (repo_read_index(the_repository) < 0)
-		BUG("Could not read index");
-
 	repo_init_revisions(the_repository, &revs, prefix);
 
 	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
@@ -190,58 +178,44 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
 	merge_opt.show_rename_progress = 0;
-	merge_opt.branch1 = "HEAD";
-	head_tree = repo_get_commit_tree(the_repository, onto);
-	result.tree = head_tree;
+	result.tree = repo_get_commit_tree(the_repository, onto);
 	last_commit = onto;
 	while ((commit = get_revision(&revs))) {
-		struct commit *pick;
+		const struct name_decoration *decoration;
 
 		if (!commit->parents)
 			die(_("replaying down to root commit is not supported yet!"));
 		if (commit->parents->next)
 			die(_("replaying merge commits is not supported yet!"));
 
-		pick = pick_regular_commit(commit, last_commit, &merge_opt, &result);
-		if (!pick)
+		last_commit = pick_regular_commit(commit, last_commit, &merge_opt, &result);
+		if (!last_commit)
 			break;
-		last_commit = pick;
-		last_picked_commit = commit;
+
+		decoration = get_name_decoration(&commit->object);
+		if (!decoration)
+			continue;
+
+		while (decoration) {
+			if (decoration->type == DECORATION_REF_LOCAL) {
+				printf("update %s %s %s\n",
+				       decoration->name,
+				       oid_to_hex(&last_commit->object.oid),
+				       oid_to_hex(&commit->object.oid));
+			}
+			decoration = decoration->next;
+		}
 	}
 
 	merge_finalize(&merge_opt, &result);
+	ret = result.clean;
 
-	if (result.clean < 0)
-		exit(128);
-
-	if (result.clean) {
-		strbuf_addf(&reflog_msg, "finish rebase %s onto %s",
-			    oid_to_hex(&last_picked_commit->object.oid),
-			    oid_to_hex(&last_commit->object.oid));
-		if (update_ref(reflog_msg.buf, branch_name.buf,
-			       &last_commit->object.oid,
-			       &last_picked_commit->object.oid,
-			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
-			error(_("could not update %s"), argv[2]);
-			die("Failed to update %s", argv[2]);
-		}
-		if (create_symref("HEAD", branch_name.buf, reflog_msg.buf) < 0)
-			die(_("unable to update HEAD"));
-	} else {
-		strbuf_addf(&reflog_msg, "rebase progress up to %s",
-			    oid_to_hex(&last_picked_commit->object.oid));
-		if (update_ref(reflog_msg.buf, "HEAD",
-			       &last_commit->object.oid,
-			       &onto->object.oid,
-			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
-			error(_("could not update %s"), argv[2]);
-			die("Failed to update %s", argv[2]);
-		}
-	}
-	ret = (result.clean == 0);
 cleanup:
-	strbuf_release(&reflog_msg);
 	strbuf_release(&branch_name);
 	release_revisions(&revs);
-	return ret;
+
+	/* Return */
+	if (ret < 0)
+		exit(128);
+	return ret ? 0 : 1;
 }
diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh
index 7670b72008..099aefeffc 100755
--- a/t/t6429-merge-sequence-rename-caching.sh
+++ b/t/t6429-merge-sequence-rename-caching.sh
@@ -71,8 +71,9 @@ test_expect_success 'caching renames does not preclude finding new ones' '
 
 		git switch upstream &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		git ls-files >tracked-files &&
 		test_line_count = 2 tracked-files &&
@@ -140,7 +141,9 @@ test_expect_success 'cherry-pick both a commit and its immediate revert' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 1 calls
@@ -198,8 +201,9 @@ test_expect_success 'rename same file identically, then reintroduce it' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		git ls-files >tracked &&
 		test_line_count = 2 tracked &&
@@ -275,8 +279,9 @@ test_expect_success 'rename same file identically, then add file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		git ls-files >tracked &&
 		test_line_count = 4 tracked &&
@@ -451,8 +456,9 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls &&
@@ -517,8 +523,9 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 3 calls &&
@@ -619,8 +626,9 @@ test_expect_success 'caching renames only on upstream side, part 1' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 1 calls &&
@@ -677,8 +685,9 @@ test_expect_success 'caching renames only on upstream side, part 2' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls &&
-- 
2.42.0.126.gcf8c984877


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v4 11/15] replay: use standard revision ranges
  2023-09-07  9:25     ` [PATCH v4 " Christian Couder
                         ` (9 preceding siblings ...)
  2023-09-07  9:25       ` [PATCH v4 10/15] replay: make it a minimal server side command Christian Couder
@ 2023-09-07  9:25       ` Christian Couder
  2023-09-07 10:24         ` Johannes Schindelin
  2023-09-08 22:55         ` Linus Arver
  2023-09-07  9:25       ` [PATCH v4 12/15] replay: disallow revision specific options and pathspecs Christian Couder
                         ` (5 subsequent siblings)
  16 siblings, 2 replies; 208+ messages in thread
From: Christian Couder @ 2023-09-07  9:25 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Christian Couder

From: Elijah Newren <newren@gmail.com>

Instead of the fixed "<oldbase> <branch>" arguments, the replay
command now accepts "<revision-range>..." arguments in a similar
way as many other Git commands. This makes its interface more
standard and more flexible.

Also as the interface of the command is now mostly finalized,
we can add some documentation as well as testcases to make sure
the command will continue to work as designed in the future.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 Documentation/git-replay.txt             | 90 ++++++++++++++++++++++++
 builtin/replay.c                         | 21 ++----
 t/t3650-replay-basics.sh                 | 83 ++++++++++++++++++++++
 t/t6429-merge-sequence-rename-caching.sh | 18 ++---
 4 files changed, 186 insertions(+), 26 deletions(-)
 create mode 100644 Documentation/git-replay.txt
 create mode 100755 t/t3650-replay-basics.sh

diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
new file mode 100644
index 0000000000..9a2087b01a
--- /dev/null
+++ b/Documentation/git-replay.txt
@@ -0,0 +1,90 @@
+git-replay(1)
+=============
+
+NAME
+----
+git-replay - Replay commits on a different base, without touching working tree
+
+
+SYNOPSIS
+--------
+[verse]
+'git replay' --onto <newbase> <revision-range>...
+
+DESCRIPTION
+-----------
+
+Takes a range of commits, and replays them onto a new location.  Does
+not touch the working tree or index, and does not update any
+references.  However, the output of this command is meant to be used
+as input to `git update-ref --stdin`, which would update the relevant
+branches.
+
+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
+
+OPTIONS
+-------
+
+--onto <newbase>::
+	Starting point at which to create the new commits.  May be any
+	valid commit, and not just an existing branch name.
++
+The update-ref command(s) in the output will update the branch(es) in
+the revision range to point at the new commits, similar to the way how
+`git rebase --update-refs` updates multiple branches in the affected
+range.
+
+<revision-range>::
+	Range of commits to replay; see "Specifying Ranges" in
+	linkgit:git-rev-parse.
+
+OUTPUT
+------
+
+When there are no conflicts, the output of this command is usable as
+input to `git update-ref --stdin`.  It is basically of the form:
+
+	update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
+	update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
+	update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
+
+where the number of refs updated depends on the arguments passed and
+the shape of the history being replayed.
+
+EXIT STATUS
+-----------
+
+For a successful, non-conflicted replay, the exit status is 0.  When
+the replay has conflicts, the exit status is 1.  If the replay is not
+able to complete (or start) due to some kind of error, the exit status
+is something other than 0 or 1.
+
+EXAMPLES
+--------
+
+To simply rebase mybranch onto target:
+
+------------
+$ git replay --onto target origin/main..mybranch
+update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
+------------
+
+When calling `git replay`, one does not need to specify a range of
+commits to replay using the syntax `A..B`; any range expression will
+do:
+
+------------
+$ git replay --onto origin/main ^base branch1 branch2 branch3
+update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
+update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
+update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
+------------
+
+This will simultaneously rebase branch1, branch2, and branch3 -- all
+commits they have since base, playing them on top of origin/main.
+These three branches may have commits on top of base that they have in
+common, but that does not need to be the case.
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/builtin/replay.c b/builtin/replay.c
index e45cd59da1..de2ddeae3e 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -14,7 +14,6 @@
 #include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
-#include "strvec.h"
 #include <oidset.h>
 #include <tree.h>
 
@@ -118,16 +117,14 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct commit *onto;
 	const char *onto_name = NULL;
 	struct commit *last_commit = NULL;
-	struct strvec rev_walk_args = STRVEC_INIT;
 	struct rev_info revs;
 	struct commit *commit;
 	struct merge_options merge_opt;
 	struct merge_result result;
-	struct strbuf branch_name = STRBUF_INIT;
 	int ret = 0;
 
 	const char * const replay_usage[] = {
-		N_("git replay --onto <newbase> <oldbase> <branch>"),
+		N_("git replay --onto <newbase> <revision-range>..."),
 		NULL
 	};
 	struct option replay_options[] = {
@@ -145,20 +142,13 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		usage_with_options(replay_usage, replay_options);
 	}
 
-	if (argc != 3) {
-		error(_("bad number of arguments"));
-		usage_with_options(replay_usage, replay_options);
-	}
-
 	onto = peel_committish(onto_name);
-	strbuf_addf(&branch_name, "refs/heads/%s", argv[2]);
 
 	repo_init_revisions(the_repository, &revs, prefix);
 
-	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
-
-	if (setup_revisions(rev_walk_args.nr, rev_walk_args.v, &revs, NULL) > 1) {
-		ret = error(_("unhandled options"));
+	argc = setup_revisions(argc, argv, &revs, NULL);
+	if (argc > 1) {
+		ret = error(_("unrecognized argument: %s"), argv[1]);
 		goto cleanup;
 	}
 
@@ -168,8 +158,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	revs.topo_order = 1;
 	revs.simplify_history = 0;
 
-	strvec_clear(&rev_walk_args);
-
 	if (prepare_revision_walk(&revs) < 0) {
 		ret = error(_("error preparing revisions"));
 		goto cleanup;
@@ -211,7 +199,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	ret = result.clean;
 
 cleanup:
-	strbuf_release(&branch_name);
 	release_revisions(&revs);
 
 	/* Return */
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
new file mode 100755
index 0000000000..a1da4f9ef9
--- /dev/null
+++ b/t/t3650-replay-basics.sh
@@ -0,0 +1,83 @@
+#!/bin/sh
+
+test_description='basic git replay tests'
+
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
+. ./test-lib.sh
+
+GIT_AUTHOR_NAME=author@name
+GIT_AUTHOR_EMAIL=bogus@email@address
+export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL
+
+test_expect_success 'setup' '
+	test_commit A &&
+	test_commit B &&
+
+	git switch -c topic1 &&
+	test_commit C &&
+	git switch -c topic2 &&
+	test_commit D &&
+	test_commit E &&
+	git switch topic1 &&
+	test_commit F &&
+	git switch -c topic3 &&
+	test_commit G &&
+	test_commit H &&
+	git switch -c topic4 main &&
+	test_commit I &&
+	test_commit J &&
+
+	git switch -c next main &&
+	test_commit K &&
+	git merge -m "Merge topic1" topic1 &&
+	git merge -m "Merge topic2" topic2 &&
+	git merge -m "Merge topic3" topic3 &&
+	>evil &&
+	git add evil &&
+	git commit --amend &&
+	git merge -m "Merge topic4" topic4 &&
+
+	git switch main &&
+	test_commit L &&
+	test_commit M &&
+
+	git switch -c conflict B &&
+	test_commit C.conflict C.t conflict
+'
+
+test_expect_success 'setup bare' '
+	git clone --bare . bare
+'
+
+test_expect_success 'using replay to rebase two branches, one on top of other' '
+	git replay --onto main topic1..topic2 >result &&
+
+	test_line_count = 1 result &&
+
+	git log --format=%s $(cut -f 3 -d " " result) >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/topic2 " >expect &&
+	printf "%s " $(cut -f 3 -d " " result) >>expect &&
+	git rev-parse topic2 >>expect &&
+
+	test_cmp expect result
+'
+
+test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '
+	git -C bare replay --onto main topic1..topic2 >result-bare &&
+	test_cmp expect result-bare
+'
+
+test_expect_success 'using replay to rebase with a conflict' '
+	test_expect_code 1 git replay --onto topic1 B..conflict
+'
+
+test_expect_success 'using replay on bare repo to rebase with a conflict' '
+	test_expect_code 1 git -C bare replay --onto topic1 B..conflict
+'
+
+test_done
diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh
index 099aefeffc..0f39ed0d08 100755
--- a/t/t6429-merge-sequence-rename-caching.sh
+++ b/t/t6429-merge-sequence-rename-caching.sh
@@ -71,7 +71,7 @@ test_expect_success 'caching renames does not preclude finding new ones' '
 
 		git switch upstream &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -141,7 +141,7 @@ test_expect_success 'cherry-pick both a commit and its immediate revert' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -201,7 +201,7 @@ test_expect_success 'rename same file identically, then reintroduce it' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -279,7 +279,7 @@ test_expect_success 'rename same file identically, then add file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -357,7 +357,7 @@ test_expect_success 'cached dir rename does not prevent noticing later conflict'
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test_must_fail git replay --onto HEAD upstream~1 topic >output &&
+		test_must_fail git replay --onto HEAD upstream~1..topic >output &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls
@@ -456,7 +456,7 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -523,7 +523,7 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -626,7 +626,7 @@ test_expect_success 'caching renames only on upstream side, part 1' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -685,7 +685,7 @@ test_expect_success 'caching renames only on upstream side, part 2' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
-- 
2.42.0.126.gcf8c984877


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v4 12/15] replay: disallow revision specific options and pathspecs
  2023-09-07  9:25     ` [PATCH v4 " Christian Couder
                         ` (10 preceding siblings ...)
  2023-09-07  9:25       ` [PATCH v4 11/15] replay: use standard revision ranges Christian Couder
@ 2023-09-07  9:25       ` Christian Couder
  2023-09-07 10:24         ` Johannes Schindelin
  2023-09-07  9:25       ` [PATCH v4 13/15] replay: add --advance or 'cherry-pick' mode Christian Couder
                         ` (4 subsequent siblings)
  16 siblings, 1 reply; 208+ messages in thread
From: Christian Couder @ 2023-09-07  9:25 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Christian Couder, Christian Couder

A previous commit changed `git replay` to make it accept standard
revision ranges using the setup_revisions() function. While this is a
good thing to make this command more standard and more flexible, it has
the downside of enabling many revision related options accepted and eaten
by setup_revisions().

Some of these options might make sense, but others, like those
generating non-contiguous history, might not. Anyway those we might want
to allow should probably be tested and perhaps documented a bit, which
could be done in future work.

For now it is just simpler and safer to just disallow all of them, so
let's do that.

Other commands, like `git fast-export`, currently allow all these
revision specific options even though some of them might not make sense,
as these commands also use setup_revisions() but do not check the
options that might be passed to this function.

So a way to fix those commands as well as git replay could be to improve
or refactor the setup_revisions() mechanism to let callers allow and
disallow options in a relevant way for them. Such improvements are
outside the scope of this work though.

Pathspecs, which are also accepted and eaten by setup_revisions(), are
likely to result in disconnected history. That could perhaps be useful,
but that would need tests and documentation, which can be added in
future work. So, while at it, let's disallow them too.

Helped-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c         | 26 +++++++++++++++++++++++++-
 t/t3650-replay-basics.sh | 16 ++++++++++++++++
 2 files changed, 41 insertions(+), 1 deletion(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index de2ddeae3e..60abdaee9e 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -121,7 +121,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct commit *commit;
 	struct merge_options merge_opt;
 	struct merge_result result;
-	int ret = 0;
+	int ret = 0, i;
 
 	const char * const replay_usage[] = {
 		N_("git replay --onto <newbase> <revision-range>..."),
@@ -137,6 +137,20 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
 			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
 
+	/*
+	 * TODO: For now, we reject any unknown or invalid option,
+	 * including revision related ones, like --not,
+	 * --first-parent, etc that would be allowed and eaten by
+	 * setup_revisions() below. In the future we should definitely
+	 * accept those that make sense and add related tests and doc
+	 * though.
+	 */
+	for (i = 0; i < argc; i++)
+		if (argv[i][0] == '-') {
+			error(_("invalid option: %s"), argv[i]);
+			usage_with_options(replay_usage, replay_options);
+		}
+
 	if (!onto_name) {
 		error(_("option --onto is mandatory"));
 		usage_with_options(replay_usage, replay_options);
@@ -152,6 +166,16 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		goto cleanup;
 	}
 
+	/*
+	 * Reject any pathspec. (They are allowed and eaten by
+	 * setup_revisions() above.) In the future we might accept
+	 * them, after adding related tests and doc though.
+	 */
+	if (revs.prune_data.nr) {
+		error(_("no pathspec is allowed: '%s'"), revs.prune_data.items[0].match);
+		usage_with_options(replay_usage, replay_options);
+	}
+
 	/* requirements/overrides for revs */
 	revs.reverse = 1;
 	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index a1da4f9ef9..de6e40950e 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -80,4 +80,20 @@ test_expect_success 'using replay on bare repo to rebase with a conflict' '
 	test_expect_code 1 git -C bare replay --onto topic1 B..conflict
 '
 
+test_expect_success 'using replay with (for now) disallowed revision specific option --not' '
+	test_must_fail git replay --onto main topic2 --not topic1
+'
+
+test_expect_success 'using replay on bare repo with (for now) disallowed revision specific option --first-parent' '
+	test_must_fail git -C bare replay --onto main --first-parent topic1..topic2
+'
+
+test_expect_success 'using replay with disallowed pathspec' '
+	test_must_fail git replay --onto main topic1..topic2 A.t
+'
+
+test_expect_success 'using replay on bare repo with disallowed pathspec' '
+	test_must_fail git -C bare replay --onto main topic1..topic2 -- A.t
+'
+
 test_done
-- 
2.42.0.126.gcf8c984877


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v4 13/15] replay: add --advance or 'cherry-pick' mode
  2023-09-07  9:25     ` [PATCH v4 " Christian Couder
                         ` (11 preceding siblings ...)
  2023-09-07  9:25       ` [PATCH v4 12/15] replay: disallow revision specific options and pathspecs Christian Couder
@ 2023-09-07  9:25       ` Christian Couder
  2023-09-07  9:25       ` [PATCH v4 14/15] replay: add --contained to rebase contained branches Christian Couder
                         ` (3 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-09-07  9:25 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Christian Couder

From: Elijah Newren <newren@gmail.com>

There is already a 'rebase' mode with `--onto`. Let's add an 'advance' or
'cherry-pick' mode with `--advance`. This new mode will make the target
branch advance as we replay commits onto it.

The replayed commits should have a single tip, so that it's clear where
the target branch should be advanced. If they have more than one tip,
this new mode will error out.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 Documentation/git-replay.txt |  40 ++++++--
 builtin/replay.c             | 185 +++++++++++++++++++++++++++++++++--
 t/t3650-replay-basics.sh     |  34 +++++++
 3 files changed, 242 insertions(+), 17 deletions(-)

diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
index 9a2087b01a..5c5c15237d 100644
--- a/Documentation/git-replay.txt
+++ b/Documentation/git-replay.txt
@@ -9,7 +9,7 @@ git-replay - Replay commits on a different base, without touching working tree
 SYNOPSIS
 --------
 [verse]
-'git replay' --onto <newbase> <revision-range>...
+'git replay' (--onto <newbase> | --advance <branch>) <revision-range>...
 
 DESCRIPTION
 -----------
@@ -29,14 +29,24 @@ OPTIONS
 	Starting point at which to create the new commits.  May be any
 	valid commit, and not just an existing branch name.
 +
-The update-ref command(s) in the output will update the branch(es) in
-the revision range to point at the new commits, similar to the way how
-`git rebase --update-refs` updates multiple branches in the affected
-range.
+When `--onto` is specified, the update-ref command(s) in the output will
+update the branch(es) in the revision range to point at the new
+commits, similar to the way how `git rebase --update-refs` updates
+multiple branches in the affected range.
+
+--advance <branch>::
+	Starting point at which to create the new commits; must be a
+	branch name.
++
+When `--advance` is specified, the update-ref command(s) in the output
+will update the branch passed as an argument to `--advance` to point at
+the new commits (in other words, this mimics a cherry-pick operation).
 
 <revision-range>::
-	Range of commits to replay; see "Specifying Ranges" in
-	linkgit:git-rev-parse.
+	Range of commits to replay. More than one <revision-range> can
+	be passed, but in `--advance <branch>` mode, they should have
+	a single tip, so that it's clear where <branch> should point
+	to. See "Specifying Ranges" in linkgit:git-rev-parse.
 
 OUTPUT
 ------
@@ -49,7 +59,9 @@ input to `git update-ref --stdin`.  It is basically of the form:
 	update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
 
 where the number of refs updated depends on the arguments passed and
-the shape of the history being replayed.
+the shape of the history being replayed.  When using `--advance`, the
+number of refs updated is always one, but for `--onto`, it can be one
+or more (rebasing multiple branches simultaneously is supported).
 
 EXIT STATUS
 -----------
@@ -69,6 +81,18 @@ $ git replay --onto target origin/main..mybranch
 update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
 ------------
 
+To cherry-pick the commits from mybranch onto target:
+
+------------
+$ git replay --advance target origin/main..mybranch
+update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}
+------------
+
+Note that the first two examples replay the exact same commits and on
+top of the exact same new base, they only differ in that the first
+provides instructions to make mybranch point at the new commits and
+the second provides instructions to make target point at them.
+
 When calling `git replay`, one does not need to specify a range of
 commits to replay using the syntax `A..B`; any range expression will
 do:
diff --git a/builtin/replay.c b/builtin/replay.c
index 60abdaee9e..6b89964be9 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -14,6 +14,7 @@
 #include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
+#include "strmap.h"
 #include <oidset.h>
 #include <tree.h>
 
@@ -82,6 +83,146 @@ static struct commit *create_commit(struct tree *tree,
 	return (struct commit *)obj;
 }
 
+struct ref_info {
+	struct commit *onto;
+	struct strset positive_refs;
+	struct strset negative_refs;
+	int positive_refexprs;
+	int negative_refexprs;
+};
+
+static void get_ref_information(struct rev_cmdline_info *cmd_info,
+				struct ref_info *ref_info)
+{
+	int i;
+
+	ref_info->onto = NULL;
+	strset_init(&ref_info->positive_refs);
+	strset_init(&ref_info->negative_refs);
+	ref_info->positive_refexprs = 0;
+	ref_info->negative_refexprs = 0;
+
+	/*
+	 * When the user specifies e.g.
+	 *   git replay origin/main..mybranch
+	 *   git replay ^origin/next mybranch1 mybranch2
+	 * we want to be able to determine where to replay the commits.  In
+	 * these examples, the branches are probably based on an old version
+	 * of either origin/main or origin/next, so we want to replay on the
+	 * newest version of that branch.  In contrast we would want to error
+	 * out if they ran
+	 *   git replay ^origin/master ^origin/next mybranch
+	 *   git replay mybranch~2..mybranch
+	 * the first of those because there's no unique base to choose, and
+	 * the second because they'd likely just be replaying commits on top
+	 * of the same commit and not making any difference.
+	 */
+	for (i = 0; i < cmd_info->nr; i++) {
+		struct rev_cmdline_entry *e = cmd_info->rev + i;
+		struct object_id oid;
+		const char *refexpr = e->name;
+		char *fullname = NULL;
+		int can_uniquely_dwim = 1;
+
+		if (*refexpr == '^')
+			refexpr++;
+		if (repo_dwim_ref(the_repository, refexpr, strlen(refexpr), &oid, &fullname, 0) != 1)
+			can_uniquely_dwim = 0;
+
+		if (e->flags & BOTTOM) {
+			if (can_uniquely_dwim)
+				strset_add(&ref_info->negative_refs, fullname);
+			if (!ref_info->negative_refexprs)
+				ref_info->onto = lookup_commit_reference_gently(the_repository,
+										&e->item->oid, 1);
+			ref_info->negative_refexprs++;
+		} else {
+			if (can_uniquely_dwim)
+				strset_add(&ref_info->positive_refs, fullname);
+			ref_info->positive_refexprs++;
+		}
+
+		free(fullname);
+	}
+}
+
+static void determine_replay_mode(struct rev_cmdline_info *cmd_info,
+				  const char *onto_name,
+				  const char **advance_name,
+				  struct commit **onto,
+				  struct strset **update_refs)
+{
+	struct ref_info rinfo;
+
+	get_ref_information(cmd_info, &rinfo);
+	if (!rinfo.positive_refexprs)
+		die(_("need some commits to replay"));
+	if (onto_name && *advance_name)
+		die(_("--onto and --advance are incompatible"));
+	else if (onto_name) {
+		*onto = peel_committish(onto_name);
+		if (rinfo.positive_refexprs <
+		    strset_get_size(&rinfo.positive_refs))
+			die(_("all positive revisions given must be references"));
+	} else if (*advance_name) {
+		struct object_id oid;
+		char *fullname = NULL;
+
+		*onto = peel_committish(*advance_name);
+		if (repo_dwim_ref(the_repository, *advance_name, strlen(*advance_name),
+			     &oid, &fullname, 0) == 1) {
+			*advance_name = fullname;
+		} else {
+			die(_("argument to --advance must be a reference"));
+		}
+		if (rinfo.positive_refexprs > 1)
+			die(_("cannot advance target with multiple sources because ordering would be ill-defined"));
+	} else {
+		int positive_refs_complete = (
+			rinfo.positive_refexprs ==
+			strset_get_size(&rinfo.positive_refs));
+		int negative_refs_complete = (
+			rinfo.negative_refexprs ==
+			strset_get_size(&rinfo.negative_refs));
+		/*
+		 * We need either positive_refs_complete or
+		 * negative_refs_complete, but not both.
+		 */
+		if (rinfo.negative_refexprs > 0 &&
+		    positive_refs_complete == negative_refs_complete)
+			die(_("cannot implicitly determine whether this is an --advance or --onto operation"));
+		if (negative_refs_complete) {
+			struct hashmap_iter iter;
+			struct strmap_entry *entry;
+
+			if (rinfo.negative_refexprs == 0)
+				die(_("all positive revisions given must be references"));
+			else if (rinfo.negative_refexprs > 1)
+				die(_("cannot implicitly determine whether this is an --advance or --onto operation"));
+			else if (rinfo.positive_refexprs > 1)
+				die(_("cannot advance target with multiple source branches because ordering would be ill-defined"));
+
+			/* Only one entry, but we have to loop to get it */
+			strset_for_each_entry(&rinfo.negative_refs,
+					      &iter, entry) {
+				*advance_name = entry->key;
+			}
+		} else { /* positive_refs_complete */
+			if (rinfo.negative_refexprs > 1)
+				die(_("cannot implicitly determine correct base for --onto"));
+			if (rinfo.negative_refexprs == 1)
+				*onto = rinfo.onto;
+		}
+	}
+	if (!*advance_name) {
+		*update_refs = xcalloc(1, sizeof(**update_refs));
+		**update_refs = rinfo.positive_refs;
+		memset(&rinfo.positive_refs, 0, sizeof(**update_refs));
+	}
+	strset_clear(&rinfo.negative_refs);
+	strset_clear(&rinfo.positive_refs);
+}
+
 static struct commit *pick_regular_commit(struct commit *pickme,
 					  struct commit *last_commit,
 					  struct merge_options *merge_opt,
@@ -114,20 +255,26 @@ static struct commit *pick_regular_commit(struct commit *pickme,
 
 int cmd_replay(int argc, const char **argv, const char *prefix)
 {
-	struct commit *onto;
+	const char *advance_name = NULL;
+	struct commit *onto = NULL;
 	const char *onto_name = NULL;
-	struct commit *last_commit = NULL;
+
 	struct rev_info revs;
+	struct commit *last_commit = NULL;
 	struct commit *commit;
 	struct merge_options merge_opt;
 	struct merge_result result;
+	struct strset *update_refs = NULL;
 	int ret = 0, i;
 
 	const char * const replay_usage[] = {
-		N_("git replay --onto <newbase> <revision-range>..."),
+		N_("git replay (--onto <newbase> | --advance <branch>) <revision-range>..."),
 		NULL
 	};
 	struct option replay_options[] = {
+		OPT_STRING(0, "advance", &advance_name,
+			   N_("branch"),
+			   N_("make replay advance given branch")),
 		OPT_STRING(0, "onto", &onto_name,
 			   N_("revision"),
 			   N_("replay onto given commit")),
@@ -151,13 +298,11 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			usage_with_options(replay_usage, replay_options);
 		}
 
-	if (!onto_name) {
-		error(_("option --onto is mandatory"));
+	if (!onto_name && !advance_name) {
+		error(_("option --onto or --advance is mandatory"));
 		usage_with_options(replay_usage, replay_options);
 	}
 
-	onto = peel_committish(onto_name);
-
 	repo_init_revisions(the_repository, &revs, prefix);
 
 	argc = setup_revisions(argc, argv, &revs, NULL);
@@ -182,6 +327,12 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	revs.topo_order = 1;
 	revs.simplify_history = 0;
 
+	determine_replay_mode(&revs.cmdline, onto_name, &advance_name,
+			      &onto, &update_refs);
+
+	if (!onto) /* FIXME: Should handle replaying down to root commit */
+		die("Replaying down to root commit is not supported yet!");
+
 	if (prepare_revision_walk(&revs) < 0) {
 		ret = error(_("error preparing revisions"));
 		goto cleanup;
@@ -190,6 +341,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
 	merge_opt.show_rename_progress = 0;
+
 	result.tree = repo_get_commit_tree(the_repository, onto);
 	last_commit = onto;
 	while ((commit = get_revision(&revs))) {
@@ -204,12 +356,15 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		if (!last_commit)
 			break;
 
+		/* Update any necessary branches */
+		if (advance_name)
+			continue;
 		decoration = get_name_decoration(&commit->object);
 		if (!decoration)
 			continue;
-
 		while (decoration) {
-			if (decoration->type == DECORATION_REF_LOCAL) {
+			if (decoration->type == DECORATION_REF_LOCAL &&
+			    strset_contains(update_refs, decoration->name)) {
 				printf("update %s %s %s\n",
 				       decoration->name,
 				       oid_to_hex(&last_commit->object.oid),
@@ -219,10 +374,22 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		}
 	}
 
+	/* In --advance mode, advance the target ref */
+	if (result.clean == 1 && advance_name) {
+		printf("update %s %s %s\n",
+		       advance_name,
+		       oid_to_hex(&last_commit->object.oid),
+		       oid_to_hex(&onto->object.oid));
+	}
+
 	merge_finalize(&merge_opt, &result);
 	ret = result.clean;
 
 cleanup:
+	if (update_refs) {
+		strset_clear(update_refs);
+		free(update_refs);
+	}
 	release_revisions(&revs);
 
 	/* Return */
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index de6e40950e..1919f7d5d1 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -96,4 +96,38 @@ test_expect_success 'using replay on bare repo with disallowed pathspec' '
 	test_must_fail git -C bare replay --onto main topic1..topic2 -- A.t
 '
 
+test_expect_success 'using replay to perform basic cherry-pick' '
+	# The differences between this test and previous ones are:
+	#   --advance vs --onto
+	# 2nd field of result is refs/heads/main vs. refs/heads/topic2
+	# 4th field of result is hash for main instead of hash for topic2
+
+	git replay --advance main topic1..topic2 >result &&
+
+	test_line_count = 1 result &&
+
+	git log --format=%s $(cut -f 3 -d " " result) >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/main " >expect &&
+	printf "%s " $(cut -f 3 -d " " result) >>expect &&
+	git rev-parse main >>expect &&
+
+	test_cmp expect result
+'
+
+test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
+	git -C bare replay --advance main topic1..topic2 >result-bare &&
+	test_cmp expect result-bare
+'
+
+test_expect_success 'replay on bare repo fails with both --advance and --onto' '
+	test_must_fail git -C bare replay --advance main --onto main topic1..topic2 >result-bare
+'
+
+test_expect_success 'replay fails when both --advance and --onto are omitted' '
+	test_must_fail git replay topic1..topic2 >result
+'
+
 test_done
-- 
2.42.0.126.gcf8c984877


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v4 14/15] replay: add --contained to rebase contained branches
  2023-09-07  9:25     ` [PATCH v4 " Christian Couder
                         ` (12 preceding siblings ...)
  2023-09-07  9:25       ` [PATCH v4 13/15] replay: add --advance or 'cherry-pick' mode Christian Couder
@ 2023-09-07  9:25       ` Christian Couder
  2023-09-07  9:25       ` [PATCH v4 15/15] replay: stop assuming replayed branches do not diverge Christian Couder
                         ` (2 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-09-07  9:25 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Christian Couder

From: Elijah Newren <newren@gmail.com>

Let's add a `--contained` option that can be used along with
`--onto` to rebase all the branches contained in the <revision-range>
argument.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 Documentation/git-replay.txt | 12 +++++++++++-
 builtin/replay.c             | 12 ++++++++++--
 t/t3650-replay-basics.sh     | 29 +++++++++++++++++++++++++++++
 3 files changed, 50 insertions(+), 3 deletions(-)

diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
index 5c5c15237d..b94c39b161 100644
--- a/Documentation/git-replay.txt
+++ b/Documentation/git-replay.txt
@@ -9,7 +9,7 @@ git-replay - Replay commits on a different base, without touching working tree
 SYNOPSIS
 --------
 [verse]
-'git replay' (--onto <newbase> | --advance <branch>) <revision-range>...
+'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...
 
 DESCRIPTION
 -----------
@@ -93,6 +93,16 @@ top of the exact same new base, they only differ in that the first
 provides instructions to make mybranch point at the new commits and
 the second provides instructions to make target point at them.
 
+What if you have a stack of branches, one depending upon another, and
+you'd really like to rebase the whole set?
+
+------------
+$ git replay --contained --onto origin/main origin/main..tipbranch
+update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
+update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
+update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}
+------------
+
 When calling `git replay`, one does not need to specify a range of
 commits to replay using the syntax `A..B`; any range expression will
 do:
diff --git a/builtin/replay.c b/builtin/replay.c
index 6b89964be9..a7d36a639c 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -258,6 +258,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	const char *advance_name = NULL;
 	struct commit *onto = NULL;
 	const char *onto_name = NULL;
+	int contained = 0;
 
 	struct rev_info revs;
 	struct commit *last_commit = NULL;
@@ -268,7 +269,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	int ret = 0, i;
 
 	const char * const replay_usage[] = {
-		N_("git replay (--onto <newbase> | --advance <branch>) <revision-range>..."),
+		N_("git replay ([--contained] --onto <newbase> | --advance <branch>) <revision-range>..."),
 		NULL
 	};
 	struct option replay_options[] = {
@@ -278,6 +279,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		OPT_STRING(0, "onto", &onto_name,
 			   N_("revision"),
 			   N_("replay onto given commit")),
+		OPT_BOOL(0, "contained", &contained,
+			 N_("advance all branches contained in revision-range")),
 		OPT_END()
 	};
 
@@ -303,6 +306,10 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		usage_with_options(replay_usage, replay_options);
 	}
 
+	if (advance_name && contained)
+		die(_("options '%s' and '%s' cannot be used together"),
+		    "--advance", "--contained");
+
 	repo_init_revisions(the_repository, &revs, prefix);
 
 	argc = setup_revisions(argc, argv, &revs, NULL);
@@ -364,7 +371,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			continue;
 		while (decoration) {
 			if (decoration->type == DECORATION_REF_LOCAL &&
-			    strset_contains(update_refs, decoration->name)) {
+			    (contained || strset_contains(update_refs,
+							  decoration->name))) {
 				printf("update %s %s %s\n",
 				       decoration->name,
 				       oid_to_hex(&last_commit->object.oid),
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 1919f7d5d1..57d2ef9ea4 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -130,4 +130,33 @@ test_expect_success 'replay fails when both --advance and --onto are omitted' '
 	test_must_fail git replay topic1..topic2 >result
 '
 
+test_expect_success 'using replay to also rebase a contained branch' '
+	git replay --contained --onto main main..topic3 >result &&
+
+	test_line_count = 2 result &&
+	cut -f 3 -d " " result >new-branch-tips &&
+
+	git log --format=%s $(head -n 1 new-branch-tips) >actual &&
+	test_write_lines F C M L B A >expect &&
+	test_cmp expect actual &&
+
+	git log --format=%s $(tail -n 1 new-branch-tips) >actual &&
+	test_write_lines H G F C M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/topic1 " >expect &&
+	printf "%s " $(head -n 1 new-branch-tips) >>expect &&
+	git rev-parse topic1 >>expect &&
+	printf "update refs/heads/topic3 " >>expect &&
+	printf "%s " $(tail -n 1 new-branch-tips) >>expect &&
+	git rev-parse topic3 >>expect &&
+
+	test_cmp expect result
+'
+
+test_expect_success 'using replay on bare repo to also rebase a contained branch' '
+	git -C bare replay --contained --onto main main..topic3 >result-bare &&
+	test_cmp expect result-bare
+'
+
 test_done
-- 
2.42.0.126.gcf8c984877


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v4 15/15] replay: stop assuming replayed branches do not diverge
  2023-09-07  9:25     ` [PATCH v4 " Christian Couder
                         ` (13 preceding siblings ...)
  2023-09-07  9:25       ` [PATCH v4 14/15] replay: add --contained to rebase contained branches Christian Couder
@ 2023-09-07  9:25       ` Christian Couder
  2023-09-07 10:25       ` [PATCH v4 00/15] Introduce new `git replay` command Johannes Schindelin
  2023-10-10 12:38       ` [PATCH v5 00/14] " Christian Couder
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-09-07  9:25 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Christian Couder

From: Elijah Newren <newren@gmail.com>

The replay command is able to replay multiple branches but when some of
them are based on other replayed branches, their commit should be
replayed onto already replayed commits.

For this purpose, let's store the replayed commit and its original
commit in a key value store, so that we can easily find and reuse a
replayed commit instead of the original one.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c         | 44 ++++++++++++++++++++++++++--------
 t/t3650-replay-basics.sh | 52 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 86 insertions(+), 10 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index a7d36a639c..5479406863 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -223,20 +223,33 @@ static void determine_replay_mode(struct rev_cmdline_info *cmd_info,
 	strset_clear(&rinfo.positive_refs);
 }
 
+static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,
+				    struct commit *commit,
+				    struct commit *fallback)
+{
+	khint_t pos = kh_get_oid_map(replayed_commits, commit->object.oid);
+	if (pos == kh_end(replayed_commits))
+		return fallback;
+	return kh_value(replayed_commits, pos);
+}
+
 static struct commit *pick_regular_commit(struct commit *pickme,
-					  struct commit *last_commit,
+					  kh_oid_map_t *replayed_commits,
+					  struct commit *onto,
 					  struct merge_options *merge_opt,
 					  struct merge_result *result)
 {
-	struct commit *base;
+	struct commit *base, *replayed_base;
 	struct tree *pickme_tree, *base_tree;
 
 	base = pickme->parents->item;
+	replayed_base = mapped_commit(replayed_commits, base, onto);
 
+	result->tree = repo_get_commit_tree(the_repository, replayed_base);
 	pickme_tree = repo_get_commit_tree(the_repository, pickme);
 	base_tree = repo_get_commit_tree(the_repository, base);
 
-	merge_opt->branch1 = short_commit_name(last_commit);
+	merge_opt->branch1 = short_commit_name(replayed_base);
 	merge_opt->branch2 = short_commit_name(pickme);
 	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
 
@@ -250,7 +263,7 @@ static struct commit *pick_regular_commit(struct commit *pickme,
 	merge_opt->ancestor = NULL;
 	if (!result->clean)
 		return NULL;
-	return create_commit(result->tree, pickme, last_commit);
+	return create_commit(result->tree, pickme, replayed_base);
 }
 
 int cmd_replay(int argc, const char **argv, const char *prefix)
@@ -266,6 +279,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct merge_options merge_opt;
 	struct merge_result result;
 	struct strset *update_refs = NULL;
+	kh_oid_map_t *replayed_commits;
 	int ret = 0, i;
 
 	const char * const replay_usage[] = {
@@ -348,21 +362,30 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
 	merge_opt.show_rename_progress = 0;
-
-	result.tree = repo_get_commit_tree(the_repository, onto);
 	last_commit = onto;
+	replayed_commits = kh_init_oid_map();
 	while ((commit = get_revision(&revs))) {
 		const struct name_decoration *decoration;
+		khint_t pos;
+		int hr;
 
 		if (!commit->parents)
 			die(_("replaying down to root commit is not supported yet!"));
 		if (commit->parents->next)
 			die(_("replaying merge commits is not supported yet!"));
 
-		last_commit = pick_regular_commit(commit, last_commit, &merge_opt, &result);
+		last_commit = pick_regular_commit(commit, replayed_commits, onto,
+						  &merge_opt, &result);
 		if (!last_commit)
 			break;
 
+		/* Record commit -> last_commit mapping */
+		pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr);
+		if (hr == 0)
+			BUG("Duplicate rewritten commit: %s\n",
+			    oid_to_hex(&commit->object.oid));
+		kh_value(replayed_commits, pos) = last_commit;
+
 		/* Update any necessary branches */
 		if (advance_name)
 			continue;
@@ -391,13 +414,14 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	}
 
 	merge_finalize(&merge_opt, &result);
-	ret = result.clean;
-
-cleanup:
+	kh_destroy_oid_map(replayed_commits);
 	if (update_refs) {
 		strset_clear(update_refs);
 		free(update_refs);
 	}
+	ret = result.clean;
+
+cleanup:
 	release_revisions(&revs);
 
 	/* Return */
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 57d2ef9ea4..fe5ae0391d 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -159,4 +159,56 @@ test_expect_success 'using replay on bare repo to also rebase a contained branch
 	test_cmp expect result-bare
 '
 
+test_expect_success 'using replay to rebase multiple divergent branches' '
+	git replay --onto main ^topic1 topic2 topic4 >result &&
+
+	test_line_count = 2 result &&
+	cut -f 3 -d " " result >new-branch-tips &&
+
+	git log --format=%s $(head -n 1 new-branch-tips) >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual &&
+
+	git log --format=%s $(tail -n 1 new-branch-tips) >actual &&
+	test_write_lines J I M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/topic2 " >expect &&
+	printf "%s " $(head -n 1 new-branch-tips) >>expect &&
+	git rev-parse topic2 >>expect &&
+	printf "update refs/heads/topic4 " >>expect &&
+	printf "%s " $(tail -n 1 new-branch-tips) >>expect &&
+	git rev-parse topic4 >>expect &&
+
+	test_cmp expect result
+'
+
+test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '
+	git -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&
+
+	test_line_count = 4 result &&
+	cut -f 3 -d " " result >new-branch-tips &&
+
+	>expect &&
+	for i in 2 1 3 4
+	do
+		printf "update refs/heads/topic$i " >>expect &&
+		printf "%s " $(grep topic$i result | cut -f 3 -d " ") >>expect &&
+		git -C bare rev-parse topic$i >>expect || return 1
+	done &&
+
+	test_cmp expect result &&
+
+	test_write_lines F C M L B A >expect1 &&
+	test_write_lines E D C M L B A >expect2 &&
+	test_write_lines H G F C M L B A >expect3 &&
+	test_write_lines J I M L B A >expect4 &&
+
+	for i in 1 2 3 4
+	do
+		git -C bare log --format=%s $(grep topic$i result | cut -f 3 -d " ") >actual &&
+		test_cmp expect$i actual || return 1
+	done
+'
+
 test_done
-- 
2.42.0.126.gcf8c984877


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* Re: [PATCH 11/14] replay: use standard revision ranges
  2023-09-07  8:39                           ` Christian Couder
@ 2023-09-07 10:22                             ` Johannes Schindelin
  0 siblings, 0 replies; 208+ messages in thread
From: Johannes Schindelin @ 2023-09-07 10:22 UTC (permalink / raw)
  To: Christian Couder
  Cc: Elijah Newren, Derrick Stolee, git, Junio C Hamano,
	Patrick Steinhardt, John Cai, Christian Couder

[-- Attachment #1: Type: text/plain, Size: 4502 bytes --]

Hi Christian,

On Thu, 7 Sep 2023, Christian Couder wrote:

> On Sun, Sep 3, 2023 at 5:47 PM Johannes Schindelin
> <Johannes.Schindelin@gmx.de> wrote:
> >
> > On Sat, 29 Apr 2023, Elijah Newren wrote:
> >
> > > On Mon, Apr 24, 2023 at 8:23 AM Derrick Stolee <derrickstolee@github.com> wrote:
>
> > > > Basically, I'm not super thrilled about exposing options that are
> > > > unlikely to be valuable to users and instead are more likely to cause
> > > > confusion due to changes that won't successfully apply.
> > >
> > > Oh, I got thrown by the "right now" portion of your comment; I
> > > couldn't see how time or future changes would affect anything to make
> > > it less (or more) confusing for users.
> > >
> > > Quick clarification, though: while you correctly point out the type of
> > > confusion the user would experience without my overriding, my
> > > overriding of rev.reverse (after setup_revisions() returns, not before
> > > it is called) precludes that experience.  The override means none of
> > > the above happens, and they would instead just wonder why their option
> > > is being ignored.
> >
> > FWIW here is my view on the matter: `git replay`, at least in its current
> > incarnation, is a really low-level tool. As such, I actually do not want
> > to worry much about protecting users from nonsensical invocations.
> >
> > In that light, I would like to see that code rejecting all revision
> > options except `--diff-algorithm` be dropped. Should we ever decide to add
> > a non-low-level mode to `git replay`, we can easily add some user-friendly
> > sanity check of the options then, and only for that non-low-level code.
> > For now, I feel that it's just complicating things, and `git replay` is in
> > the experimental phase anyway.
>
> I would be Ok with removing the patch (called "replay: disallow
> revision specific options and pathspecs")
> that rejects all revision options and pathspecs if there is a
> consensus for that.

Well, since you talk about "consensus" and I already made my strong
preference known, how about you add yours?

> It might not simplify things too much if there is still an exception for
> `--diff-algorithm` though. Also it's not clear if you are Ok with
> allowing pathspecs or not.

I want to remove all the rev-list-option-disallowing code. Including the
pathspec one.

> The idea with disallowing all of them was to later add back those that
> make sense along with tests and maybe docs to explain them in the
> context of this command. It was not to disallow them permanently. So I
> would think the best path forward would be a patch series on top of this
> one that would revert the patch disallowing these options and maybe
> pathspecs, and instead allow most of them and document and test things a
> bit.

I understand that that was the reasoning.

What I would like to suggest is that we should treat `git replay` as a
low-level (or: "plumbing" in Git Speak) command.

That would allow us to leave the option for stricter command-line
parameter validation to an _additional_ option, to be added later (or
never), and to stop worrying about this tool being used in ways that make
no sense (or make no sense _to us_, _now_).

> > And further, I would even like to see that `--reverse` override go, and
> > turn it into `revs.reverse = !revs.reverse` instead. (And yes, I can
> > easily think of instances where I would have wanted to reverse a series of
> > patches...).
>
> I think this might deserve docs and tests too,

I agree.

> so it might want to be part of a separate patch series once the existing
> one has graduated.

Here, I disagree. This is a bug, from my perspective, and needs to be
fixed before graduating.

> At this point I don't think it's worth delaying this patch series for
> relatively small issues like this. There are many different ways this
> new command can be polished and improved. The important thing is that
> it looks like we all agree that the new command makes sense and should
> have roughly the basic set of features that Elijah originally
> implemented, so let's go with this, and then we can improve and
> iterate on top of this.

If it were for a small issue such as a typo, sure.

But it is a bug that `--no-reverse` is hard-coded (in a confusing manner
so because the `reverse` attribute is set), and the
`--no-reverse`/`--reverse` options are ignored, silently so.

Ciao,
Johannes

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v4 02/15] replay: introduce new builtin
  2023-09-07  9:25       ` [PATCH v4 02/15] replay: introduce new builtin Christian Couder
@ 2023-09-07 10:23         ` Johannes Schindelin
  2023-10-10 12:42           ` Christian Couder
  0 siblings, 1 reply; 208+ messages in thread
From: Johannes Schindelin @ 2023-09-07 10:23 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Patrick Steinhardt, Elijah Newren, John Cai,
	Derrick Stolee, Phillip Wood, Calvin Wan, Toon Claes,
	Christian Couder

Hi Christian & Elijah,

On Thu, 7 Sep 2023, Christian Couder wrote:

> diff --git a/command-list.txt b/command-list.txt
> index 54b2a50f5f..d74836ab21 100644
> --- a/command-list.txt
> +++ b/command-list.txt
> @@ -160,6 +160,7 @@ git-reflog                              ancillarymanipulators           complete
>  git-remote                              ancillarymanipulators           complete
>  git-repack                              ancillarymanipulators           complete
>  git-replace                             ancillarymanipulators           complete
> +git-replay                              mainporcelain           history

I recall this having come up before, but in light of the still active
discussion revolving around command-line parameter validation, I would
strongly advise removing `git replay` from the main page and instead go
for plumbingmanipulators.

Ciao,
Johannes

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v4 06/15] replay: don't simplify history
  2023-09-07  9:25       ` [PATCH v4 06/15] replay: don't simplify history Christian Couder
@ 2023-09-07 10:23         ` Johannes Schindelin
  2023-10-10 12:43           ` Christian Couder
  0 siblings, 1 reply; 208+ messages in thread
From: Johannes Schindelin @ 2023-09-07 10:23 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Patrick Steinhardt, Elijah Newren, John Cai,
	Derrick Stolee, Phillip Wood, Calvin Wan, Toon Claes,
	Christian Couder

Hi Christian & Elijah,

On Thu, 7 Sep 2023, Christian Couder wrote:

> From: Elijah Newren <newren@gmail.com>
>
> Let's set the rev walking options we need after calling
> setup_revisions() instead of before. This makes it clearer which options
> we need.

In light of the currently open issue about command-line validation, this
change does more than this paragraph lets on: It hardcodes certain
settings, overriding (silently) any rev-list options the user might have
passed.

Is there any chance that we can avoid this change?

> Also we don't want history simplification, as we want to deal with all
> the commits in the affected range.

This, however, is a good change. It deserves to live in its own commit,
with its own commit message, in particular because it is not obvious from
the attribute names which ones we're talking about (I guess it's `limited`
and `simplify_history`, not just the latter.

Ciao,
Johannes

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v4 11/15] replay: use standard revision ranges
  2023-09-07  9:25       ` [PATCH v4 11/15] replay: use standard revision ranges Christian Couder
@ 2023-09-07 10:24         ` Johannes Schindelin
  2023-10-10 12:49           ` Christian Couder
  2023-09-08 22:55         ` Linus Arver
  1 sibling, 1 reply; 208+ messages in thread
From: Johannes Schindelin @ 2023-09-07 10:24 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Patrick Steinhardt, Elijah Newren, John Cai,
	Derrick Stolee, Phillip Wood, Calvin Wan, Toon Claes,
	Christian Couder

Hi Christian,

It is a bit surprising to see the manual page added in _this_ patch, in
the middle of the series... I can live with it, though.

On Thu, 7 Sep 2023, Christian Couder wrote:

> diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
> new file mode 100644
> index 0000000000..9a2087b01a
> --- /dev/null
> +++ b/Documentation/git-replay.txt
> @@ -0,0 +1,90 @@
> +git-replay(1)
> +=============
> +
> +NAME
> +----
> +git-replay - Replay commits on a different base, without touching working tree
> +
> +
> +SYNOPSIS
> +--------
> +[verse]
> +'git replay' --onto <newbase> <revision-range>...

We need to make it clear here, already in the SYNOPSIS, that this is
experimental. Let's add an `(EXPERIMENTAL!)` prefix here.

> [...]
> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
> new file mode 100755

Just like the manual page, I would have expected this test to be
introduced earlier, and not piggy-backed onto one of the handful "let's
turn fast-rebase into replay" patches.

Ciao,
Johannes

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v4 12/15] replay: disallow revision specific options and pathspecs
  2023-09-07  9:25       ` [PATCH v4 12/15] replay: disallow revision specific options and pathspecs Christian Couder
@ 2023-09-07 10:24         ` Johannes Schindelin
  2023-10-10 12:49           ` Christian Couder
  0 siblings, 1 reply; 208+ messages in thread
From: Johannes Schindelin @ 2023-09-07 10:24 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Patrick Steinhardt, Elijah Newren, John Cai,
	Derrick Stolee, Phillip Wood, Calvin Wan, Toon Claes,
	Christian Couder

Hi Christian & Elijah,

On Thu, 7 Sep 2023, Christian Couder wrote:

> A previous commit changed `git replay` to make it accept standard
> revision ranges using the setup_revisions() function. While this is a
> good thing to make this command more standard and more flexible, it has
> the downside of enabling many revision related options accepted and eaten
> by setup_revisions().
>
> Some of these options might make sense, but others, like those
> generating non-contiguous history, might not. Anyway those we might want
> to allow should probably be tested and perhaps documented a bit, which
> could be done in future work.
>
> For now it is just simpler and safer to just disallow all of them, so
> let's do that.
>
> Other commands, like `git fast-export`, currently allow all these
> revision specific options even though some of them might not make sense,
> as these commands also use setup_revisions() but do not check the
> options that might be passed to this function.
>
> So a way to fix those commands as well as git replay could be to improve
> or refactor the setup_revisions() mechanism to let callers allow and
> disallow options in a relevant way for them. Such improvements are
> outside the scope of this work though.
>
> Pathspecs, which are also accepted and eaten by setup_revisions(), are
> likely to result in disconnected history. That could perhaps be useful,
> but that would need tests and documentation, which can be added in
> future work. So, while at it, let's disallow them too.

As pointed out elsewhere in this mail thread, I consider this patch to do
more harm than good. After switching the command to plumbingmanipulators
it should be possible to just forego all command-line validation and leave
that job to the caller.

Therefore I would love to see this patch dropped.

Ciao,
Johannes

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v4 00/15] Introduce new `git replay` command
  2023-09-07  9:25     ` [PATCH v4 " Christian Couder
                         ` (14 preceding siblings ...)
  2023-09-07  9:25       ` [PATCH v4 15/15] replay: stop assuming replayed branches do not diverge Christian Couder
@ 2023-09-07 10:25       ` Johannes Schindelin
  2023-10-10 12:50         ` Christian Couder
  2023-10-10 12:38       ` [PATCH v5 00/14] " Christian Couder
  16 siblings, 1 reply; 208+ messages in thread
From: Johannes Schindelin @ 2023-09-07 10:25 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Patrick Steinhardt, Elijah Newren, John Cai,
	Derrick Stolee, Phillip Wood, Calvin Wan, Toon Claes

Hi Christian,

hope you had a restful vacation!

On Thu, 7 Sep 2023, Christian Couder wrote:

> # Changes between v3 and v4
>
> Thanks to Toon, Junio and Dscho for their suggestions on the previous
> version! The very few and minor changes compared to v3 are:
>
> * The patch series was rebased onto master at d814540bb7 (The fifth
>   batch, 2023-09-01). This is to fix a few header related conflicts as
>   can be seen in the range-diff.
>
> * In patch 10/15 (replay: make it a minimal server side command) a /*
>   Cleanup */ code comment has been removed as suggested by Toon.
>
> * In patch 11/15 (replay: use standard revision ranges) the git-replay
>   documentation related to --onto has been improved to better explain
>   which branches will be updated by the update-ref command(s) in the
>   output as suggested by Junio.
>
> * In patch 12/15 (replay: disallow revision specific options and
>   pathspecs) an error message has been improved as suggested by Junio.
>
> * In patch 13/15 (replay: add --advance or 'cherry-pick' mode) the
>   commit message and the git-replay documentation have been improved
>   to better explain that --advance only works when the revision range
>   passed has a single tip as suggested by Junio.
>
> * Also in patch 13/15 (replay: add --advance or 'cherry-pick' mode) an
>   error message has been improved, and a few tests have been added to
>   check that `git replay` fails when it's passed both --advance and
>   --onto and when it's passed none of these options, as suggested by
>   Toon.

I left a bit of feedback and think that once my concerns are addressed, a
v5 will be ready for `next`.

Ciao,
Johannes

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v3 11/15] replay: use standard revision ranges
  2023-09-07  8:32         ` Christian Couder
@ 2023-09-07 21:02           ` Dragan Simic
  2023-10-10 12:44             ` Christian Couder
  0 siblings, 1 reply; 208+ messages in thread
From: Dragan Simic @ 2023-09-07 21:02 UTC (permalink / raw)
  To: Christian Couder
  Cc: Toon Claes, git, Junio C Hamano, Patrick Steinhardt,
	Johannes Schindelin, Elijah Newren, John Cai, Derrick Stolee,
	Phillip Wood, Felipe Contreras, Calvin Wan, Christian Couder

On 2023-09-07 10:32, Christian Couder wrote:
> On Thu, Jun 22, 2023 at 12:03 PM Toon Claes <toon@iotcl.com> wrote:
>> 
>> Christian Couder <christian.couder@gmail.com> writes:
>> 
>> > +DESCRIPTION
>> > +-----------
>> > +
>> > +Takes a range of commits, and replays them onto a new location.  Does
>> > +not touch the working tree or index, and does not update any
>> > +references.  However, the output of this command is meant to be used
>> 
>> Small suggestion here:
>> 
>> Takes a range of commits, and replays them onto a new location.  Does
>> neither touch the working tree nor index, and does not update any
>> references.
> 
> I am not a native speaker, so I am not sure what's best here. I find
> your suggestion a bit less clear though, so until a native speaker
> agrees with it or maybe finds something even better, I prefer to leave
> it as-is.

I'm also not a native English speaker, but I spent about 2.5 years 
contributing a whole lot to English Wikipedia, so I'd dare to say I've 
honed my English skills rather well.  Thus, here's my take on this:

     Takes a range of commits and replays them onto a new location.
     Leaves the working tree and the index untouched, and updates no
     references.  The output of this command is to be used...

This is written in a concise yet slightly imperative way, which should 
be suitable for the purpose.  I hope you agree.

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v4 11/15] replay: use standard revision ranges
  2023-09-07  9:25       ` [PATCH v4 11/15] replay: use standard revision ranges Christian Couder
  2023-09-07 10:24         ` Johannes Schindelin
@ 2023-09-08 22:55         ` Linus Arver
  2023-09-10  3:20           ` Linus Arver
  2023-10-10 12:48           ` Christian Couder
  1 sibling, 2 replies; 208+ messages in thread
From: Linus Arver @ 2023-09-08 22:55 UTC (permalink / raw)
  To: Christian Couder, git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Christian Couder

Hi Christian,

I am only reviewing the docs. To assume the mindset of a Git user
unfamiliar with this command, I purposely did not read the cover letter
until after this review was done.

Christian Couder <christian.couder@gmail.com> writes:

> [...]
>
> diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
> new file mode 100644
> index 0000000000..9a2087b01a
> --- /dev/null
> +++ b/Documentation/git-replay.txt
> @@ -0,0 +1,90 @@
> +git-replay(1)
> +=============
> +
> +NAME
> +----
> +git-replay - Replay commits on a different base, without touching working tree

How about using the same language ("new location") as in the DESCRIPTION
heading? Also, the "without touching working tree" part is incomplete
because as explained later on, the index and refs are also left alone.
How about just "safely"?

    git-replay - Replay commits onto a new location, safely

> +SYNOPSIS
> +--------
> +[verse]
> +'git replay' --onto <newbase> <revision-range>...
> +
> +DESCRIPTION
> +-----------
> +
> +Takes a range of commits, and replays them onto a new location.

OK.

> Does
> +not touch the working tree or index, and does not update any
> +references.

How about this version?

    The new commits are created without modifying the working tree,
    index, or any references.

Also, by "references" we mean the refs in ".git/refs/*". In the
gitrevisions man page we use the term "refnames" to refer to these bits,
so maybe "refnames" is better than "references"? The simpler "branches"
is another option.

> However, the output of this command is meant to be used
> +as input to `git update-ref --stdin`, which would update the relevant
> +branches.

Before we get to this sentence, it would be great to explain why this
command is useful (what problem does it solve)?

Also, it would help to add "(see OUTPUT section below)" as a
navigational aid in case some readers are wondering what the output
looks like (and have not yet gotten to that section).

I've noticed that you're using the phrase "starting point" in the
OPTIONS section. I think this is better than "location" or "base" (FWIW
we started using "starting point" in 0a02ca2383 (SubmittingPatches:
simplify guidance for choosing a starting point, 2023-07-14)).

The following is a version of this section that attempts to address my
comments above (I've included the other bits already reviewed earlier to
make it easier to read):

    Takes a range of commits, and replays them onto a new starting
    point. The new commits are created without modifying the working
    tree, index, or any branches. If there are branches that point to
    the commits in <revision-range>, list them in the output with both
    the original commit hash and the corresponding (replayed) commit
    hash (see OUTPUT section below) .

    This command is like linkgit:git-rebase[1], but notably does not
    require a working tree or index. This means you can run this command
    in a bare repo (useful for server-side environments). And because
    nothing is modified (only new commits are created), it's like a "dry
    run" rebase.

    By combining this command with `git update-ref --stdin`, the
    relevant branches can be updated. That is, the branches that were
    pointing originally to the commits given in the <revision-range>
    will be updated to point to the replayed commits. This is similar to
    the way how `git rebase --update-refs` updates multiple branches in
    the affected range.

> +THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
> +
> +OPTIONS
> +-------
> +
> +--onto <newbase>::

How about 'starting-point' instead of 'newbase'?

> +	Starting point at which to create the new commits.  May be any
> +	valid commit, and not just an existing branch name.

Add "See linkgit:gitrevisions[7]." at the end?

> +The update-ref command(s) in the output will update the branch(es) in
> +the revision range to point at the new commits, similar to the way how
> +`git rebase --update-refs` updates multiple branches in the affected
> +range.

Ah, good example. I've moved this to my larger example above, so I don't
think this paragraph is needed here any more (it probably didn't belong
in OPTIONS anyway).

> +<revision-range>::
> +	Range of commits to replay; see "Specifying Ranges" in
> +	linkgit:git-rev-parse.

OK.

> +OUTPUT
> +------
> +
> +When there are no conflicts, the output of this command is usable as
> +input to `git update-ref --stdin`.

What happens if there are conflicts? Probably good to mention in the
DISCUSSION section. Some questions you may want to answer for the
reader:

(1) Is git-replay an all-or-nothing operation? That is, if there are any
conflicts, is the output always empty (or do we still output those
branches that *can* be updated without conflicts)?

(2) What is meant by "conflict" for git-replay? Is it the same meaning
as the case for git-rebase?

For (1), in your cover letter under "# Important limitations" you say
"No resumability" but I am not sure if this means git-replay will output
*something* before exiting with an error, or simply nothing at all.

Speaking of the limitations section, perhaps it's worth pointing those
out under DISCUSSION as well?

> It is basically of the form:

Why "basically"? Are there cases where the output can be different than
the example given below? If not, then perhaps drop the word "basically"?

> +	update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
> +	update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
> +	update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
> +
> +where the number of refs updated depends on the arguments passed and
> +the shape of the history being replayed.

Let's use "number of branches" instead of "number of refs" here to be
consistent with the language elsewhere.

> +EXIT STATUS
> +-----------
> +
> +For a successful, non-conflicted replay, the exit status is 0.  When
> +the replay has conflicts, the exit status is 1.

OK.

> If the replay is not
> +able to complete (or start) due to some kind of error, the exit status
> +is something other than 0 or 1.

Not sure how useful "due to some kind of error" is here --- presumably
the inability to replay is always due to some kind of error.

Would it be worth including a brief explanation about why git-replay
might be unable to complete or start (is this behavior in practice a
common-enough thing to document here)?

> +EXAMPLES
> +--------
> +
> +To simply rebase mybranch onto target:

Looking at the CLI arguments, I think this phrase should be:

    Replay the commits in `origin/main..mybranch` onto `target`:

> +------------
> +$ git replay --onto target origin/main..mybranch
> +update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
> +------------
> +
> +When calling `git replay`, one does not need to specify a range of
> +commits to replay using the syntax `A..B`; any range expression will
> +do:
> +
> +------------
> +$ git replay --onto origin/main ^base branch1 branch2 branch3

Instead of `base`, how about `olderbranch`?

> +update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
> +update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
> +update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
> +------------
> +
> +This will simultaneously rebase branch1, branch2, and branch3 -- all
> +commits they have since base, playing them on top of origin/main.

How about

    This will rebase the commits in `branch1`, `branch2`, and `branch3`
    (excluding those in `base`), preplaying them on top of `origin/main`.

> +These three branches may have commits on top of base that they have in
> +common, but that does not need to be the case.

s/base/`base`

> +GIT
> +---
> +Part of the linkgit:git[1] suite

One question I do have is what happens if you run git-replay twice
(successfully)? Does the second invocation create another set of (new)
successfully replayed commits? I ask because I'm interested in informing
the readers of our docs about any potential pitfalls from abusing this
command by mistake.

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v4 11/15] replay: use standard revision ranges
  2023-09-08 22:55         ` Linus Arver
@ 2023-09-10  3:20           ` Linus Arver
  2023-10-10 12:48             ` Christian Couder
  2023-10-10 12:48           ` Christian Couder
  1 sibling, 1 reply; 208+ messages in thread
From: Linus Arver @ 2023-09-10  3:20 UTC (permalink / raw)
  To: Christian Couder, git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Christian Couder

Linus Arver <linusa@google.com> writes:

>> +update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
>> +update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
>> +update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
>> +------------
>> +
>> +This will simultaneously rebase branch1, branch2, and branch3 -- all
>> +commits they have since base, playing them on top of origin/main.
>
> How about
>
>     This will rebase the commits in `branch1`, `branch2`, and `branch3`
>     (excluding those in `base`), preplaying them on top of `origin/main`.

Oops, I meant "replaying" not "preplaying". But also, perhaps the
following is simpler?

    This will replay the commits in `branch1`, `branch2`, and `branch3`
    (excluding those in `base`), on top of `origin/main`.

^ permalink raw reply	[flat|nested] 208+ messages in thread

* [PATCH v5 00/14] Introduce new `git replay` command
  2023-09-07  9:25     ` [PATCH v4 " Christian Couder
                         ` (15 preceding siblings ...)
  2023-09-07 10:25       ` [PATCH v4 00/15] Introduce new `git replay` command Johannes Schindelin
@ 2023-10-10 12:38       ` Christian Couder
  2023-10-10 12:38         ` [PATCH v5 01/14] t6429: remove switching aspects of fast-rebase Christian Couder
                           ` (16 more replies)
  16 siblings, 17 replies; 208+ messages in thread
From: Christian Couder @ 2023-10-10 12:38 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

# Intro

`git replay` has initially been developed entirely by Elijah Newren
mostly last year (2022) at:

https://github.com/newren/git/commits/replay

I took over this year to polish and upstream it as GitLab is
interested in replacing libgit2, and for that purpose needs a command
to do server side (so without using a worktree) rebases, cherry-picks
and reverts.

I reduced the number of commits and features in this first patch
series, compared to what Elijah already developed. Especially I
stopped short of replaying merge commits and replaying
interactively. These and other features might be upstreamed in the
future after this patch series has graduated.

The focus in this series is to make it a good plumbing command that
can already be used server side and that replaces the "fast-rebase"
test-tool command. So things to make it easier to use on the command
line, and more advanced features (like replaying merges) are left out.

It looks like GitHub has actually already been using version 3 of this
patch series in production with good results. See:

https://github.blog/2023-07-27-scaling-merge-ort-across-github/
https://lore.kernel.org/git/304f2a49-5e05-7655-9f87-2011606df5db@gmx.de/

# Content of this cover letter

The "Quick Overview" and "Reasons for diverging from cherry-pick &
rebase" sections just below are describing the purpose of the new
command in the big scheme of things. They are taken from Elijah's
design notes
(https://github.com/newren/git/blob/replay/replay-design-notes.txt)
and describe what we want this command to become and the reasons for
that, not what the command is after only this patch series. Also these
design notes were written at least one year ago, so parts of those 2
sections are not true anymore. I have added Phillip Wood's or Felipe
Contreras' notes (thanks to them) where that's the case, but some now
flawed parts may have missed.

After these two sections, starting with the "Important limitations"
section, you will find sections describing what is actually in this
patch series.

More interesting material is available in Elijah's design notes like
an "Intro via examples"
(https://github.com/newren/git/blob/replay/replay-design-notes.txt#L37-L132),
a discussion about "Preserving topology, replaying merges"
(https://github.com/newren/git/blob/replay/replay-design-notes.txt#L264-L341)
and a "Current status" section describing Elijah's work
(https://github.com/newren/git/blob/replay/replay-design-notes.txt#L344-L392)
before I started working on upstreaming it.

I have not included this material here though, as the documentation
added by this patch series for the `git replay` command already
includes an "EXAMPLES" section, and other sections of Elijah's design
notes might not be interesting for now. Also this cover letter is
already pretty long.  But reviewers can refer to the links above if
they think it can help.

# Quick Overview (from Elijah's design notes)

`git replay`, at a basic level, can perhaps be thought of as a
"default-to-dry-run rebase" -- meaning no updates to the working tree,
or to the index, or to any references.  However, it differs from
rebase in that it:

  * Works for branches that aren't checked out

  * Works in a bare repository

  * Can replay multiple branches simultaneously (with or without common
    history in the range being replayed)

  * Preserves relative topology by default (merges are replayed too in
    Elijah's original work, not in this series)

  * Focuses on performance

  * Has several altered defaults as a result of the above

I sometimes think of `git replay` as "fast-replay", a patch-based
analogue to the snapshot-based fast-export & fast-import tools.

# Reasons for diverging from cherry-pick & rebase (from Elijah's
  design notes)

There are multiple reasons to diverge from the defaults in cherry-pick and
rebase.

* Server side needs

  * Both cherry-pick and rebase, via the sequencer, are heavily tied
    to updating the working tree, index, some refs, and a lot of
    control files with every commit replayed, and invoke a mess of
    hooks[1] that might be hard to avoid for backward compatibility
    reasons (at least, that's been brought up a few times on the
    list).

  * cherry-pick and rebase both fork various subprocesses
    unnecessarily, but somewhat intrinsically in part to ensure the
    same hooks are called that old scripted implementations would have
    called.

    Note: since 356ee4659bb (sequencer: try to commit without forking
    'git commit', 2017-11-24) cherry-pick and rebase do not fork
    subprocesses other than hooks for the cases covered by this patch
    series (i.e. they do not fork "git commit" for simple picks).

  * "Dry run" behavior, where there are no updates to worktree, index,
    or even refs might be important.

  * Should not assume users only want to operate on HEAD (see next
    section)

* Decapitate HEAD-centric assumptions

  * cherry-pick forces commits to be played on top of HEAD;
    inflexible.

  * rebase assumes the range of commits to be replayed is
    upstream..HEAD by default, though it allows one to replay
    upstream..otherbranch -- but it still forcibly and needlessly
    checks out 'otherbranch' before starting to replay things.

    Note: since 767a9c417eb (rebase -i: stop checking out the tip of
    the branch to rebase, 2020-01-24) it's not true that rebase
    forcibly and needlessly checks out 'otherbranch'.

  * Assuming HEAD is involved severely limits replaying multiple
    (possibly divergent) branches.

    Note: since 89fc0b53fdb (rebase: update refs from 'update-ref'
    commands, 2022-07-19) the sequencer can update multiple
    branches. The issue with divergent branch is with command line
    arguments and the todo list generation rather than the
    capabilities of the sequencer.

  * Once you stop assuming HEAD has a certain meaning, there's not
    much reason to have two separate commands anymore (except for the
    funny extra not-necessarily-compatible options both have gained
    over time).

  * (Micro issue: Assuming HEAD is involved also makes it harder for
    new users to learn what rebase means and does; it makes command
    lines hard to parse.  Not sure I want to harp on this too much, as
    I have a suspicion I might be creating a tool for experts with
    complicated use cases, but it's a minor quibble.)

* Performance

  * jj is slaughtering us on rebase speed[2].  I would like us to become
    competitive.  (I dropped a few comments in the link at [2] about why
    git is currently so bad.)

  * From [3], there was a simple 4-patch series in linux.git that took
    53 seconds to rebase.  Switching to ort dropped it to 16 seconds.
    While that sounds great, only 11 *milliseconds* were needed to do
    the actual merges.  That means almost *all* the time (>99%) was
    overhead!  Big offenders:

    * --reapply-cherry-picks should be the default

    * can_fast_forward() should be ripped out, and perhaps other extraneous
      revision walks

      Note: d42c9ffa0f (rebase: factor out branch_base calculation,
      2022-10-17) might already deal with that (according to Felipe
      Contreras).

    * avoid updating working tree, index, refs, reflogs, and control
      structures except when needed (e.g. hitting a conflict, or operation
      finished)

  * Other performance ideas (mostly for future work, not in this
    series)

    * single-file control structures instead of directory of files
      (when doing interactive things which is in Elijah's original
      work, but not in this series)

    * avoid forking subprocesses unless explicitly requested (e.g.
      --exec, --strategy, --run-hooks).  For example, definitely do not
      invoke `git commit` or `git merge`.

    * Sanitize hooks:

      * dispense with all per-commit hooks for sure (pre-commit,
        post-commit, post-checkout).

      * pre-rebase also seems to assume exactly 1 ref is written, and
        invoking it repeatedly would be stupid.  Plus, it's specific
        to "rebase".  So...ignore?  (Stolee's --ref-update option for
        rebase probably broke the pre-rebase assumptions already...)

      * post-rewrite hook might make sense, but fast-import got
        exempted, and I think of replay like a patch-based analogue
        to the snapshot-based fast-import.

    * When not running server side, resolve conflicts in a sparse-cone
      sparse-index worktree to reduce number of files written to a
      working tree.  (See below as well.)

    * [High risk of possible premature optimization] Avoid large
      numbers of newly created loose objects, when replaying large
      numbers of commits.  Two possibilities: (1) Consider using
      tmp-objdir and pack objects from the tmp-objdir at end of
      exercise, (2) Lift code from git-fast-import to immediately
      stuff new objects into a pack?

* Multiple branches and non-checked out branches

  * The ability to operate on non-checked out branches also implies
    that we should generally be able to replay when in a dirty working
    tree (exception being when we expect to update HEAD and any of the
    dirty files is one that needs to be updated by the replay).

  * Also, if we are operating locally on a non-checked out branch and
    hit a conflict, we should have a way to resolve the conflict
    without messing with the user's work on their current
    branch. (This is not is this patch series though.)

    * Idea: new worktree with sparse cone + sparse index checkout,
      containing only files in the root directory, and whatever is
      necessary to get the conflicts

    * Companion to above idea: control structures should be written to
      $GIT_COMMON_DIR/replay-${worktree}, so users can have multiple
      replay sessions, and so we know which worktrees are associated
      with which replay operations.

  - [1] https://lore.kernel.org/git/pull.749.v3.git.git.1586044818132.gitgitgadget@gmail.com/
  - [2] https://github.com/martinvonz/jj/discussions/49
  - [3] https://lore.kernel.org/git/CABPp-BE48=97k_3tnNqXPjSEfA163F8hoE+HY0Zvz1SWB2B8EA@mail.gmail.com/

# Important limitations

* The code exits with code 1 if there are any conflict. No
  resumability. No nice output. No interactivity. No special exit code
  depending on the reason.

* When a commit becomes empty as it is replayed, it is still replayed
  as an empty commit, instead of being dropped.

* No replaying merges, nor root commits. Only regular commits.

* Signed commits are not properly handled. It's not clear what to do
  to such commits when replaying on the server side.

* Notes associated with replayed commits are not updated nor
  duplicated. (Thanks to Phillip Wood for noticing.)

# Commit overview

* 1/14 t6429: remove switching aspects of fast-rebase

    Preparatory commit to make it easier to later replace the
    fast-rebase test-tool by `git replay` without breaking existing
    tests.

* 2/14 replay: introduce new builtin

    This creates a minimal `git replay` command by moving the code
    from the `fast-rebase` test helper from `t/helper/` into
    `builtin/` and doing some renames and a few other needed changes.

* - 3/14 replay: start using parse_options API
  - 4/14 replay: die() instead of failing assert()
  - 5/14 replay: introduce pick_regular_commit()
  - 6/14 replay: change rev walking options
  - 7/14 replay: add an important FIXME comment about gpg signing
  - 8/14 replay: remove progress and info output
  - 9/14 replay: remove HEAD related sanity check

    These slowly change the command to make it behave more like
    regular commands and to start cleaning up its output.

* 10/14 replay: make it a minimal server side command

    After the cleaning up in previous commits, it's now time to
    radically change the way it works by stopping it to do ref
    updates, to update the index and worktree, to consider HEAD as
    special. Instead just make it output commands that should be
    passed to `git update-ref --stdin`.

* 11/14 replay: use standard revision ranges

    Start addind new interesting features and also documentation and
    tests, as the interface of the command is cristalizing into its
    final form.

* - 12/14 replay: add --advance or 'cherry-pick' mode
  - 13/14 replay: add --contained to rebase contained branches

    Add new options and features to the command.

* 14/14 replay: stop assuming replayed branches do not diverge

    This adds another interesting feature, as well as related
    documentation and tests.

# Notes about `fast-rebase`, tests and documentation

The `fast-rebase` test-tool helper was developed by Elijah to
experiment with a rebasing tool that would be developed from scratch
based on his merge-ort work, could be used to test that merge-ort
work, and would not have the speed and interface limitations of `git
rebase` or `git cherry-pick`.

This `fast-rebase` helper was used before this series in:

t6429-merge-sequence-rename-caching.sh

So when `git replay` is created from `fast-rebase` in patch 2/14, the
t6429 test script is also converted to use `git replay`. This ensures
that `git replay` doesn't break too badly during the first 10 patches
in this patch series.

Tests and documentation are introduced specifically for `git replay`
only in 11/14 and later patches as it doesn't make much sense to
document and test behavior that we know is going to change soon. So
it's only when the command is crystalizing towards its final form that
we start documenting and testing it.

# Possibly controversial issues 

* bare or not bare: this series works towards a plumbing command with
  the end goal of it being usable and used first on bare repos,
  contrary to existing commands like `git rebase` and `git
  cherry-pick`. The tests check that the command works on both bare
  and non-bare repo though.

* exit status: a successful, non-conflicted replay exits with code
  0. When the replay has conflicts, the exit status is 1. If the
  replay is not able to complete (or start) due to some kind of error,
  the exit status is something other than 0 or 1. There are a few
  tests checking that. It has been suggested in an internal review
  that conflicts might want to get a more specific error code as an
  error code of 1 might be quite easy to return by accident. It
  doesn't seem to me from their docs (which might want to be improved,
  I didn't look at the code) that other commands like `git merge` and
  `git rebase` exit with a special error code in case of conflict.

* make worktree and index changes optional: commit 10/14 stops
  updating the index and worktree, but it might be better especially
  for cli users to make that optional. The issue is that this would
  make the command more complex while we are developing a number of
  important features so that the command can be used on bare repos. It
  seems that this should rather be done in an iterative improvement
  after the important features have landed.

* when and where to add tests and docs: although t6429 has tests that
  are changed to use the new command instead of the fast-rebase
  test-tool command as soon as the former is introduced, there is no
  specific test script and no doc for the new command until commit
  11/14 when standard revision ranges are used. This is done to avoid
  churn in tests and docs while the final form of the command hasn't
  crystalized enough. Adding tests and doc at this point makes this
  commit quite big and possibly more difficult to review than if they
  were in separate commits though. On the other hand when tests and
  docs are added in specific commits some reviewers say it would be
  better to introduce them when the related changes are made.

* --advance and --contained: these two advanced options might not
  belong to this first series and could perhaps be added in a followup
  series in separate commits. On the other hand the code for
  --contained seems involved with the code of --advance and it's nice
  to see soon that git replay can indeed do cherry-picking and rebase
  many refs at once, and this way fullfil these parts of its promise.

* replaying diverging branches: 14/14 the last patch in the series,
  which allow replaying diverging branches, can be seen as a
  fundamental fix or alternatively as adding an interesting
  feature. So it's debatable if it should be in its own patch along
  with its own tests as in this series, or if it should be merged into
  a previous patch and which one.

* only 2 patches: this patch series can be seen from a high level
  point of view as 1) introducing the new `git replay` command, and 2)
  using `git replay` to replace, and get rid of, the fast-rebase
  test-tool command. The fact that not much of the original
  fast-rebase code and interface is left would agree with that point
  of view. On the other hand, fast-rebase can also be seen as a first
  iteration towards `git replay`. So it can also make sense to see how
  `git replay` evolved from it.

# Changes between v4 and v5

Thanks to Dscho, Linus Arver and Dragan Simic for their suggestions on
the previous version! The few changes compared to v4 are:

* The patch series was rebased onto master at 3a06386e31 (The
  fifteenth batch, 2023-10-04). This is to fix possible conflicts
  especially related to header changes.

* Patch 12/15 (replay: disallow revision specific options and
  pathspecs) in version 4 has been removed, so there are now only 14
  patches instead of 15 in the series. This follows a suggestion by
  Dscho, and goes in the direction Elijah initially wanted before
  Derrick Stolee argued for disallowing revision specific options and
  pathspecs.

* In patch 2/14 (replay: introduce new builtin) the command is now in
  the "plumbingmanipulators" category instead of the "mainporcelain"
  category. This is more in line with the goal of introducing it as a
  plumbing command for now. Thanks to a suggestion from Dscho.

* In patch 6/14 (replay: change rev walking options) the commit
  message has been improved, including its subject, to better match
  and explain what the patch does. Also instead of forcing reverse
  order we use the reverse order by default but allow it to be changed
  using `--reverse`. Thanks to Dscho.

* In patch 11/14 (replay: use standard revision ranges) the commit
  message has been improved to discuss a bit the new options that are
  now accepted and eaten by setup_revisions().

* In patch 11/14 too, the documentation has been improved thanks to
  suggestions by Linus Arver and Dragan Simic. It also now includes
  `rev-list-options.txt` to document the new rev walking options
  accepted by setup_revisions().

All CI tests seem to pass according to:

https://github.com/chriscool/git/actions/runs/6468200080

# Range-diff between v4 and v5

 1:  1eaca9b788 =  1:  72c34a0eba t6429: remove switching aspects of fast-rebase
 2:  5ac4beb1ae !  2:  f85e6c823c replay: introduce new builtin
    @@ Commit message
         Subsequent commits will flesh out its capabilities and make it a more
         standard regular builtin.
     
    +    Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
         Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
         Signed-off-by: Elijah Newren <newren@gmail.com>
         Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
    @@ command-list.txt: git-reflog                              ancillarymanipulators
      git-remote                              ancillarymanipulators           complete
      git-repack                              ancillarymanipulators           complete
      git-replace                             ancillarymanipulators           complete
    -+git-replay                              mainporcelain           history
    ++git-replay                              plumbingmanipulators
      git-request-pull                        foreignscminterface             complete
      git-rerere                              ancillaryinterrogators
      git-reset                               mainporcelain           history
 3:  299381aa9b =  3:  11abb9d120 replay: start using parse_options API
 4:  3b825c9be0 =  4:  9e568eae84 replay: die() instead of failing assert()
 5:  b1e890745d =  5:  e7ebf3c5ef replay: introduce pick_regular_commit()
 6:  ec51351889 !  6:  37d545d5d6 replay: don't simplify history
    @@ Metadata
     Author: Elijah Newren <newren@gmail.com>
     
      ## Commit message ##
    -    replay: don't simplify history
    +    replay: change rev walking options
     
         Let's set the rev walking options we need after calling
    -    setup_revisions() instead of before. This makes it clearer which options
    -    we need.
    +    setup_revisions() instead of before. This enforces options we always
    +    want.
    +
    +    We want the command to work from older commits to newer ones by default,
    +    but we are Ok with letting users reverse that, using --reverse, if that's
    +    what they really want.
     
         Also we don't want history simplification, as we want to deal with all
         the commits in the affected range.
     
    +    Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
         Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
         Signed-off-by: Elijah Newren <newren@gmail.com>
         Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
        }
      
     +  /* requirements/overrides for revs */
    -+  revs.reverse = 1;
    ++  revs.reverse = !revs.reverse;
     +  revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
     +  revs.topo_order = 1;
     +  revs.simplify_history = 0;
 7:  cd4ed07d2d =  7:  2943f08926 replay: add an important FIXME comment about gpg signing
 8:  e45a55917c =  8:  f81962ba41 replay: remove progress and info output
 9:  0587a76cbb =  9:  236747497e replay: remove HEAD related sanity check
10:  d10368e87a = 10:  3374d5be23 replay: make it a minimal server side command
11:  4e09572c43 ! 11:  197d076a93 replay: use standard revision ranges
    @@ Commit message
         way as many other Git commands. This makes its interface more
         standard and more flexible.
     
    +    This also enables many revision related options accepted and
    +    eaten by setup_revisions(). If the replay command was a high level
    +    one or had a high level mode, it would make sense to restrict some
    +    of the possible options, like those generating non-contiguous
    +    history, as they could be confusing for most users.
    +
         Also as the interface of the command is now mostly finalized,
         we can add some documentation as well as testcases to make sure
         the command will continue to work as designed in the future.
     
    +    We only document the rev-list related options among all the
    +    revision related options that are now accepted, as the rev-list
    +    related ones are probably the most useful for now.
    +
    +    Helped-by: Dragan Simic <dsimic@manjaro.org>
    +    Helped-by: Linus Arver <linusa@google.com>
         Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
         Signed-off-by: Elijah Newren <newren@gmail.com>
         Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
    @@ Documentation/git-replay.txt (new)
     +
     +NAME
     +----
    -+git-replay - Replay commits on a different base, without touching working tree
    ++git-replay - Replay commits on a new base, works on bare repos too
     +
     +
     +SYNOPSIS
    @@ Documentation/git-replay.txt (new)
     +DESCRIPTION
     +-----------
     +
    -+Takes a range of commits, and replays them onto a new location.  Does
    -+not touch the working tree or index, and does not update any
    -+references.  However, the output of this command is meant to be used
    -+as input to `git update-ref --stdin`, which would update the relevant
    -+branches.
    ++Takes a range of commits and replays them onto a new location. Leaves
    ++the working tree and the index untouched, and updates no
    ++references. The output of this command is meant to be used as input to
    ++`git update-ref --stdin`, which would update the relevant branches
    ++(see the OUTPUT section below).
     +
     +THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
     +
    @@ Documentation/git-replay.txt (new)
     +
     +<revision-range>::
     +  Range of commits to replay; see "Specifying Ranges" in
    -+  linkgit:git-rev-parse.
    ++  linkgit:git-rev-parse and the "Commit Limiting" options below.
    ++
    ++include::rev-list-options.txt[]
     +
     +OUTPUT
     +------
     +
     +When there are no conflicts, the output of this command is usable as
    -+input to `git update-ref --stdin`.  It is basically of the form:
    ++input to `git update-ref --stdin`.  It is of the form:
     +
     +  update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
     +  update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
    @@ Documentation/git-replay.txt (new)
     +EXAMPLES
     +--------
     +
    -+To simply rebase mybranch onto target:
    ++To simply rebase `mybranch` onto `target`:
     +
     +------------
     +$ git replay --onto target origin/main..mybranch
    @@ Documentation/git-replay.txt (new)
     +update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
     +------------
     +
    -+This will simultaneously rebase branch1, branch2, and branch3 -- all
    -+commits they have since base, playing them on top of origin/main.
    -+These three branches may have commits on top of base that they have in
    -+common, but that does not need to be the case.
    ++This will simultaneously rebase `branch1`, `branch2`, and `branch3`,
    ++all commits they have since `base`, playing them on top of
    ++`origin/main`. These three branches may have commits on top of `base`
    ++that they have in common, but that does not need to be the case.
     +
     +GIT
     +---
12:  64b803f1cf <  -:  ---------- replay: disallow revision specific options and pathspecs
13:  04f27d81ab ! 12:  e52d8b961c replay: add --advance or 'cherry-pick' mode
    @@ Commit message
         Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
     
      ## Documentation/git-replay.txt ##
    -@@ Documentation/git-replay.txt: git-replay - Replay commits on a different base, without touching working tree
    +@@ Documentation/git-replay.txt: git-replay - Replay commits on a new base, works on bare repos too
      SYNOPSIS
      --------
      [verse]
    @@ Documentation/git-replay.txt: OPTIONS
      
      <revision-range>::
     -  Range of commits to replay; see "Specifying Ranges" in
    --  linkgit:git-rev-parse.
    +-  linkgit:git-rev-parse and the "Commit Limiting" options below.
     +  Range of commits to replay. More than one <revision-range> can
     +  be passed, but in `--advance <branch>` mode, they should have
     +  a single tip, so that it's clear where <branch> should point
    -+  to. See "Specifying Ranges" in linkgit:git-rev-parse.
    ++  to. See "Specifying Ranges" in linkgit:git-rev-parse and the
    ++  "Commit Limiting" options below.
      
    - OUTPUT
    - ------
    -@@ Documentation/git-replay.txt: input to `git update-ref --stdin`.  It is basically of the form:
    + include::rev-list-options.txt[]
    + 
    +@@ Documentation/git-replay.txt: input to `git update-ref --stdin`.  It is of the form:
        update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
      
      where the number of refs updated depends on the arguments passed and
    @@ builtin/replay.c: static struct commit *pick_regular_commit(struct commit *pickm
        struct merge_options merge_opt;
        struct merge_result result;
     +  struct strset *update_refs = NULL;
    -   int ret = 0, i;
    +   int ret = 0;
      
        const char * const replay_usage[] = {
     -          N_("git replay --onto <newbase> <revision-range>..."),
    @@ builtin/replay.c: static struct commit *pick_regular_commit(struct commit *pickm
                           N_("revision"),
                           N_("replay onto given commit")),
     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix)
    -                   usage_with_options(replay_usage, replay_options);
    -           }
    +   argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
    +                        PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
      
     -  if (!onto_name) {
     -          error(_("option --onto is mandatory"));
    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
        /* Return */
     
      ## t/t3650-replay-basics.sh ##
    -@@ t/t3650-replay-basics.sh: test_expect_success 'using replay on bare repo with disallowed pathspec' '
    -   test_must_fail git -C bare replay --onto main topic1..topic2 -- A.t
    +@@ t/t3650-replay-basics.sh: test_expect_success 'using replay on bare repo to rebase with a conflict' '
    +   test_expect_code 1 git -C bare replay --onto topic1 B..conflict
      '
      
     +test_expect_success 'using replay to perform basic cherry-pick' '
14:  9ed0919ad5 ! 13:  fc79a930b5 replay: add --contained to rebase contained branches
    @@ Commit message
         Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
     
      ## Documentation/git-replay.txt ##
    -@@ Documentation/git-replay.txt: git-replay - Replay commits on a different base, without touching working tree
    +@@ Documentation/git-replay.txt: git-replay - Replay commits on a new base, works on bare repos too
      SYNOPSIS
      --------
      [verse]
    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
        struct rev_info revs;
        struct commit *last_commit = NULL;
     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix)
    -   int ret = 0, i;
    +   int ret = 0;
      
        const char * const replay_usage[] = {
     -          N_("git replay (--onto <newbase> | --advance <branch>) <revision-range>..."),
15:  cf8c984877 ! 14:  1160ff54e6 replay: stop assuming replayed branches do not diverge
    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
        struct merge_result result;
        struct strset *update_refs = NULL;
     +  kh_oid_map_t *replayed_commits;
    -   int ret = 0, i;
    +   int ret = 0;
      
        const char * const replay_usage[] = {
     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix)


Elijah Newren (14):
  t6429: remove switching aspects of fast-rebase
  replay: introduce new builtin
  replay: start using parse_options API
  replay: die() instead of failing assert()
  replay: introduce pick_regular_commit()
  replay: change rev walking options
  replay: add an important FIXME comment about gpg signing
  replay: remove progress and info output
  replay: remove HEAD related sanity check
  replay: make it a minimal server side command
  replay: use standard revision ranges
  replay: add --advance or 'cherry-pick' mode
  replay: add --contained to rebase contained branches
  replay: stop assuming replayed branches do not diverge

 .gitignore                               |   1 +
 Documentation/git-replay.txt             | 127 +++++++
 Makefile                                 |   2 +-
 builtin.h                                |   1 +
 builtin/replay.c                         | 407 +++++++++++++++++++++++
 command-list.txt                         |   1 +
 git.c                                    |   1 +
 t/helper/test-fast-rebase.c              | 241 --------------
 t/helper/test-tool.c                     |   1 -
 t/helper/test-tool.h                     |   1 -
 t/t3650-replay-basics.sh                 | 198 +++++++++++
 t/t6429-merge-sequence-rename-caching.sh |  45 +--
 12 files changed, 762 insertions(+), 264 deletions(-)
 create mode 100644 Documentation/git-replay.txt
 create mode 100644 builtin/replay.c
 delete mode 100644 t/helper/test-fast-rebase.c
 create mode 100755 t/t3650-replay-basics.sh

-- 
2.42.0.339.g663cbc8ab1


^ permalink raw reply	[flat|nested] 208+ messages in thread

* [PATCH v5 01/14] t6429: remove switching aspects of fast-rebase
  2023-10-10 12:38       ` [PATCH v5 00/14] " Christian Couder
@ 2023-10-10 12:38         ` Christian Couder
  2023-10-10 12:38         ` [PATCH v5 02/14] replay: introduce new builtin Christian Couder
                           ` (15 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-10-10 12:38 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

At the time t6429 was written, merge-ort was still under development,
did not have quite as many tests, and certainly was not widely deployed.
Since t6429 was exercising some codepaths just a little differently, we
thought having them also test the "merge_switch_to_result()" bits of
merge-ort was useful even though they weren't intrinsic to the real
point of these tests.

However, the value provided by doing extra testing of the
"merge_switch_to_result()" bits has decreased a bit over time, and it's
actively making it harder to refactor `test-tool fast-rebase` into `git
replay`, which we are going to do in following commits.  Dispense with
these bits.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 t/helper/test-fast-rebase.c              | 9 +--------
 t/t6429-merge-sequence-rename-caching.sh | 9 +++++++--
 2 files changed, 8 insertions(+), 10 deletions(-)

diff --git a/t/helper/test-fast-rebase.c b/t/helper/test-fast-rebase.c
index cac20a72b3..2bfab66b1b 100644
--- a/t/helper/test-fast-rebase.c
+++ b/t/helper/test-fast-rebase.c
@@ -194,7 +194,7 @@ int cmd__fast_rebase(int argc, const char **argv)
 		last_commit = create_commit(result.tree, commit, last_commit);
 	}
 
-	merge_switch_to_result(&merge_opt, head_tree, &result, 1, !result.clean);
+	merge_finalize(&merge_opt, &result);
 
 	if (result.clean < 0)
 		exit(128);
@@ -213,9 +213,6 @@ int cmd__fast_rebase(int argc, const char **argv)
 		}
 		if (create_symref("HEAD", branch_name.buf, reflog_msg.buf) < 0)
 			die(_("unable to update HEAD"));
-
-		prime_cache_tree(the_repository, the_repository->index,
-				 result.tree);
 	} else {
 		fprintf(stderr, "\nAborting: Hit a conflict.\n");
 		strbuf_addf(&reflog_msg, "rebase progress up to %s",
@@ -228,10 +225,6 @@ int cmd__fast_rebase(int argc, const char **argv)
 			die("Failed to update %s", argv[4]);
 		}
 	}
-	if (write_locked_index(&the_index, &lock,
-			       COMMIT_LOCK | SKIP_IF_UNCHANGED))
-		die(_("unable to write %s"), get_index_file());
-
 	ret = (result.clean == 0);
 cleanup:
 	strbuf_release(&reflog_msg);
diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh
index d02fa16614..75d3fd2dba 100755
--- a/t/t6429-merge-sequence-rename-caching.sh
+++ b/t/t6429-merge-sequence-rename-caching.sh
@@ -72,6 +72,7 @@ test_expect_success 'caching renames does not preclude finding new ones' '
 		git switch upstream &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream~1..topic
 
 		git ls-files >tracked-files &&
@@ -200,6 +201,7 @@ test_expect_success 'rename same file identically, then reintroduce it' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream~1..topic &&
 
 		git ls-files >tracked &&
@@ -277,6 +279,7 @@ test_expect_success 'rename same file identically, then add file to old dir' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream~1..topic &&
 
 		git ls-files >tracked &&
@@ -356,8 +359,6 @@ test_expect_success 'cached dir rename does not prevent noticing later conflict'
 		test_must_fail test-tool fast-rebase --onto HEAD upstream~1 topic >output &&
 		#git cherry-pick upstream..topic &&
 
-		grep CONFLICT..rename/rename output &&
-
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls
 	)
@@ -456,6 +457,7 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
@@ -522,6 +524,7 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
@@ -624,6 +627,7 @@ test_expect_success 'caching renames only on upstream side, part 1' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
@@ -682,6 +686,7 @@ test_expect_success 'caching renames only on upstream side, part 2' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
-- 
2.42.0.339.g663cbc8ab1


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v5 02/14] replay: introduce new builtin
  2023-10-10 12:38       ` [PATCH v5 00/14] " Christian Couder
  2023-10-10 12:38         ` [PATCH v5 01/14] t6429: remove switching aspects of fast-rebase Christian Couder
@ 2023-10-10 12:38         ` Christian Couder
  2023-10-10 12:38         ` [PATCH v5 03/14] replay: start using parse_options API Christian Couder
                           ` (14 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-10-10 12:38 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

For now, this is just a rename from `t/helper/test-fast-rebase.c` into
`builtin/replay.c` with minimal changes to make it build appropriately.

Subsequent commits will flesh out its capabilities and make it a more
standard regular builtin.

Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 .gitignore                                    |  1 +
 Makefile                                      |  2 +-
 builtin.h                                     |  1 +
 .../test-fast-rebase.c => builtin/replay.c    | 27 ++++++-------------
 command-list.txt                              |  1 +
 git.c                                         |  1 +
 t/helper/test-tool.c                          |  1 -
 t/helper/test-tool.h                          |  1 -
 t/t6429-merge-sequence-rename-caching.sh      | 27 +++++++------------
 9 files changed, 22 insertions(+), 40 deletions(-)
 rename t/helper/test-fast-rebase.c => builtin/replay.c (89%)

diff --git a/.gitignore b/.gitignore
index 5e56e471b3..612c0f6a0f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -135,6 +135,7 @@
 /git-remote-ext
 /git-repack
 /git-replace
+/git-replay
 /git-request-pull
 /git-rerere
 /git-reset
diff --git a/Makefile b/Makefile
index 003e63b792..b5e3843d7f 100644
--- a/Makefile
+++ b/Makefile
@@ -799,7 +799,6 @@ TEST_BUILTINS_OBJS += test-dump-split-index.o
 TEST_BUILTINS_OBJS += test-dump-untracked-cache.o
 TEST_BUILTINS_OBJS += test-env-helper.o
 TEST_BUILTINS_OBJS += test-example-decorate.o
-TEST_BUILTINS_OBJS += test-fast-rebase.o
 TEST_BUILTINS_OBJS += test-fsmonitor-client.o
 TEST_BUILTINS_OBJS += test-genrandom.o
 TEST_BUILTINS_OBJS += test-genzeros.o
@@ -1286,6 +1285,7 @@ BUILTIN_OBJS += builtin/remote-fd.o
 BUILTIN_OBJS += builtin/remote.o
 BUILTIN_OBJS += builtin/repack.o
 BUILTIN_OBJS += builtin/replace.o
+BUILTIN_OBJS += builtin/replay.o
 BUILTIN_OBJS += builtin/rerere.o
 BUILTIN_OBJS += builtin/reset.o
 BUILTIN_OBJS += builtin/rev-list.o
diff --git a/builtin.h b/builtin.h
index d560baa661..28280636da 100644
--- a/builtin.h
+++ b/builtin.h
@@ -211,6 +211,7 @@ int cmd_remote(int argc, const char **argv, const char *prefix);
 int cmd_remote_ext(int argc, const char **argv, const char *prefix);
 int cmd_remote_fd(int argc, const char **argv, const char *prefix);
 int cmd_repack(int argc, const char **argv, const char *prefix);
+int cmd_replay(int argc, const char **argv, const char *prefix);
 int cmd_rerere(int argc, const char **argv, const char *prefix);
 int cmd_reset(int argc, const char **argv, const char *prefix);
 int cmd_restore(int argc, const char **argv, const char *prefix);
diff --git a/t/helper/test-fast-rebase.c b/builtin/replay.c
similarity index 89%
rename from t/helper/test-fast-rebase.c
rename to builtin/replay.c
index 2bfab66b1b..e102749ab6 100644
--- a/t/helper/test-fast-rebase.c
+++ b/builtin/replay.c
@@ -1,17 +1,11 @@
 /*
- * "git fast-rebase" builtin command
- *
- * FAST: Forking Any Subprocesses (is) Taboo
- *
- * This is meant SOLELY as a demo of what is possible.  sequencer.c and
- * rebase.c should be refactored to use the ideas here, rather than attempting
- * to extend this file to replace those (unless Phillip or Dscho say that
- * refactoring is too hard and we need a clean slate, but I'm guessing that
- * refactoring is the better route).
+ * "git replay" builtin command
  */
 
 #define USE_THE_INDEX_VARIABLE
-#include "test-tool.h"
+#include "git-compat-util.h"
+
+#include "builtin.h"
 #include "cache-tree.h"
 #include "commit.h"
 #include "environment.h"
@@ -27,7 +21,8 @@
 #include "sequencer.h"
 #include "setup.h"
 #include "strvec.h"
-#include "tree.h"
+#include <oidset.h>
+#include <tree.h>
 
 static const char *short_commit_name(struct commit *commit)
 {
@@ -94,7 +89,7 @@ static struct commit *create_commit(struct tree *tree,
 	return (struct commit *)obj;
 }
 
-int cmd__fast_rebase(int argc, const char **argv)
+int cmd_replay(int argc, const char **argv, const char *prefix)
 {
 	struct commit *onto;
 	struct commit *last_commit = NULL, *last_picked_commit = NULL;
@@ -110,12 +105,6 @@ int cmd__fast_rebase(int argc, const char **argv)
 	struct strbuf branch_name = STRBUF_INIT;
 	int ret = 0;
 
-	/*
-	 * test-tool stuff doesn't set up the git directory by default; need to
-	 * do that manually.
-	 */
-	setup_git_directory();
-
 	if (argc == 2 && !strcmp(argv[1], "-h")) {
 		printf("Sorry, I am not a psychiatrist; I can not give you the help you need.  Oh, you meant usage...\n");
 		exit(129);
@@ -136,7 +125,7 @@ int cmd__fast_rebase(int argc, const char **argv)
 	if (repo_read_index(the_repository) < 0)
 		BUG("Could not read index");
 
-	repo_init_revisions(the_repository, &revs, NULL);
+	repo_init_revisions(the_repository, &revs, prefix);
 	revs.verbose_header = 1;
 	revs.max_parents = 1;
 	revs.cherry_mark = 1;
diff --git a/command-list.txt b/command-list.txt
index 54b2a50f5f..c4cd0f352b 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -160,6 +160,7 @@ git-reflog                              ancillarymanipulators           complete
 git-remote                              ancillarymanipulators           complete
 git-repack                              ancillarymanipulators           complete
 git-replace                             ancillarymanipulators           complete
+git-replay                              plumbingmanipulators
 git-request-pull                        foreignscminterface             complete
 git-rerere                              ancillaryinterrogators
 git-reset                               mainporcelain           history
diff --git a/git.c b/git.c
index c67e44dd82..7068a184b0 100644
--- a/git.c
+++ b/git.c
@@ -594,6 +594,7 @@ static struct cmd_struct commands[] = {
 	{ "remote-fd", cmd_remote_fd, NO_PARSEOPT },
 	{ "repack", cmd_repack, RUN_SETUP },
 	{ "replace", cmd_replace, RUN_SETUP },
+	{ "replay", cmd_replay, RUN_SETUP },
 	{ "rerere", cmd_rerere, RUN_SETUP },
 	{ "reset", cmd_reset, RUN_SETUP },
 	{ "restore", cmd_restore, RUN_SETUP | NEED_WORK_TREE },
diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
index 621ac3dd10..3109ba5c7a 100644
--- a/t/helper/test-tool.c
+++ b/t/helper/test-tool.c
@@ -30,7 +30,6 @@ static struct test_cmd cmds[] = {
 	{ "dump-untracked-cache", cmd__dump_untracked_cache },
 	{ "env-helper", cmd__env_helper },
 	{ "example-decorate", cmd__example_decorate },
-	{ "fast-rebase", cmd__fast_rebase },
 	{ "fsmonitor-client", cmd__fsmonitor_client },
 	{ "genrandom", cmd__genrandom },
 	{ "genzeros", cmd__genzeros },
diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h
index a641c3a81d..d3e55c3f4e 100644
--- a/t/helper/test-tool.h
+++ b/t/helper/test-tool.h
@@ -24,7 +24,6 @@ int cmd__dump_untracked_cache(int argc, const char **argv);
 int cmd__dump_reftable(int argc, const char **argv);
 int cmd__env_helper(int argc, const char **argv);
 int cmd__example_decorate(int argc, const char **argv);
-int cmd__fast_rebase(int argc, const char **argv);
 int cmd__fsmonitor_client(int argc, const char **argv);
 int cmd__genrandom(int argc, const char **argv);
 int cmd__genzeros(int argc, const char **argv);
diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh
index 75d3fd2dba..7670b72008 100755
--- a/t/t6429-merge-sequence-rename-caching.sh
+++ b/t/t6429-merge-sequence-rename-caching.sh
@@ -71,9 +71,8 @@ test_expect_success 'caching renames does not preclude finding new ones' '
 
 		git switch upstream &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream~1..topic
 
 		git ls-files >tracked-files &&
 		test_line_count = 2 tracked-files &&
@@ -141,8 +140,7 @@ test_expect_success 'cherry-pick both a commit and its immediate revert' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
-		#git cherry-pick upstream~1..topic &&
+		git replay --onto HEAD upstream~1 topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 1 calls
@@ -200,9 +198,8 @@ test_expect_success 'rename same file identically, then reintroduce it' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream~1..topic &&
 
 		git ls-files >tracked &&
 		test_line_count = 2 tracked &&
@@ -278,9 +275,8 @@ test_expect_success 'rename same file identically, then add file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream~1..topic &&
 
 		git ls-files >tracked &&
 		test_line_count = 4 tracked &&
@@ -356,8 +352,7 @@ test_expect_success 'cached dir rename does not prevent noticing later conflict'
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test_must_fail test-tool fast-rebase --onto HEAD upstream~1 topic >output &&
-		#git cherry-pick upstream..topic &&
+		test_must_fail git replay --onto HEAD upstream~1 topic >output &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls
@@ -456,9 +451,8 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls &&
@@ -523,9 +517,8 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 3 calls &&
@@ -626,9 +619,8 @@ test_expect_success 'caching renames only on upstream side, part 1' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 1 calls &&
@@ -685,9 +677,8 @@ test_expect_success 'caching renames only on upstream side, part 2' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls &&
-- 
2.42.0.339.g663cbc8ab1


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v5 03/14] replay: start using parse_options API
  2023-10-10 12:38       ` [PATCH v5 00/14] " Christian Couder
  2023-10-10 12:38         ` [PATCH v5 01/14] t6429: remove switching aspects of fast-rebase Christian Couder
  2023-10-10 12:38         ` [PATCH v5 02/14] replay: introduce new builtin Christian Couder
@ 2023-10-10 12:38         ` Christian Couder
  2023-10-10 12:38         ` [PATCH v5 04/14] replay: die() instead of failing assert() Christian Couder
                           ` (13 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-10-10 12:38 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

Instead of manually parsing arguments, let's start using the parse_options
API. This way this new builtin will look more standard, and in some
upcoming commits will more easily be able to handle more command line
options.

Note that we plan to later use standard revision ranges instead of
hardcoded "<oldbase> <branch>" arguments. When we will use standard
revision ranges, it will be easier to check if there are no spurious
arguments if we keep ARGV[0], so let's call parse_options() with
PARSE_OPT_KEEP_ARGV0 even if we don't need ARGV[0] right now to avoid
some useless code churn.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 45 ++++++++++++++++++++++++++++++++-------------
 1 file changed, 32 insertions(+), 13 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index e102749ab6..d6dec7c866 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -15,7 +15,7 @@
 #include "lockfile.h"
 #include "merge-ort.h"
 #include "object-name.h"
-#include "read-cache-ll.h"
+#include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
 #include "sequencer.h"
@@ -92,6 +92,7 @@ static struct commit *create_commit(struct tree *tree,
 int cmd_replay(int argc, const char **argv, const char *prefix)
 {
 	struct commit *onto;
+	const char *onto_name = NULL;
 	struct commit *last_commit = NULL, *last_picked_commit = NULL;
 	struct object_id head;
 	struct lock_file lock = LOCK_INIT;
@@ -105,16 +106,32 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct strbuf branch_name = STRBUF_INIT;
 	int ret = 0;
 
-	if (argc == 2 && !strcmp(argv[1], "-h")) {
-		printf("Sorry, I am not a psychiatrist; I can not give you the help you need.  Oh, you meant usage...\n");
-		exit(129);
+	const char * const replay_usage[] = {
+		N_("git replay --onto <newbase> <oldbase> <branch>"),
+		NULL
+	};
+	struct option replay_options[] = {
+		OPT_STRING(0, "onto", &onto_name,
+			   N_("revision"),
+			   N_("replay onto given commit")),
+		OPT_END()
+	};
+
+	argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
+			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
+
+	if (!onto_name) {
+		error(_("option --onto is mandatory"));
+		usage_with_options(replay_usage, replay_options);
 	}
 
-	if (argc != 5 || strcmp(argv[1], "--onto"))
-		die("usage: read the code, figure out how to use it, then do so");
+	if (argc != 3) {
+		error(_("bad number of arguments"));
+		usage_with_options(replay_usage, replay_options);
+	}
 
-	onto = peel_committish(argv[2]);
-	strbuf_addf(&branch_name, "refs/heads/%s", argv[4]);
+	onto = peel_committish(onto_name);
+	strbuf_addf(&branch_name, "refs/heads/%s", argv[2]);
 
 	/* Sanity check */
 	if (repo_get_oid(the_repository, "HEAD", &head))
@@ -126,6 +143,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		BUG("Could not read index");
 
 	repo_init_revisions(the_repository, &revs, prefix);
+
 	revs.verbose_header = 1;
 	revs.max_parents = 1;
 	revs.cherry_mark = 1;
@@ -134,7 +152,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	revs.right_only = 1;
 	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
 	revs.topo_order = 1;
-	strvec_pushl(&rev_walk_args, "", argv[4], "--not", argv[3], NULL);
+
+	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
 
 	if (setup_revisions(rev_walk_args.nr, rev_walk_args.v, &revs, NULL) > 1) {
 		ret = error(_("unhandled options"));
@@ -197,8 +216,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			       &last_commit->object.oid,
 			       &last_picked_commit->object.oid,
 			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
-			error(_("could not update %s"), argv[4]);
-			die("Failed to update %s", argv[4]);
+			error(_("could not update %s"), argv[2]);
+			die("Failed to update %s", argv[2]);
 		}
 		if (create_symref("HEAD", branch_name.buf, reflog_msg.buf) < 0)
 			die(_("unable to update HEAD"));
@@ -210,8 +229,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			       &last_commit->object.oid,
 			       &head,
 			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
-			error(_("could not update %s"), argv[4]);
-			die("Failed to update %s", argv[4]);
+			error(_("could not update %s"), argv[2]);
+			die("Failed to update %s", argv[2]);
 		}
 	}
 	ret = (result.clean == 0);
-- 
2.42.0.339.g663cbc8ab1


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v5 04/14] replay: die() instead of failing assert()
  2023-10-10 12:38       ` [PATCH v5 00/14] " Christian Couder
                           ` (2 preceding siblings ...)
  2023-10-10 12:38         ` [PATCH v5 03/14] replay: start using parse_options API Christian Couder
@ 2023-10-10 12:38         ` Christian Couder
  2023-10-10 12:38         ` [PATCH v5 05/14] replay: introduce pick_regular_commit() Christian Couder
                           ` (12 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-10-10 12:38 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

It's not a good idea for regular Git commands to use an assert() to
check for things that could happen but are not supported.

Let's die() with an explanation of the issue instead.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index d6dec7c866..f3fdbe48c9 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -179,7 +179,12 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 
 		fprintf(stderr, "Rebasing %s...\r",
 			oid_to_hex(&commit->object.oid));
-		assert(commit->parents && !commit->parents->next);
+
+		if (!commit->parents)
+			die(_("replaying down to root commit is not supported yet!"));
+		if (commit->parents->next)
+			die(_("replaying merge commits is not supported yet!"));
+
 		base = commit->parents->item;
 
 		next_tree = repo_get_commit_tree(the_repository, commit);
-- 
2.42.0.339.g663cbc8ab1


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v5 05/14] replay: introduce pick_regular_commit()
  2023-10-10 12:38       ` [PATCH v5 00/14] " Christian Couder
                           ` (3 preceding siblings ...)
  2023-10-10 12:38         ` [PATCH v5 04/14] replay: die() instead of failing assert() Christian Couder
@ 2023-10-10 12:38         ` Christian Couder
  2023-10-10 12:38         ` [PATCH v5 06/14] replay: change rev walking options Christian Couder
                           ` (11 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-10-10 12:38 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

Let's refactor the code to handle a regular commit (a commit that is
neither a root commit nor a merge commit) into a single function instead
of keeping it inside cmd_replay().

This is good for separation of concerns, and this will help further work
in the future to replay merge commits.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 54 ++++++++++++++++++++++++++++++------------------
 1 file changed, 34 insertions(+), 20 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index f3fdbe48c9..c66888679b 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -89,6 +89,35 @@ static struct commit *create_commit(struct tree *tree,
 	return (struct commit *)obj;
 }
 
+static struct commit *pick_regular_commit(struct commit *pickme,
+					  struct commit *last_commit,
+					  struct merge_options *merge_opt,
+					  struct merge_result *result)
+{
+	struct commit *base;
+	struct tree *pickme_tree, *base_tree;
+
+	base = pickme->parents->item;
+
+	pickme_tree = repo_get_commit_tree(the_repository, pickme);
+	base_tree = repo_get_commit_tree(the_repository, base);
+
+	merge_opt->branch2 = short_commit_name(pickme);
+	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
+
+	merge_incore_nonrecursive(merge_opt,
+				  base_tree,
+				  result->tree,
+				  pickme_tree,
+				  result);
+
+	free((char*)merge_opt->ancestor);
+	merge_opt->ancestor = NULL;
+	if (!result->clean)
+		return NULL;
+	return create_commit(result->tree, pickme, last_commit);
+}
+
 int cmd_replay(int argc, const char **argv, const char *prefix)
 {
 	struct commit *onto;
@@ -100,7 +129,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct rev_info revs;
 	struct commit *commit;
 	struct merge_options merge_opt;
-	struct tree *next_tree, *base_tree, *head_tree;
+	struct tree *head_tree;
 	struct merge_result result;
 	struct strbuf reflog_msg = STRBUF_INIT;
 	struct strbuf branch_name = STRBUF_INIT;
@@ -175,7 +204,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	result.tree = head_tree;
 	last_commit = onto;
 	while ((commit = get_revision(&revs))) {
-		struct commit *base;
+		struct commit *pick;
 
 		fprintf(stderr, "Rebasing %s...\r",
 			oid_to_hex(&commit->object.oid));
@@ -185,26 +214,11 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		if (commit->parents->next)
 			die(_("replaying merge commits is not supported yet!"));
 
-		base = commit->parents->item;
-
-		next_tree = repo_get_commit_tree(the_repository, commit);
-		base_tree = repo_get_commit_tree(the_repository, base);
-
-		merge_opt.branch2 = short_commit_name(commit);
-		merge_opt.ancestor = xstrfmt("parent of %s", merge_opt.branch2);
-
-		merge_incore_nonrecursive(&merge_opt,
-					  base_tree,
-					  result.tree,
-					  next_tree,
-					  &result);
-
-		free((char*)merge_opt.ancestor);
-		merge_opt.ancestor = NULL;
-		if (!result.clean)
+		pick = pick_regular_commit(commit, last_commit, &merge_opt, &result);
+		if (!pick)
 			break;
+		last_commit = pick;
 		last_picked_commit = commit;
-		last_commit = create_commit(result.tree, commit, last_commit);
 	}
 
 	merge_finalize(&merge_opt, &result);
-- 
2.42.0.339.g663cbc8ab1


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v5 06/14] replay: change rev walking options
  2023-10-10 12:38       ` [PATCH v5 00/14] " Christian Couder
                           ` (4 preceding siblings ...)
  2023-10-10 12:38         ` [PATCH v5 05/14] replay: introduce pick_regular_commit() Christian Couder
@ 2023-10-10 12:38         ` Christian Couder
  2023-10-10 12:38         ` [PATCH v5 07/14] replay: add an important FIXME comment about gpg signing Christian Couder
                           ` (10 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-10-10 12:38 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

Let's set the rev walking options we need after calling
setup_revisions() instead of before. This enforces options we always
want.

We want the command to work from older commits to newer ones by default,
but we are Ok with letting users reverse that, using --reverse, if that's
what they really want.

Also we don't want history simplification, as we want to deal with all
the commits in the affected range.

Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 15 ++++++---------
 1 file changed, 6 insertions(+), 9 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index c66888679b..5bab89857f 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -173,15 +173,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 
 	repo_init_revisions(the_repository, &revs, prefix);
 
-	revs.verbose_header = 1;
-	revs.max_parents = 1;
-	revs.cherry_mark = 1;
-	revs.limited = 1;
-	revs.reverse = 1;
-	revs.right_only = 1;
-	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
-	revs.topo_order = 1;
-
 	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
 
 	if (setup_revisions(rev_walk_args.nr, rev_walk_args.v, &revs, NULL) > 1) {
@@ -189,6 +180,12 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		goto cleanup;
 	}
 
+	/* requirements/overrides for revs */
+	revs.reverse = !revs.reverse;
+	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
+	revs.topo_order = 1;
+	revs.simplify_history = 0;
+
 	strvec_clear(&rev_walk_args);
 
 	if (prepare_revision_walk(&revs) < 0) {
-- 
2.42.0.339.g663cbc8ab1


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v5 07/14] replay: add an important FIXME comment about gpg signing
  2023-10-10 12:38       ` [PATCH v5 00/14] " Christian Couder
                           ` (5 preceding siblings ...)
  2023-10-10 12:38         ` [PATCH v5 06/14] replay: change rev walking options Christian Couder
@ 2023-10-10 12:38         ` Christian Couder
  2023-10-10 12:38         ` [PATCH v5 08/14] replay: remove progress and info output Christian Couder
                           ` (9 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-10-10 12:38 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

We want to be able to handle signed commits in some way in the future,
but we are not ready to do it now. So for the time being let's just add
a FIXME comment to remind us about it.

These are different ways we could handle them:

  - in case of a cli user and if there was an interactive mode, we could
    perhaps ask if the user wants to sign again
  - we could add an option to just fail if there are signed commits

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 5bab89857f..dbadf3b9b4 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -62,7 +62,7 @@ static struct commit *create_commit(struct tree *tree,
 	struct object *obj;
 	struct commit_list *parents = NULL;
 	char *author;
-	char *sign_commit = NULL;
+	char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
 	struct commit_extra_header *extra;
 	struct strbuf msg = STRBUF_INIT;
 	const char *out_enc = get_commit_output_encoding();
-- 
2.42.0.339.g663cbc8ab1


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v5 08/14] replay: remove progress and info output
  2023-10-10 12:38       ` [PATCH v5 00/14] " Christian Couder
                           ` (6 preceding siblings ...)
  2023-10-10 12:38         ` [PATCH v5 07/14] replay: add an important FIXME comment about gpg signing Christian Couder
@ 2023-10-10 12:38         ` Christian Couder
  2023-10-10 12:38         ` [PATCH v5 09/14] replay: remove HEAD related sanity check Christian Couder
                           ` (8 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-10-10 12:38 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

The replay command will be changed in a follow up commit, so that it
will not update refs directly, but instead it will print on stdout a
list of commands that can be consumed by `git update-ref --stdin`.

We don't want this output to be polluted by its current low value
output, so let's just remove the latter.

In the future, when the command gets an option to update refs by
itself, it will make a lot of sense to display a progress meter, but
we are not there yet.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 7 +------
 1 file changed, 1 insertion(+), 6 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index dbadf3b9b4..08a7f68420 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -195,7 +195,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
-	merge_opt.show_rename_progress = 1;
+	merge_opt.show_rename_progress = 0;
 	merge_opt.branch1 = "HEAD";
 	head_tree = repo_get_commit_tree(the_repository, onto);
 	result.tree = head_tree;
@@ -203,9 +203,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	while ((commit = get_revision(&revs))) {
 		struct commit *pick;
 
-		fprintf(stderr, "Rebasing %s...\r",
-			oid_to_hex(&commit->object.oid));
-
 		if (!commit->parents)
 			die(_("replaying down to root commit is not supported yet!"));
 		if (commit->parents->next)
@@ -224,7 +221,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		exit(128);
 
 	if (result.clean) {
-		fprintf(stderr, "\nDone.\n");
 		strbuf_addf(&reflog_msg, "finish rebase %s onto %s",
 			    oid_to_hex(&last_picked_commit->object.oid),
 			    oid_to_hex(&last_commit->object.oid));
@@ -238,7 +234,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		if (create_symref("HEAD", branch_name.buf, reflog_msg.buf) < 0)
 			die(_("unable to update HEAD"));
 	} else {
-		fprintf(stderr, "\nAborting: Hit a conflict.\n");
 		strbuf_addf(&reflog_msg, "rebase progress up to %s",
 			    oid_to_hex(&last_picked_commit->object.oid));
 		if (update_ref(reflog_msg.buf, "HEAD",
-- 
2.42.0.339.g663cbc8ab1


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v5 09/14] replay: remove HEAD related sanity check
  2023-10-10 12:38       ` [PATCH v5 00/14] " Christian Couder
                           ` (7 preceding siblings ...)
  2023-10-10 12:38         ` [PATCH v5 08/14] replay: remove progress and info output Christian Couder
@ 2023-10-10 12:38         ` Christian Couder
  2023-10-10 12:38         ` [PATCH v5 10/14] replay: make it a minimal server side command Christian Couder
                           ` (7 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-10-10 12:38 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

We want replay to be a command that can be used on the server side on
any branch, not just the current one, so we are going to stop updating
HEAD in a future commit.

A "sanity check" that makes sure we are replaying the current branch
doesn't make sense anymore. Let's remove it.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 8 +-------
 1 file changed, 1 insertion(+), 7 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 08a7f68420..87fe655c6f 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -123,7 +123,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct commit *onto;
 	const char *onto_name = NULL;
 	struct commit *last_commit = NULL, *last_picked_commit = NULL;
-	struct object_id head;
 	struct lock_file lock = LOCK_INIT;
 	struct strvec rev_walk_args = STRVEC_INIT;
 	struct rev_info revs;
@@ -162,11 +161,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	onto = peel_committish(onto_name);
 	strbuf_addf(&branch_name, "refs/heads/%s", argv[2]);
 
-	/* Sanity check */
-	if (repo_get_oid(the_repository, "HEAD", &head))
-		die(_("Cannot read HEAD"));
-	assert(oideq(&onto->object.oid, &head));
-
 	repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
 	if (repo_read_index(the_repository) < 0)
 		BUG("Could not read index");
@@ -238,7 +232,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			    oid_to_hex(&last_picked_commit->object.oid));
 		if (update_ref(reflog_msg.buf, "HEAD",
 			       &last_commit->object.oid,
-			       &head,
+			       &onto->object.oid,
 			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
 			error(_("could not update %s"), argv[2]);
 			die("Failed to update %s", argv[2]);
-- 
2.42.0.339.g663cbc8ab1


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v5 10/14] replay: make it a minimal server side command
  2023-10-10 12:38       ` [PATCH v5 00/14] " Christian Couder
                           ` (8 preceding siblings ...)
  2023-10-10 12:38         ` [PATCH v5 09/14] replay: remove HEAD related sanity check Christian Couder
@ 2023-10-10 12:38         ` Christian Couder
  2023-10-10 12:38         ` [PATCH v5 11/14] replay: use standard revision ranges Christian Couder
                           ` (6 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-10-10 12:38 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

We want this command to be a minimal command that just does server side
picking of commits, displaying the results on stdout for higher level
scripts to consume.

So let's simplify it:
  * remove the worktree and index reading/writing,
  * remove the ref (and reflog) updating,
  * remove the assumptions tying us to HEAD, since (a) this is not a
    rebase and (b) we want to be able to pick commits in a bare repo,
    i.e. to/from branches that are not checked out and not the main
    branch,
  * remove unneeded includes,
  * handle rebasing multiple branches by printing on stdout the update
    ref commands that should be performed.

The output can be piped into `git update-ref --stdin` for the ref
updates to happen.

In the future to make it easier for users to use this command
directly maybe an option can be added to automatically pipe its output
into `git update-ref`.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c                         | 78 ++++++++----------------
 t/t6429-merge-sequence-rename-caching.sh | 39 +++++++-----
 2 files changed, 50 insertions(+), 67 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 87fe655c6f..ace9ac6df0 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -6,11 +6,7 @@
 #include "git-compat-util.h"
 
 #include "builtin.h"
-#include "cache-tree.h"
-#include "commit.h"
 #include "environment.h"
-#include "gettext.h"
-#include "hash.h"
 #include "hex.h"
 #include "lockfile.h"
 #include "merge-ort.h"
@@ -18,8 +14,6 @@
 #include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
-#include "sequencer.h"
-#include "setup.h"
 #include "strvec.h"
 #include <oidset.h>
 #include <tree.h>
@@ -102,6 +96,7 @@ static struct commit *pick_regular_commit(struct commit *pickme,
 	pickme_tree = repo_get_commit_tree(the_repository, pickme);
 	base_tree = repo_get_commit_tree(the_repository, base);
 
+	merge_opt->branch1 = short_commit_name(last_commit);
 	merge_opt->branch2 = short_commit_name(pickme);
 	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
 
@@ -122,15 +117,12 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 {
 	struct commit *onto;
 	const char *onto_name = NULL;
-	struct commit *last_commit = NULL, *last_picked_commit = NULL;
-	struct lock_file lock = LOCK_INIT;
+	struct commit *last_commit = NULL;
 	struct strvec rev_walk_args = STRVEC_INIT;
 	struct rev_info revs;
 	struct commit *commit;
 	struct merge_options merge_opt;
-	struct tree *head_tree;
 	struct merge_result result;
-	struct strbuf reflog_msg = STRBUF_INIT;
 	struct strbuf branch_name = STRBUF_INIT;
 	int ret = 0;
 
@@ -161,10 +153,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	onto = peel_committish(onto_name);
 	strbuf_addf(&branch_name, "refs/heads/%s", argv[2]);
 
-	repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
-	if (repo_read_index(the_repository) < 0)
-		BUG("Could not read index");
-
 	repo_init_revisions(the_repository, &revs, prefix);
 
 	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
@@ -190,58 +178,44 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
 	merge_opt.show_rename_progress = 0;
-	merge_opt.branch1 = "HEAD";
-	head_tree = repo_get_commit_tree(the_repository, onto);
-	result.tree = head_tree;
+	result.tree = repo_get_commit_tree(the_repository, onto);
 	last_commit = onto;
 	while ((commit = get_revision(&revs))) {
-		struct commit *pick;
+		const struct name_decoration *decoration;
 
 		if (!commit->parents)
 			die(_("replaying down to root commit is not supported yet!"));
 		if (commit->parents->next)
 			die(_("replaying merge commits is not supported yet!"));
 
-		pick = pick_regular_commit(commit, last_commit, &merge_opt, &result);
-		if (!pick)
+		last_commit = pick_regular_commit(commit, last_commit, &merge_opt, &result);
+		if (!last_commit)
 			break;
-		last_commit = pick;
-		last_picked_commit = commit;
+
+		decoration = get_name_decoration(&commit->object);
+		if (!decoration)
+			continue;
+
+		while (decoration) {
+			if (decoration->type == DECORATION_REF_LOCAL) {
+				printf("update %s %s %s\n",
+				       decoration->name,
+				       oid_to_hex(&last_commit->object.oid),
+				       oid_to_hex(&commit->object.oid));
+			}
+			decoration = decoration->next;
+		}
 	}
 
 	merge_finalize(&merge_opt, &result);
+	ret = result.clean;
 
-	if (result.clean < 0)
-		exit(128);
-
-	if (result.clean) {
-		strbuf_addf(&reflog_msg, "finish rebase %s onto %s",
-			    oid_to_hex(&last_picked_commit->object.oid),
-			    oid_to_hex(&last_commit->object.oid));
-		if (update_ref(reflog_msg.buf, branch_name.buf,
-			       &last_commit->object.oid,
-			       &last_picked_commit->object.oid,
-			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
-			error(_("could not update %s"), argv[2]);
-			die("Failed to update %s", argv[2]);
-		}
-		if (create_symref("HEAD", branch_name.buf, reflog_msg.buf) < 0)
-			die(_("unable to update HEAD"));
-	} else {
-		strbuf_addf(&reflog_msg, "rebase progress up to %s",
-			    oid_to_hex(&last_picked_commit->object.oid));
-		if (update_ref(reflog_msg.buf, "HEAD",
-			       &last_commit->object.oid,
-			       &onto->object.oid,
-			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
-			error(_("could not update %s"), argv[2]);
-			die("Failed to update %s", argv[2]);
-		}
-	}
-	ret = (result.clean == 0);
 cleanup:
-	strbuf_release(&reflog_msg);
 	strbuf_release(&branch_name);
 	release_revisions(&revs);
-	return ret;
+
+	/* Return */
+	if (ret < 0)
+		exit(128);
+	return ret ? 0 : 1;
 }
diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh
index 7670b72008..099aefeffc 100755
--- a/t/t6429-merge-sequence-rename-caching.sh
+++ b/t/t6429-merge-sequence-rename-caching.sh
@@ -71,8 +71,9 @@ test_expect_success 'caching renames does not preclude finding new ones' '
 
 		git switch upstream &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		git ls-files >tracked-files &&
 		test_line_count = 2 tracked-files &&
@@ -140,7 +141,9 @@ test_expect_success 'cherry-pick both a commit and its immediate revert' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 1 calls
@@ -198,8 +201,9 @@ test_expect_success 'rename same file identically, then reintroduce it' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		git ls-files >tracked &&
 		test_line_count = 2 tracked &&
@@ -275,8 +279,9 @@ test_expect_success 'rename same file identically, then add file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		git ls-files >tracked &&
 		test_line_count = 4 tracked &&
@@ -451,8 +456,9 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls &&
@@ -517,8 +523,9 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 3 calls &&
@@ -619,8 +626,9 @@ test_expect_success 'caching renames only on upstream side, part 1' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 1 calls &&
@@ -677,8 +685,9 @@ test_expect_success 'caching renames only on upstream side, part 2' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls &&
-- 
2.42.0.339.g663cbc8ab1


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v5 11/14] replay: use standard revision ranges
  2023-10-10 12:38       ` [PATCH v5 00/14] " Christian Couder
                           ` (9 preceding siblings ...)
  2023-10-10 12:38         ` [PATCH v5 10/14] replay: make it a minimal server side command Christian Couder
@ 2023-10-10 12:38         ` Christian Couder
  2023-10-19 19:49           ` Linus Arver
  2023-10-10 12:38         ` [PATCH v5 12/14] replay: add --advance or 'cherry-pick' mode Christian Couder
                           ` (5 subsequent siblings)
  16 siblings, 1 reply; 208+ messages in thread
From: Christian Couder @ 2023-10-10 12:38 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

Instead of the fixed "<oldbase> <branch>" arguments, the replay
command now accepts "<revision-range>..." arguments in a similar
way as many other Git commands. This makes its interface more
standard and more flexible.

This also enables many revision related options accepted and
eaten by setup_revisions(). If the replay command was a high level
one or had a high level mode, it would make sense to restrict some
of the possible options, like those generating non-contiguous
history, as they could be confusing for most users.

Also as the interface of the command is now mostly finalized,
we can add some documentation as well as testcases to make sure
the command will continue to work as designed in the future.

We only document the rev-list related options among all the
revision related options that are now accepted, as the rev-list
related ones are probably the most useful for now.

Helped-by: Dragan Simic <dsimic@manjaro.org>
Helped-by: Linus Arver <linusa@google.com>
Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 Documentation/git-replay.txt             | 92 ++++++++++++++++++++++++
 builtin/replay.c                         | 21 ++----
 t/t3650-replay-basics.sh                 | 83 +++++++++++++++++++++
 t/t6429-merge-sequence-rename-caching.sh | 18 ++---
 4 files changed, 188 insertions(+), 26 deletions(-)
 create mode 100644 Documentation/git-replay.txt
 create mode 100755 t/t3650-replay-basics.sh

diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
new file mode 100644
index 0000000000..86be363f9b
--- /dev/null
+++ b/Documentation/git-replay.txt
@@ -0,0 +1,92 @@
+git-replay(1)
+=============
+
+NAME
+----
+git-replay - Replay commits on a new base, works on bare repos too
+
+
+SYNOPSIS
+--------
+[verse]
+'git replay' --onto <newbase> <revision-range>...
+
+DESCRIPTION
+-----------
+
+Takes a range of commits and replays them onto a new location. Leaves
+the working tree and the index untouched, and updates no
+references. The output of this command is meant to be used as input to
+`git update-ref --stdin`, which would update the relevant branches
+(see the OUTPUT section below).
+
+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
+
+OPTIONS
+-------
+
+--onto <newbase>::
+	Starting point at which to create the new commits.  May be any
+	valid commit, and not just an existing branch name.
++
+The update-ref command(s) in the output will update the branch(es) in
+the revision range to point at the new commits, similar to the way how
+`git rebase --update-refs` updates multiple branches in the affected
+range.
+
+<revision-range>::
+	Range of commits to replay; see "Specifying Ranges" in
+	linkgit:git-rev-parse and the "Commit Limiting" options below.
+
+include::rev-list-options.txt[]
+
+OUTPUT
+------
+
+When there are no conflicts, the output of this command is usable as
+input to `git update-ref --stdin`.  It is of the form:
+
+	update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
+	update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
+	update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
+
+where the number of refs updated depends on the arguments passed and
+the shape of the history being replayed.
+
+EXIT STATUS
+-----------
+
+For a successful, non-conflicted replay, the exit status is 0.  When
+the replay has conflicts, the exit status is 1.  If the replay is not
+able to complete (or start) due to some kind of error, the exit status
+is something other than 0 or 1.
+
+EXAMPLES
+--------
+
+To simply rebase `mybranch` onto `target`:
+
+------------
+$ git replay --onto target origin/main..mybranch
+update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
+------------
+
+When calling `git replay`, one does not need to specify a range of
+commits to replay using the syntax `A..B`; any range expression will
+do:
+
+------------
+$ git replay --onto origin/main ^base branch1 branch2 branch3
+update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
+update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
+update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
+------------
+
+This will simultaneously rebase `branch1`, `branch2`, and `branch3`,
+all commits they have since `base`, playing them on top of
+`origin/main`. These three branches may have commits on top of `base`
+that they have in common, but that does not need to be the case.
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/builtin/replay.c b/builtin/replay.c
index ace9ac6df0..213ac2764e 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -14,7 +14,6 @@
 #include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
-#include "strvec.h"
 #include <oidset.h>
 #include <tree.h>
 
@@ -118,16 +117,14 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct commit *onto;
 	const char *onto_name = NULL;
 	struct commit *last_commit = NULL;
-	struct strvec rev_walk_args = STRVEC_INIT;
 	struct rev_info revs;
 	struct commit *commit;
 	struct merge_options merge_opt;
 	struct merge_result result;
-	struct strbuf branch_name = STRBUF_INIT;
 	int ret = 0;
 
 	const char * const replay_usage[] = {
-		N_("git replay --onto <newbase> <oldbase> <branch>"),
+		N_("git replay --onto <newbase> <revision-range>..."),
 		NULL
 	};
 	struct option replay_options[] = {
@@ -145,20 +142,13 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		usage_with_options(replay_usage, replay_options);
 	}
 
-	if (argc != 3) {
-		error(_("bad number of arguments"));
-		usage_with_options(replay_usage, replay_options);
-	}
-
 	onto = peel_committish(onto_name);
-	strbuf_addf(&branch_name, "refs/heads/%s", argv[2]);
 
 	repo_init_revisions(the_repository, &revs, prefix);
 
-	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
-
-	if (setup_revisions(rev_walk_args.nr, rev_walk_args.v, &revs, NULL) > 1) {
-		ret = error(_("unhandled options"));
+	argc = setup_revisions(argc, argv, &revs, NULL);
+	if (argc > 1) {
+		ret = error(_("unrecognized argument: %s"), argv[1]);
 		goto cleanup;
 	}
 
@@ -168,8 +158,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	revs.topo_order = 1;
 	revs.simplify_history = 0;
 
-	strvec_clear(&rev_walk_args);
-
 	if (prepare_revision_walk(&revs) < 0) {
 		ret = error(_("error preparing revisions"));
 		goto cleanup;
@@ -211,7 +199,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	ret = result.clean;
 
 cleanup:
-	strbuf_release(&branch_name);
 	release_revisions(&revs);
 
 	/* Return */
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
new file mode 100755
index 0000000000..a1da4f9ef9
--- /dev/null
+++ b/t/t3650-replay-basics.sh
@@ -0,0 +1,83 @@
+#!/bin/sh
+
+test_description='basic git replay tests'
+
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
+. ./test-lib.sh
+
+GIT_AUTHOR_NAME=author@name
+GIT_AUTHOR_EMAIL=bogus@email@address
+export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL
+
+test_expect_success 'setup' '
+	test_commit A &&
+	test_commit B &&
+
+	git switch -c topic1 &&
+	test_commit C &&
+	git switch -c topic2 &&
+	test_commit D &&
+	test_commit E &&
+	git switch topic1 &&
+	test_commit F &&
+	git switch -c topic3 &&
+	test_commit G &&
+	test_commit H &&
+	git switch -c topic4 main &&
+	test_commit I &&
+	test_commit J &&
+
+	git switch -c next main &&
+	test_commit K &&
+	git merge -m "Merge topic1" topic1 &&
+	git merge -m "Merge topic2" topic2 &&
+	git merge -m "Merge topic3" topic3 &&
+	>evil &&
+	git add evil &&
+	git commit --amend &&
+	git merge -m "Merge topic4" topic4 &&
+
+	git switch main &&
+	test_commit L &&
+	test_commit M &&
+
+	git switch -c conflict B &&
+	test_commit C.conflict C.t conflict
+'
+
+test_expect_success 'setup bare' '
+	git clone --bare . bare
+'
+
+test_expect_success 'using replay to rebase two branches, one on top of other' '
+	git replay --onto main topic1..topic2 >result &&
+
+	test_line_count = 1 result &&
+
+	git log --format=%s $(cut -f 3 -d " " result) >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/topic2 " >expect &&
+	printf "%s " $(cut -f 3 -d " " result) >>expect &&
+	git rev-parse topic2 >>expect &&
+
+	test_cmp expect result
+'
+
+test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '
+	git -C bare replay --onto main topic1..topic2 >result-bare &&
+	test_cmp expect result-bare
+'
+
+test_expect_success 'using replay to rebase with a conflict' '
+	test_expect_code 1 git replay --onto topic1 B..conflict
+'
+
+test_expect_success 'using replay on bare repo to rebase with a conflict' '
+	test_expect_code 1 git -C bare replay --onto topic1 B..conflict
+'
+
+test_done
diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh
index 099aefeffc..0f39ed0d08 100755
--- a/t/t6429-merge-sequence-rename-caching.sh
+++ b/t/t6429-merge-sequence-rename-caching.sh
@@ -71,7 +71,7 @@ test_expect_success 'caching renames does not preclude finding new ones' '
 
 		git switch upstream &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -141,7 +141,7 @@ test_expect_success 'cherry-pick both a commit and its immediate revert' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -201,7 +201,7 @@ test_expect_success 'rename same file identically, then reintroduce it' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -279,7 +279,7 @@ test_expect_success 'rename same file identically, then add file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -357,7 +357,7 @@ test_expect_success 'cached dir rename does not prevent noticing later conflict'
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test_must_fail git replay --onto HEAD upstream~1 topic >output &&
+		test_must_fail git replay --onto HEAD upstream~1..topic >output &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls
@@ -456,7 +456,7 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -523,7 +523,7 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -626,7 +626,7 @@ test_expect_success 'caching renames only on upstream side, part 1' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -685,7 +685,7 @@ test_expect_success 'caching renames only on upstream side, part 2' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
-- 
2.42.0.339.g663cbc8ab1


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v5 12/14] replay: add --advance or 'cherry-pick' mode
  2023-10-10 12:38       ` [PATCH v5 00/14] " Christian Couder
                           ` (10 preceding siblings ...)
  2023-10-10 12:38         ` [PATCH v5 11/14] replay: use standard revision ranges Christian Couder
@ 2023-10-10 12:38         ` Christian Couder
  2023-10-10 12:38         ` [PATCH v5 13/14] replay: add --contained to rebase contained branches Christian Couder
                           ` (4 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-10-10 12:38 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

There is already a 'rebase' mode with `--onto`. Let's add an 'advance' or
'cherry-pick' mode with `--advance`. This new mode will make the target
branch advance as we replay commits onto it.

The replayed commits should have a single tip, so that it's clear where
the target branch should be advanced. If they have more than one tip,
this new mode will error out.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 Documentation/git-replay.txt |  41 ++++++--
 builtin/replay.c             | 185 +++++++++++++++++++++++++++++++++--
 t/t3650-replay-basics.sh     |  34 +++++++
 3 files changed, 243 insertions(+), 17 deletions(-)

diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
index 86be363f9b..2c09232c8f 100644
--- a/Documentation/git-replay.txt
+++ b/Documentation/git-replay.txt
@@ -9,7 +9,7 @@ git-replay - Replay commits on a new base, works on bare repos too
 SYNOPSIS
 --------
 [verse]
-'git replay' --onto <newbase> <revision-range>...
+'git replay' (--onto <newbase> | --advance <branch>) <revision-range>...
 
 DESCRIPTION
 -----------
@@ -29,14 +29,25 @@ OPTIONS
 	Starting point at which to create the new commits.  May be any
 	valid commit, and not just an existing branch name.
 +
-The update-ref command(s) in the output will update the branch(es) in
-the revision range to point at the new commits, similar to the way how
-`git rebase --update-refs` updates multiple branches in the affected
-range.
+When `--onto` is specified, the update-ref command(s) in the output will
+update the branch(es) in the revision range to point at the new
+commits, similar to the way how `git rebase --update-refs` updates
+multiple branches in the affected range.
+
+--advance <branch>::
+	Starting point at which to create the new commits; must be a
+	branch name.
++
+When `--advance` is specified, the update-ref command(s) in the output
+will update the branch passed as an argument to `--advance` to point at
+the new commits (in other words, this mimics a cherry-pick operation).
 
 <revision-range>::
-	Range of commits to replay; see "Specifying Ranges" in
-	linkgit:git-rev-parse and the "Commit Limiting" options below.
+	Range of commits to replay. More than one <revision-range> can
+	be passed, but in `--advance <branch>` mode, they should have
+	a single tip, so that it's clear where <branch> should point
+	to. See "Specifying Ranges" in linkgit:git-rev-parse and the
+	"Commit Limiting" options below.
 
 include::rev-list-options.txt[]
 
@@ -51,7 +62,9 @@ input to `git update-ref --stdin`.  It is of the form:
 	update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
 
 where the number of refs updated depends on the arguments passed and
-the shape of the history being replayed.
+the shape of the history being replayed.  When using `--advance`, the
+number of refs updated is always one, but for `--onto`, it can be one
+or more (rebasing multiple branches simultaneously is supported).
 
 EXIT STATUS
 -----------
@@ -71,6 +84,18 @@ $ git replay --onto target origin/main..mybranch
 update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
 ------------
 
+To cherry-pick the commits from mybranch onto target:
+
+------------
+$ git replay --advance target origin/main..mybranch
+update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}
+------------
+
+Note that the first two examples replay the exact same commits and on
+top of the exact same new base, they only differ in that the first
+provides instructions to make mybranch point at the new commits and
+the second provides instructions to make target point at them.
+
 When calling `git replay`, one does not need to specify a range of
 commits to replay using the syntax `A..B`; any range expression will
 do:
diff --git a/builtin/replay.c b/builtin/replay.c
index 213ac2764e..d458837e9c 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -14,6 +14,7 @@
 #include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
+#include "strmap.h"
 #include <oidset.h>
 #include <tree.h>
 
@@ -82,6 +83,146 @@ static struct commit *create_commit(struct tree *tree,
 	return (struct commit *)obj;
 }
 
+struct ref_info {
+	struct commit *onto;
+	struct strset positive_refs;
+	struct strset negative_refs;
+	int positive_refexprs;
+	int negative_refexprs;
+};
+
+static void get_ref_information(struct rev_cmdline_info *cmd_info,
+				struct ref_info *ref_info)
+{
+	int i;
+
+	ref_info->onto = NULL;
+	strset_init(&ref_info->positive_refs);
+	strset_init(&ref_info->negative_refs);
+	ref_info->positive_refexprs = 0;
+	ref_info->negative_refexprs = 0;
+
+	/*
+	 * When the user specifies e.g.
+	 *   git replay origin/main..mybranch
+	 *   git replay ^origin/next mybranch1 mybranch2
+	 * we want to be able to determine where to replay the commits.  In
+	 * these examples, the branches are probably based on an old version
+	 * of either origin/main or origin/next, so we want to replay on the
+	 * newest version of that branch.  In contrast we would want to error
+	 * out if they ran
+	 *   git replay ^origin/master ^origin/next mybranch
+	 *   git replay mybranch~2..mybranch
+	 * the first of those because there's no unique base to choose, and
+	 * the second because they'd likely just be replaying commits on top
+	 * of the same commit and not making any difference.
+	 */
+	for (i = 0; i < cmd_info->nr; i++) {
+		struct rev_cmdline_entry *e = cmd_info->rev + i;
+		struct object_id oid;
+		const char *refexpr = e->name;
+		char *fullname = NULL;
+		int can_uniquely_dwim = 1;
+
+		if (*refexpr == '^')
+			refexpr++;
+		if (repo_dwim_ref(the_repository, refexpr, strlen(refexpr), &oid, &fullname, 0) != 1)
+			can_uniquely_dwim = 0;
+
+		if (e->flags & BOTTOM) {
+			if (can_uniquely_dwim)
+				strset_add(&ref_info->negative_refs, fullname);
+			if (!ref_info->negative_refexprs)
+				ref_info->onto = lookup_commit_reference_gently(the_repository,
+										&e->item->oid, 1);
+			ref_info->negative_refexprs++;
+		} else {
+			if (can_uniquely_dwim)
+				strset_add(&ref_info->positive_refs, fullname);
+			ref_info->positive_refexprs++;
+		}
+
+		free(fullname);
+	}
+}
+
+static void determine_replay_mode(struct rev_cmdline_info *cmd_info,
+				  const char *onto_name,
+				  const char **advance_name,
+				  struct commit **onto,
+				  struct strset **update_refs)
+{
+	struct ref_info rinfo;
+
+	get_ref_information(cmd_info, &rinfo);
+	if (!rinfo.positive_refexprs)
+		die(_("need some commits to replay"));
+	if (onto_name && *advance_name)
+		die(_("--onto and --advance are incompatible"));
+	else if (onto_name) {
+		*onto = peel_committish(onto_name);
+		if (rinfo.positive_refexprs <
+		    strset_get_size(&rinfo.positive_refs))
+			die(_("all positive revisions given must be references"));
+	} else if (*advance_name) {
+		struct object_id oid;
+		char *fullname = NULL;
+
+		*onto = peel_committish(*advance_name);
+		if (repo_dwim_ref(the_repository, *advance_name, strlen(*advance_name),
+			     &oid, &fullname, 0) == 1) {
+			*advance_name = fullname;
+		} else {
+			die(_("argument to --advance must be a reference"));
+		}
+		if (rinfo.positive_refexprs > 1)
+			die(_("cannot advance target with multiple sources because ordering would be ill-defined"));
+	} else {
+		int positive_refs_complete = (
+			rinfo.positive_refexprs ==
+			strset_get_size(&rinfo.positive_refs));
+		int negative_refs_complete = (
+			rinfo.negative_refexprs ==
+			strset_get_size(&rinfo.negative_refs));
+		/*
+		 * We need either positive_refs_complete or
+		 * negative_refs_complete, but not both.
+		 */
+		if (rinfo.negative_refexprs > 0 &&
+		    positive_refs_complete == negative_refs_complete)
+			die(_("cannot implicitly determine whether this is an --advance or --onto operation"));
+		if (negative_refs_complete) {
+			struct hashmap_iter iter;
+			struct strmap_entry *entry;
+
+			if (rinfo.negative_refexprs == 0)
+				die(_("all positive revisions given must be references"));
+			else if (rinfo.negative_refexprs > 1)
+				die(_("cannot implicitly determine whether this is an --advance or --onto operation"));
+			else if (rinfo.positive_refexprs > 1)
+				die(_("cannot advance target with multiple source branches because ordering would be ill-defined"));
+
+			/* Only one entry, but we have to loop to get it */
+			strset_for_each_entry(&rinfo.negative_refs,
+					      &iter, entry) {
+				*advance_name = entry->key;
+			}
+		} else { /* positive_refs_complete */
+			if (rinfo.negative_refexprs > 1)
+				die(_("cannot implicitly determine correct base for --onto"));
+			if (rinfo.negative_refexprs == 1)
+				*onto = rinfo.onto;
+		}
+	}
+	if (!*advance_name) {
+		*update_refs = xcalloc(1, sizeof(**update_refs));
+		**update_refs = rinfo.positive_refs;
+		memset(&rinfo.positive_refs, 0, sizeof(**update_refs));
+	}
+	strset_clear(&rinfo.negative_refs);
+	strset_clear(&rinfo.positive_refs);
+}
+
 static struct commit *pick_regular_commit(struct commit *pickme,
 					  struct commit *last_commit,
 					  struct merge_options *merge_opt,
@@ -114,20 +255,26 @@ static struct commit *pick_regular_commit(struct commit *pickme,
 
 int cmd_replay(int argc, const char **argv, const char *prefix)
 {
-	struct commit *onto;
+	const char *advance_name = NULL;
+	struct commit *onto = NULL;
 	const char *onto_name = NULL;
-	struct commit *last_commit = NULL;
+
 	struct rev_info revs;
+	struct commit *last_commit = NULL;
 	struct commit *commit;
 	struct merge_options merge_opt;
 	struct merge_result result;
+	struct strset *update_refs = NULL;
 	int ret = 0;
 
 	const char * const replay_usage[] = {
-		N_("git replay --onto <newbase> <revision-range>..."),
+		N_("git replay (--onto <newbase> | --advance <branch>) <revision-range>..."),
 		NULL
 	};
 	struct option replay_options[] = {
+		OPT_STRING(0, "advance", &advance_name,
+			   N_("branch"),
+			   N_("make replay advance given branch")),
 		OPT_STRING(0, "onto", &onto_name,
 			   N_("revision"),
 			   N_("replay onto given commit")),
@@ -137,13 +284,11 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
 			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
 
-	if (!onto_name) {
-		error(_("option --onto is mandatory"));
+	if (!onto_name && !advance_name) {
+		error(_("option --onto or --advance is mandatory"));
 		usage_with_options(replay_usage, replay_options);
 	}
 
-	onto = peel_committish(onto_name);
-
 	repo_init_revisions(the_repository, &revs, prefix);
 
 	argc = setup_revisions(argc, argv, &revs, NULL);
@@ -158,6 +303,12 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	revs.topo_order = 1;
 	revs.simplify_history = 0;
 
+	determine_replay_mode(&revs.cmdline, onto_name, &advance_name,
+			      &onto, &update_refs);
+
+	if (!onto) /* FIXME: Should handle replaying down to root commit */
+		die("Replaying down to root commit is not supported yet!");
+
 	if (prepare_revision_walk(&revs) < 0) {
 		ret = error(_("error preparing revisions"));
 		goto cleanup;
@@ -166,6 +317,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
 	merge_opt.show_rename_progress = 0;
+
 	result.tree = repo_get_commit_tree(the_repository, onto);
 	last_commit = onto;
 	while ((commit = get_revision(&revs))) {
@@ -180,12 +332,15 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		if (!last_commit)
 			break;
 
+		/* Update any necessary branches */
+		if (advance_name)
+			continue;
 		decoration = get_name_decoration(&commit->object);
 		if (!decoration)
 			continue;
-
 		while (decoration) {
-			if (decoration->type == DECORATION_REF_LOCAL) {
+			if (decoration->type == DECORATION_REF_LOCAL &&
+			    strset_contains(update_refs, decoration->name)) {
 				printf("update %s %s %s\n",
 				       decoration->name,
 				       oid_to_hex(&last_commit->object.oid),
@@ -195,10 +350,22 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		}
 	}
 
+	/* In --advance mode, advance the target ref */
+	if (result.clean == 1 && advance_name) {
+		printf("update %s %s %s\n",
+		       advance_name,
+		       oid_to_hex(&last_commit->object.oid),
+		       oid_to_hex(&onto->object.oid));
+	}
+
 	merge_finalize(&merge_opt, &result);
 	ret = result.clean;
 
 cleanup:
+	if (update_refs) {
+		strset_clear(update_refs);
+		free(update_refs);
+	}
 	release_revisions(&revs);
 
 	/* Return */
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index a1da4f9ef9..68a87e7803 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -80,4 +80,38 @@ test_expect_success 'using replay on bare repo to rebase with a conflict' '
 	test_expect_code 1 git -C bare replay --onto topic1 B..conflict
 '
 
+test_expect_success 'using replay to perform basic cherry-pick' '
+	# The differences between this test and previous ones are:
+	#   --advance vs --onto
+	# 2nd field of result is refs/heads/main vs. refs/heads/topic2
+	# 4th field of result is hash for main instead of hash for topic2
+
+	git replay --advance main topic1..topic2 >result &&
+
+	test_line_count = 1 result &&
+
+	git log --format=%s $(cut -f 3 -d " " result) >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/main " >expect &&
+	printf "%s " $(cut -f 3 -d " " result) >>expect &&
+	git rev-parse main >>expect &&
+
+	test_cmp expect result
+'
+
+test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
+	git -C bare replay --advance main topic1..topic2 >result-bare &&
+	test_cmp expect result-bare
+'
+
+test_expect_success 'replay on bare repo fails with both --advance and --onto' '
+	test_must_fail git -C bare replay --advance main --onto main topic1..topic2 >result-bare
+'
+
+test_expect_success 'replay fails when both --advance and --onto are omitted' '
+	test_must_fail git replay topic1..topic2 >result
+'
+
 test_done
-- 
2.42.0.339.g663cbc8ab1


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v5 13/14] replay: add --contained to rebase contained branches
  2023-10-10 12:38       ` [PATCH v5 00/14] " Christian Couder
                           ` (11 preceding siblings ...)
  2023-10-10 12:38         ` [PATCH v5 12/14] replay: add --advance or 'cherry-pick' mode Christian Couder
@ 2023-10-10 12:38         ` Christian Couder
  2023-10-10 12:38         ` [PATCH v5 14/14] replay: stop assuming replayed branches do not diverge Christian Couder
                           ` (3 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-10-10 12:38 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

Let's add a `--contained` option that can be used along with
`--onto` to rebase all the branches contained in the <revision-range>
argument.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 Documentation/git-replay.txt | 12 +++++++++++-
 builtin/replay.c             | 12 ++++++++++--
 t/t3650-replay-basics.sh     | 29 +++++++++++++++++++++++++++++
 3 files changed, 50 insertions(+), 3 deletions(-)

diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
index 2c09232c8f..e9697fcfb6 100644
--- a/Documentation/git-replay.txt
+++ b/Documentation/git-replay.txt
@@ -9,7 +9,7 @@ git-replay - Replay commits on a new base, works on bare repos too
 SYNOPSIS
 --------
 [verse]
-'git replay' (--onto <newbase> | --advance <branch>) <revision-range>...
+'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...
 
 DESCRIPTION
 -----------
@@ -96,6 +96,16 @@ top of the exact same new base, they only differ in that the first
 provides instructions to make mybranch point at the new commits and
 the second provides instructions to make target point at them.
 
+What if you have a stack of branches, one depending upon another, and
+you'd really like to rebase the whole set?
+
+------------
+$ git replay --contained --onto origin/main origin/main..tipbranch
+update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
+update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
+update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}
+------------
+
 When calling `git replay`, one does not need to specify a range of
 commits to replay using the syntax `A..B`; any range expression will
 do:
diff --git a/builtin/replay.c b/builtin/replay.c
index d458837e9c..12689f1c89 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -258,6 +258,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	const char *advance_name = NULL;
 	struct commit *onto = NULL;
 	const char *onto_name = NULL;
+	int contained = 0;
 
 	struct rev_info revs;
 	struct commit *last_commit = NULL;
@@ -268,7 +269,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	int ret = 0;
 
 	const char * const replay_usage[] = {
-		N_("git replay (--onto <newbase> | --advance <branch>) <revision-range>..."),
+		N_("git replay ([--contained] --onto <newbase> | --advance <branch>) <revision-range>..."),
 		NULL
 	};
 	struct option replay_options[] = {
@@ -278,6 +279,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		OPT_STRING(0, "onto", &onto_name,
 			   N_("revision"),
 			   N_("replay onto given commit")),
+		OPT_BOOL(0, "contained", &contained,
+			 N_("advance all branches contained in revision-range")),
 		OPT_END()
 	};
 
@@ -289,6 +292,10 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		usage_with_options(replay_usage, replay_options);
 	}
 
+	if (advance_name && contained)
+		die(_("options '%s' and '%s' cannot be used together"),
+		    "--advance", "--contained");
+
 	repo_init_revisions(the_repository, &revs, prefix);
 
 	argc = setup_revisions(argc, argv, &revs, NULL);
@@ -340,7 +347,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			continue;
 		while (decoration) {
 			if (decoration->type == DECORATION_REF_LOCAL &&
-			    strset_contains(update_refs, decoration->name)) {
+			    (contained || strset_contains(update_refs,
+							  decoration->name))) {
 				printf("update %s %s %s\n",
 				       decoration->name,
 				       oid_to_hex(&last_commit->object.oid),
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 68a87e7803..d6286f9580 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -114,4 +114,33 @@ test_expect_success 'replay fails when both --advance and --onto are omitted' '
 	test_must_fail git replay topic1..topic2 >result
 '
 
+test_expect_success 'using replay to also rebase a contained branch' '
+	git replay --contained --onto main main..topic3 >result &&
+
+	test_line_count = 2 result &&
+	cut -f 3 -d " " result >new-branch-tips &&
+
+	git log --format=%s $(head -n 1 new-branch-tips) >actual &&
+	test_write_lines F C M L B A >expect &&
+	test_cmp expect actual &&
+
+	git log --format=%s $(tail -n 1 new-branch-tips) >actual &&
+	test_write_lines H G F C M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/topic1 " >expect &&
+	printf "%s " $(head -n 1 new-branch-tips) >>expect &&
+	git rev-parse topic1 >>expect &&
+	printf "update refs/heads/topic3 " >>expect &&
+	printf "%s " $(tail -n 1 new-branch-tips) >>expect &&
+	git rev-parse topic3 >>expect &&
+
+	test_cmp expect result
+'
+
+test_expect_success 'using replay on bare repo to also rebase a contained branch' '
+	git -C bare replay --contained --onto main main..topic3 >result-bare &&
+	test_cmp expect result-bare
+'
+
 test_done
-- 
2.42.0.339.g663cbc8ab1


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v5 14/14] replay: stop assuming replayed branches do not diverge
  2023-10-10 12:38       ` [PATCH v5 00/14] " Christian Couder
                           ` (12 preceding siblings ...)
  2023-10-10 12:38         ` [PATCH v5 13/14] replay: add --contained to rebase contained branches Christian Couder
@ 2023-10-10 12:38         ` Christian Couder
  2023-10-26 13:44         ` [PATCH v5 00/14] Introduce new `git replay` command Johannes Schindelin
                           ` (2 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-10-10 12:38 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

The replay command is able to replay multiple branches but when some of
them are based on other replayed branches, their commit should be
replayed onto already replayed commits.

For this purpose, let's store the replayed commit and its original
commit in a key value store, so that we can easily find and reuse a
replayed commit instead of the original one.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c         | 44 ++++++++++++++++++++++++++--------
 t/t3650-replay-basics.sh | 52 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 86 insertions(+), 10 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 12689f1c89..f74261b75e 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -223,20 +223,33 @@ static void determine_replay_mode(struct rev_cmdline_info *cmd_info,
 	strset_clear(&rinfo.positive_refs);
 }
 
+static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,
+				    struct commit *commit,
+				    struct commit *fallback)
+{
+	khint_t pos = kh_get_oid_map(replayed_commits, commit->object.oid);
+	if (pos == kh_end(replayed_commits))
+		return fallback;
+	return kh_value(replayed_commits, pos);
+}
+
 static struct commit *pick_regular_commit(struct commit *pickme,
-					  struct commit *last_commit,
+					  kh_oid_map_t *replayed_commits,
+					  struct commit *onto,
 					  struct merge_options *merge_opt,
 					  struct merge_result *result)
 {
-	struct commit *base;
+	struct commit *base, *replayed_base;
 	struct tree *pickme_tree, *base_tree;
 
 	base = pickme->parents->item;
+	replayed_base = mapped_commit(replayed_commits, base, onto);
 
+	result->tree = repo_get_commit_tree(the_repository, replayed_base);
 	pickme_tree = repo_get_commit_tree(the_repository, pickme);
 	base_tree = repo_get_commit_tree(the_repository, base);
 
-	merge_opt->branch1 = short_commit_name(last_commit);
+	merge_opt->branch1 = short_commit_name(replayed_base);
 	merge_opt->branch2 = short_commit_name(pickme);
 	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
 
@@ -250,7 +263,7 @@ static struct commit *pick_regular_commit(struct commit *pickme,
 	merge_opt->ancestor = NULL;
 	if (!result->clean)
 		return NULL;
-	return create_commit(result->tree, pickme, last_commit);
+	return create_commit(result->tree, pickme, replayed_base);
 }
 
 int cmd_replay(int argc, const char **argv, const char *prefix)
@@ -266,6 +279,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct merge_options merge_opt;
 	struct merge_result result;
 	struct strset *update_refs = NULL;
+	kh_oid_map_t *replayed_commits;
 	int ret = 0;
 
 	const char * const replay_usage[] = {
@@ -324,21 +338,30 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
 	merge_opt.show_rename_progress = 0;
-
-	result.tree = repo_get_commit_tree(the_repository, onto);
 	last_commit = onto;
+	replayed_commits = kh_init_oid_map();
 	while ((commit = get_revision(&revs))) {
 		const struct name_decoration *decoration;
+		khint_t pos;
+		int hr;
 
 		if (!commit->parents)
 			die(_("replaying down to root commit is not supported yet!"));
 		if (commit->parents->next)
 			die(_("replaying merge commits is not supported yet!"));
 
-		last_commit = pick_regular_commit(commit, last_commit, &merge_opt, &result);
+		last_commit = pick_regular_commit(commit, replayed_commits, onto,
+						  &merge_opt, &result);
 		if (!last_commit)
 			break;
 
+		/* Record commit -> last_commit mapping */
+		pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr);
+		if (hr == 0)
+			BUG("Duplicate rewritten commit: %s\n",
+			    oid_to_hex(&commit->object.oid));
+		kh_value(replayed_commits, pos) = last_commit;
+
 		/* Update any necessary branches */
 		if (advance_name)
 			continue;
@@ -367,13 +390,14 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	}
 
 	merge_finalize(&merge_opt, &result);
-	ret = result.clean;
-
-cleanup:
+	kh_destroy_oid_map(replayed_commits);
 	if (update_refs) {
 		strset_clear(update_refs);
 		free(update_refs);
 	}
+	ret = result.clean;
+
+cleanup:
 	release_revisions(&revs);
 
 	/* Return */
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index d6286f9580..389670262e 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -143,4 +143,56 @@ test_expect_success 'using replay on bare repo to also rebase a contained branch
 	test_cmp expect result-bare
 '
 
+test_expect_success 'using replay to rebase multiple divergent branches' '
+	git replay --onto main ^topic1 topic2 topic4 >result &&
+
+	test_line_count = 2 result &&
+	cut -f 3 -d " " result >new-branch-tips &&
+
+	git log --format=%s $(head -n 1 new-branch-tips) >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual &&
+
+	git log --format=%s $(tail -n 1 new-branch-tips) >actual &&
+	test_write_lines J I M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/topic2 " >expect &&
+	printf "%s " $(head -n 1 new-branch-tips) >>expect &&
+	git rev-parse topic2 >>expect &&
+	printf "update refs/heads/topic4 " >>expect &&
+	printf "%s " $(tail -n 1 new-branch-tips) >>expect &&
+	git rev-parse topic4 >>expect &&
+
+	test_cmp expect result
+'
+
+test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '
+	git -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&
+
+	test_line_count = 4 result &&
+	cut -f 3 -d " " result >new-branch-tips &&
+
+	>expect &&
+	for i in 2 1 3 4
+	do
+		printf "update refs/heads/topic$i " >>expect &&
+		printf "%s " $(grep topic$i result | cut -f 3 -d " ") >>expect &&
+		git -C bare rev-parse topic$i >>expect || return 1
+	done &&
+
+	test_cmp expect result &&
+
+	test_write_lines F C M L B A >expect1 &&
+	test_write_lines E D C M L B A >expect2 &&
+	test_write_lines H G F C M L B A >expect3 &&
+	test_write_lines J I M L B A >expect4 &&
+
+	for i in 1 2 3 4
+	do
+		git -C bare log --format=%s $(grep topic$i result | cut -f 3 -d " ") >actual &&
+		test_cmp expect$i actual || return 1
+	done
+'
+
 test_done
-- 
2.42.0.339.g663cbc8ab1


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* Re: [PATCH v4 02/15] replay: introduce new builtin
  2023-09-07 10:23         ` Johannes Schindelin
@ 2023-10-10 12:42           ` Christian Couder
  0 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-10-10 12:42 UTC (permalink / raw)
  To: Johannes Schindelin
  Cc: git, Junio C Hamano, Patrick Steinhardt, Elijah Newren, John Cai,
	Derrick Stolee, Phillip Wood, Calvin Wan, Toon Claes,
	Christian Couder

Hi Dscho,

On Thu, Sep 7, 2023 at 1:01 PM Johannes Schindelin
<Johannes.Schindelin@gmx.de> wrote:
>
> Hi Christian & Elijah,
>
> On Thu, 7 Sep 2023, Christian Couder wrote:
>
> > diff --git a/command-list.txt b/command-list.txt
> > index 54b2a50f5f..d74836ab21 100644
> > --- a/command-list.txt
> > +++ b/command-list.txt
> > @@ -160,6 +160,7 @@ git-reflog                              ancillarymanipulators           complete
> >  git-remote                              ancillarymanipulators           complete
> >  git-repack                              ancillarymanipulators           complete
> >  git-replace                             ancillarymanipulators           complete
> > +git-replay                              mainporcelain           history
>
> I recall this having come up before, but in light of the still active
> discussion revolving around command-line parameter validation, I would
> strongly advise removing `git replay` from the main page and instead go
> for plumbingmanipulators.

Ok, I have done that in the version 5 I just sent.

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v4 06/15] replay: don't simplify history
  2023-09-07 10:23         ` Johannes Schindelin
@ 2023-10-10 12:43           ` Christian Couder
  0 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-10-10 12:43 UTC (permalink / raw)
  To: Johannes Schindelin
  Cc: git, Junio C Hamano, Patrick Steinhardt, Elijah Newren, John Cai,
	Derrick Stolee, Phillip Wood, Calvin Wan, Toon Claes,
	Christian Couder

Hi Dscho,

On Thu, Sep 7, 2023 at 1:02 PM Johannes Schindelin
<Johannes.Schindelin@gmx.de> wrote:
>
> Hi Christian & Elijah,
>
> On Thu, 7 Sep 2023, Christian Couder wrote:
>
> > From: Elijah Newren <newren@gmail.com>
> >
> > Let's set the rev walking options we need after calling
> > setup_revisions() instead of before. This makes it clearer which options
> > we need.
>
> In light of the currently open issue about command-line validation, this
> change does more than this paragraph lets on: It hardcodes certain
> settings, overriding (silently) any rev-list options the user might have
> passed.
>
> Is there any chance that we can avoid this change?

In the version 5 I just sent, I have changed the commit message and the code.

The commit messages says:

"
   replay: change rev walking options

   Let's set the rev walking options we need after calling
   setup_revisions() instead of before. This enforces options we always
   want.

   We want the command to work from older commits to newer ones by default,
   but we are Ok with letting users reverse that, using --reverse, if that's
   what they really want.

   Also we don't want history simplification, as we want to deal with all
   the commits in the affected range.
"

So about "revs.reverse" the code is now:

  revs.reverse = !revs.reverse;

which seems to be what you suggested elsewhere.

Apart from that I think it's fair to enforce some values for a few
options. This way we can make the command work the way we want by
default, get consistent behavior and avoid users shooting themselves
in the foot for now. If more flexibility is needed and useful in the
future, then we might allow it in future patches with proper
justification, tests and docs. There is still a lot of flexibility
left especially as the patch that disallowed some rev walking options
and pathspecs has been removed as you suggested.

> > Also we don't want history simplification, as we want to deal with all
> > the commits in the affected range.
>
> This, however, is a good change. It deserves to live in its own commit,
> with its own commit message, in particular because it is not obvious from
> the attribute names which ones we're talking about (I guess it's `limited`
> and `simplify_history`, not just the latter.

We only enforce "revs.sort_order", "revs.topo_order" and yeah
"revs.simplify_history".

Also I don't think it makes much sense to separate these changes in
different commits/patches. I am Ok to improve the commit message if
you think it's worth it, but the patch series is designed to make it
easy to review the changes from the "fast-rebase" test-tool to a good
"replay" plumbing command, and I don't think separating those related
changes would improve things much.

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v3 11/15] replay: use standard revision ranges
  2023-09-07 21:02           ` Dragan Simic
@ 2023-10-10 12:44             ` Christian Couder
  2023-10-10 14:02               ` Dragan Simic
  0 siblings, 1 reply; 208+ messages in thread
From: Christian Couder @ 2023-10-10 12:44 UTC (permalink / raw)
  To: Dragan Simic
  Cc: Toon Claes, git, Junio C Hamano, Patrick Steinhardt,
	Johannes Schindelin, Elijah Newren, John Cai, Derrick Stolee,
	Phillip Wood, Felipe Contreras, Calvin Wan, Christian Couder

On Thu, Sep 7, 2023 at 11:02 PM Dragan Simic <dsimic@manjaro.org> wrote:
>
> On 2023-09-07 10:32, Christian Couder wrote:
> > On Thu, Jun 22, 2023 at 12:03 PM Toon Claes <toon@iotcl.com> wrote:
> >>
> >> Christian Couder <christian.couder@gmail.com> writes:
> >>
> >> > +DESCRIPTION
> >> > +-----------
> >> > +
> >> > +Takes a range of commits, and replays them onto a new location.  Does
> >> > +not touch the working tree or index, and does not update any
> >> > +references.  However, the output of this command is meant to be used
> >>
> >> Small suggestion here:
> >>
> >> Takes a range of commits, and replays them onto a new location.  Does
> >> neither touch the working tree nor index, and does not update any
> >> references.
> >
> > I am not a native speaker, so I am not sure what's best here. I find
> > your suggestion a bit less clear though, so until a native speaker
> > agrees with it or maybe finds something even better, I prefer to leave
> > it as-is.
>
> I'm also not a native English speaker, but I spent about 2.5 years
> contributing a whole lot to English Wikipedia, so I'd dare to say I've
> honed my English skills rather well.  Thus, here's my take on this:
>
>      Takes a range of commits and replays them onto a new location.
>      Leaves the working tree and the index untouched, and updates no
>      references.  The output of this command is to be used...
>
> This is written in a concise yet slightly imperative way, which should
> be suitable for the purpose.  I hope you agree.

I agree and I like it, so I have changed it to the above in version 5
I just sent.

Thanks!

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v4 11/15] replay: use standard revision ranges
  2023-09-08 22:55         ` Linus Arver
  2023-09-10  3:20           ` Linus Arver
@ 2023-10-10 12:48           ` Christian Couder
  2023-10-19 19:26             ` Linus Arver
  1 sibling, 1 reply; 208+ messages in thread
From: Christian Couder @ 2023-10-10 12:48 UTC (permalink / raw)
  To: Linus Arver
  Cc: git, Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Christian Couder

On Sat, Sep 9, 2023 at 12:55 AM Linus Arver <linusa@google.com> wrote:
>
> Hi Christian,
>
> I am only reviewing the docs. To assume the mindset of a Git user
> unfamiliar with this command, I purposely did not read the cover letter
> until after this review was done.

Ok, thanks!

> Christian Couder <christian.couder@gmail.com> writes:
>
> > [...]
> >
> > diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
> > new file mode 100644
> > index 0000000000..9a2087b01a
> > --- /dev/null
> > +++ b/Documentation/git-replay.txt
> > @@ -0,0 +1,90 @@
> > +git-replay(1)
> > +=============
> > +
> > +NAME
> > +----
> > +git-replay - Replay commits on a different base, without touching working tree
>
> How about using the same language ("new location") as in the DESCRIPTION
> heading?

I actually think that "base" is better than "location", also perhaps
saying things a bit differently than in the description can help
better understand the command.

I am Ok with replacing "different" with "new", mainly because it is
shorter, though.

> Also, the "without touching working tree" part is incomplete
> because as explained later on, the index and refs are also left alone.

I agree that it is incomplete.

> How about just "safely"?

I think that we want to say that the command can work on bare repos,
and I don't think "safely" conveys that meaning well. So now it is:

"git-replay - Replay commits on a new base, works on bare repos too"

> > +SYNOPSIS
> > +--------
> > +[verse]
> > +'git replay' --onto <newbase> <revision-range>...
> > +
> > +DESCRIPTION
> > +-----------
> > +
> > +Takes a range of commits, and replays them onto a new location.
>
> OK.
>
> > Does
> > +not touch the working tree or index, and does not update any
> > +references.
>
> How about this version?
>
>     The new commits are created without modifying the working tree,
>     index, or any references.

I agree it's nicer, but I prefer Dragan Simic's version.

> Also, by "references" we mean the refs in ".git/refs/*". In the
> gitrevisions man page we use the term "refnames" to refer to these bits,
> so maybe "refnames" is better than "references"? The simpler "branches"
> is another option.

"references" seems to be used much more often in the docs than
"refnames" (204 vs 18 occurrences). And deciding between "refnames"
and "references" in the docs is a global issue clearly outside the
scope of this series. So for now I will keep "references".

I don't like "branches" as I think tags and other refs are concerned too.

> > However, the output of this command is meant to be used
> > +as input to `git update-ref --stdin`, which would update the relevant
> > +branches.
>
> Before we get to this sentence, it would be great to explain why this
> command is useful (what problem does it solve)?
>
> Also, it would help to add "(see OUTPUT section below)" as a
> navigational aid in case some readers are wondering what the output
> looks like (and have not yet gotten to that section).
>
> I've noticed that you're using the phrase "starting point" in the
> OPTIONS section. I think this is better than "location" or "base" (FWIW
> we started using "starting point" in 0a02ca2383 (SubmittingPatches:
> simplify guidance for choosing a starting point, 2023-07-14)).
>
> The following is a version of this section that attempts to address my
> comments above (I've included the other bits already reviewed earlier to
> make it easier to read):
>
>     Takes a range of commits, and replays them onto a new starting
>     point. The new commits are created without modifying the working
>     tree, index, or any branches. If there are branches that point to
>     the commits in <revision-range>, list them in the output with both
>     the original commit hash and the corresponding (replayed) commit
>     hash (see OUTPUT section below) .

I like the "(see OUTPUT section below)" part, but I don't like the
fact that passing the output as input to `git update-ref --stdin`
isn't mentioned anymore.

So I have just added "(see the OUTPUT section below)".

>     This command is like linkgit:git-rebase[1], but notably does not
>     require a working tree or index.

I don't quite like this as the command will later also be like
cherry-pick (with the --advance mode).

> This means you can run this command
>     in a bare repo (useful for server-side environments). And because
>     nothing is modified (only new commits are created), it's like a "dry
>     run" rebase.

Bare repos are now mentioned in the title of the page, so I am not
sure it's worth mentioning.

>     By combining this command with `git update-ref --stdin`, the
>     relevant branches can be updated. That is, the branches that were
>     pointing originally to the commits given in the <revision-range>
>     will be updated to point to the replayed commits. This is similar to
>     the way how `git rebase --update-refs` updates multiple branches in
>     the affected range.

I am not sure this is very useful, also I'm afraid that with all the
changes you propose the description section would be a bit too long.

Maybe you should propose some of the changes above in a follow up
patch series after this patch series has been merged.

> > +THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
> > +
> > +OPTIONS
> > +-------
> > +
> > +--onto <newbase>::
>
> How about 'starting-point' instead of 'newbase'?

I think "newbase" is very short and to the point. With --onto we
clearly want something that can do a rebase, so I think using
something with "base" in it is a good choice here.

> > +     Starting point at which to create the new commits.  May be any
> > +     valid commit, and not just an existing branch name.
>
> Add "See linkgit:gitrevisions[7]." at the end?

I don't think it's worth it to mention "linkgit:gitrevisions[7]"
everywhere we can pass a revision. I think it makes the doc heavier
than it should be, especially here where the command is a plumbing one
for now and users should be quite experienced with Git already.

> > +The update-ref command(s) in the output will update the branch(es) in
> > +the revision range to point at the new commits, similar to the way how
> > +`git rebase --update-refs` updates multiple branches in the affected
> > +range.
>
> Ah, good example. I've moved this to my larger example above, so I don't
> think this paragraph is needed here any more (it probably didn't belong
> in OPTIONS anyway).

I think it's important here to help users understand that with this
option they have something very similar to "rebase".

> > +<revision-range>::
> > +     Range of commits to replay; see "Specifying Ranges" in
> > +     linkgit:git-rev-parse.
>
> OK.
>
> > +OUTPUT
> > +------
> > +
> > +When there are no conflicts, the output of this command is usable as
> > +input to `git update-ref --stdin`.
>
> What happens if there are conflicts? Probably good to mention in the
> DISCUSSION section. Some questions you may want to answer for the
> reader:
>
> (1) Is git-replay an all-or-nothing operation? That is, if there are any
> conflicts, is the output always empty (or do we still output those
> branches that *can* be updated without conflicts)?
>
> (2) What is meant by "conflict" for git-replay? Is it the same meaning
> as the case for git-rebase?
>
> For (1), in your cover letter under "# Important limitations" you say
> "No resumability" but I am not sure if this means git-replay will output
> *something* before exiting with an error, or simply nothing at all.

In a follow up series we might add options to generate some output in
case of conflicts. When we do that we will discuss this. So I think
for now it should be Ok to not talk more about the output in case of
conflicts.

> Speaking of the limitations section, perhaps it's worth pointing those
> out under DISCUSSION as well?

I don't think it's worth officially discussing the limitations in the
docs when some of them might be lifted soon after this series
graduates. The command is experimental, so I think users can
understand it if everything is not spelled out. Also if we spell out
things too much, users might rely on what we say when it could
actually change soon.

> > It is basically of the form:
>
> Why "basically"? Are there cases where the output can be different than
> the example given below? If not, then perhaps drop the word "basically"?

Ok, I have dropped "basically".

> > +     update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
> > +     update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
> > +     update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
> > +
> > +where the number of refs updated depends on the arguments passed and
> > +the shape of the history being replayed.
>
> Let's use "number of branches" instead of "number of refs" here to be
> consistent with the language elsewhere.

I prefer "refs" or "references" here.

> > +EXIT STATUS
> > +-----------
> > +
> > +For a successful, non-conflicted replay, the exit status is 0.  When
> > +the replay has conflicts, the exit status is 1.
>
> OK.
>
> > If the replay is not
> > +able to complete (or start) due to some kind of error, the exit status
> > +is something other than 0 or 1.
>
> Not sure how useful "due to some kind of error" is here --- presumably
> the inability to replay is always due to some kind of error.

I think here "due to some kind of error" means something else than a
conflict which could also prevent the reply from fully "working".

> Would it be worth including a brief explanation about why git-replay
> might be unable to complete or start (is this behavior in practice a
> common-enough thing to document here)?

I don't think it's worth it at this step for this new command.

> > +EXAMPLES
> > +--------
> > +
> > +To simply rebase mybranch onto target:
>
> Looking at the CLI arguments, I think this phrase should be:
>
>     Replay the commits in `origin/main..mybranch` onto `target`:

We suppose that users can understand that "rebase mybranch onto
target" means the same thing as what you suggest. Also I think it's
useful to associate "rebase" with "--onto" by deliberately using
"rebase" here.

However to make it a bit clearer, I agree that quoting `mybranch` and
`target` is better, so I have changed it to:

"To simply rebase `mybranch` onto `target`:"

> > +------------
> > +$ git replay --onto target origin/main..mybranch
> > +update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
> > +------------
> > +
> > +When calling `git replay`, one does not need to specify a range of
> > +commits to replay using the syntax `A..B`; any range expression will
> > +do:
> > +
> > +------------
> > +$ git replay --onto origin/main ^base branch1 branch2 branch3
>
> Instead of `base`, how about `olderbranch`?

Sorry but like above, I like "base" here.

> > +update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
> > +update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
> > +update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
> > +------------
> > +
> > +This will simultaneously rebase branch1, branch2, and branch3 -- all
> > +commits they have since base, playing them on top of origin/main.
>
> How about
>
>     This will rebase the commits in `branch1`, `branch2`, and `branch3`
>     (excluding those in `base`), preplaying them on top of `origin/main`.
>
> > +These three branches may have commits on top of base that they have in
> > +common, but that does not need to be the case.
>
> s/base/`base`

I am Ok with quoting `branch1`, `branch2`, `branch3`, `base` and
`origin/main`, but otherwise I prefer to keep the original wording.

> > +GIT
> > +---
> > +Part of the linkgit:git[1] suite
>
> One question I do have is what happens if you run git-replay twice
> (successfully)? Does the second invocation create another set of (new)
> successfully replayed commits?

Yes, I think it does that.

> I ask because I'm interested in informing
> the readers of our docs about any potential pitfalls from abusing this
> command by mistake.

I appreciate your desire to give high quality docs to our users, but I
don't think it's a big pitfall and I think that this command is still
very much "in the works" and is also designed for experienced users
for now, so I am not sure it's the right time to spend too much time
on this.

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v4 11/15] replay: use standard revision ranges
  2023-09-10  3:20           ` Linus Arver
@ 2023-10-10 12:48             ` Christian Couder
  0 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-10-10 12:48 UTC (permalink / raw)
  To: Linus Arver
  Cc: git, Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Christian Couder

On Sun, Sep 10, 2023 at 5:20 AM Linus Arver <linusa@google.com> wrote:
>
> Linus Arver <linusa@google.com> writes:
>
> >> +update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
> >> +update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
> >> +update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
> >> +------------
> >> +
> >> +This will simultaneously rebase branch1, branch2, and branch3 -- all
> >> +commits they have since base, playing them on top of origin/main.
> >
> > How about
> >
> >     This will rebase the commits in `branch1`, `branch2`, and `branch3`
> >     (excluding those in `base`), preplaying them on top of `origin/main`.
>
> Oops, I meant "replaying" not "preplaying". But also, perhaps the
> following is simpler?
>
>     This will replay the commits in `branch1`, `branch2`, and `branch3`
>     (excluding those in `base`), on top of `origin/main`.

Here also I prefer to keep "rebase".

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v4 11/15] replay: use standard revision ranges
  2023-09-07 10:24         ` Johannes Schindelin
@ 2023-10-10 12:49           ` Christian Couder
  0 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-10-10 12:49 UTC (permalink / raw)
  To: Johannes Schindelin
  Cc: git, Junio C Hamano, Patrick Steinhardt, Elijah Newren, John Cai,
	Derrick Stolee, Phillip Wood, Calvin Wan, Toon Claes,
	Christian Couder

On Thu, Sep 7, 2023 at 1:02 PM Johannes Schindelin
<Johannes.Schindelin@gmx.de> wrote:
>
> Hi Christian,
>
> It is a bit surprising to see the manual page added in _this_ patch, in
> the middle of the series... I can live with it, though.

It's explained in the cover letter and a bit in some commit messages.

For example in the "Possibly controversial issues" section of the
cover letter, there is:

"
* when and where to add tests and docs: although t6429 has tests that
  are changed to use the new command instead of the fast-rebase
  test-tool command as soon as the former is introduced, there is no
  specific test script and no doc for the new command until commit
  11/15 when standard revision ranges are used. This is done to avoid
  churn in tests and docs while the final form of the command hasn't
  crystalized enough. Adding tests and doc at this point makes this
  commit quite big and possibly more difficult to review than if they
  were in separate commits though. On the other hand when tests and
  docs are added in specific commits, some reviewers say it would be
  better to introduce them when the related changes are made.
"

> On Thu, 7 Sep 2023, Christian Couder wrote:

> > +SYNOPSIS
> > +--------
> > +[verse]
> > +'git replay' --onto <newbase> <revision-range>...
>
> We need to make it clear here, already in the SYNOPSIS, that this is
> experimental. Let's add an `(EXPERIMENTAL!)` prefix here.

I haven't done that as other commands marked as EXPERIMENTAL don't
have that in their SYNOPSIS.

> > [...]
> > diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
> > new file mode 100755
>
> Just like the manual page, I would have expected this test to be
> introduced earlier, and not piggy-backed onto one of the handful "let's
> turn fast-rebase into replay" patches.

This is discussed above.

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v4 12/15] replay: disallow revision specific options and pathspecs
  2023-09-07 10:24         ` Johannes Schindelin
@ 2023-10-10 12:49           ` Christian Couder
  0 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-10-10 12:49 UTC (permalink / raw)
  To: Johannes Schindelin
  Cc: git, Junio C Hamano, Patrick Steinhardt, Elijah Newren, John Cai,
	Derrick Stolee, Phillip Wood, Calvin Wan, Toon Claes,
	Christian Couder

Hi Dscho,

On Thu, Sep 7, 2023 at 1:03 PM Johannes Schindelin
<Johannes.Schindelin@gmx.de> wrote:

> As pointed out elsewhere in this mail thread, I consider this patch to do
> more harm than good. After switching the command to plumbingmanipulators
> it should be possible to just forego all command-line validation and leave
> that job to the caller.
>
> Therefore I would love to see this patch dropped.

Ok, I have dropped this patch in version 5.

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v4 00/15] Introduce new `git replay` command
  2023-09-07 10:25       ` [PATCH v4 00/15] Introduce new `git replay` command Johannes Schindelin
@ 2023-10-10 12:50         ` Christian Couder
  0 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-10-10 12:50 UTC (permalink / raw)
  To: Johannes Schindelin
  Cc: git, Junio C Hamano, Patrick Steinhardt, Elijah Newren, John Cai,
	Derrick Stolee, Phillip Wood, Calvin Wan, Toon Claes

Hi Dscho,

On Thu, Sep 7, 2023 at 1:04 PM Johannes Schindelin
<Johannes.Schindelin@gmx.de> wrote:
>
> Hi Christian,
>
> hope you had a restful vacation!

Yes, thanks! I hope you had a good summer too.

> I left a bit of feedback and think that once my concerns are addressed, a
> v5 will be ready for `next`.

Thanks for your review!

I think I have addressed most (if not all) of your concerns in version 5.

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v3 11/15] replay: use standard revision ranges
  2023-10-10 12:44             ` Christian Couder
@ 2023-10-10 14:02               ` Dragan Simic
  0 siblings, 0 replies; 208+ messages in thread
From: Dragan Simic @ 2023-10-10 14:02 UTC (permalink / raw)
  To: Christian Couder
  Cc: Toon Claes, git, Junio C Hamano, Patrick Steinhardt,
	Johannes Schindelin, Elijah Newren, John Cai, Derrick Stolee,
	Phillip Wood, Felipe Contreras, Calvin Wan, Christian Couder

On 2023-10-10 14:44, Christian Couder wrote:
> On Thu, Sep 7, 2023 at 11:02 PM Dragan Simic <dsimic@manjaro.org> 
> wrote:
>> 
>> On 2023-09-07 10:32, Christian Couder wrote:
>> > On Thu, Jun 22, 2023 at 12:03 PM Toon Claes <toon@iotcl.com> wrote:
>> >>
>> >> Christian Couder <christian.couder@gmail.com> writes:
>> >>
>> >> > +DESCRIPTION
>> >> > +-----------
>> >> > +
>> >> > +Takes a range of commits, and replays them onto a new location.  Does
>> >> > +not touch the working tree or index, and does not update any
>> >> > +references.  However, the output of this command is meant to be used
>> >>
>> >> Small suggestion here:
>> >>
>> >> Takes a range of commits, and replays them onto a new location.  Does
>> >> neither touch the working tree nor index, and does not update any
>> >> references.
>> >
>> > I am not a native speaker, so I am not sure what's best here. I find
>> > your suggestion a bit less clear though, so until a native speaker
>> > agrees with it or maybe finds something even better, I prefer to leave
>> > it as-is.
>> 
>> I'm also not a native English speaker, but I spent about 2.5 years
>> contributing a whole lot to English Wikipedia, so I'd dare to say I've
>> honed my English skills rather well.  Thus, here's my take on this:
>> 
>>      Takes a range of commits and replays them onto a new location.
>>      Leaves the working tree and the index untouched, and updates no
>>      references.  The output of this command is to be used...
>> 
>> This is written in a concise yet slightly imperative way, which should
>> be suitable for the purpose.  I hope you agree.
> 
> I agree and I like it, so I have changed it to the above in version 5
> I just sent.

Great, thanks.

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v4 11/15] replay: use standard revision ranges
  2023-10-10 12:48           ` Christian Couder
@ 2023-10-19 19:26             ` Linus Arver
  0 siblings, 0 replies; 208+ messages in thread
From: Linus Arver @ 2023-10-19 19:26 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Christian Couder

Christian Couder <christian.couder@gmail.com> writes:

> On Sat, Sep 9, 2023 at 12:55 AM Linus Arver <linusa@google.com> wrote:
>>
>> Hi Christian,
>>
>> I am only reviewing the docs. To assume the mindset of a Git user
>> unfamiliar with this command, I purposely did not read the cover letter
>> until after this review was done.
>
> Ok, thanks!
>
> [...]
>> > +     Starting point at which to create the new commits.  May be any
>> > +     valid commit, and not just an existing branch name.
>>
>> Add "See linkgit:gitrevisions[7]." at the end?
>
> I don't think it's worth it to mention "linkgit:gitrevisions[7]"
> everywhere we can pass a revision. I think it makes the doc heavier
> than it should be, especially here where the command is a plumbing one
> for now and users should be quite experienced with Git already.

I agree that plumbing commands assume that users are more
experienced with Git already, so SGTM.

>> I ask because I'm interested in informing
>> the readers of our docs about any potential pitfalls from abusing this
>> command by mistake.
>
> I appreciate your desire to give high quality docs to our users, but I
> don't think it's a big pitfall and I think that this command is still
> very much "in the works" and is also designed for experienced users
> for now, so I am not sure it's the right time to spend too much time
> on this.

Also sounds reasonable to me. Thank you for considering my suggestions,
much appreciated!

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v5 11/14] replay: use standard revision ranges
  2023-10-10 12:38         ` [PATCH v5 11/14] replay: use standard revision ranges Christian Couder
@ 2023-10-19 19:49           ` Linus Arver
  0 siblings, 0 replies; 208+ messages in thread
From: Linus Arver @ 2023-10-19 19:49 UTC (permalink / raw)
  To: Christian Couder, git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Christian Couder

This patch's documentation bits LGTM. The comments I have below are all
nits and I don't think they are enough to require a re-roll. If v5 is
accepted as-is, then this stuff could be #leftoverbits for a future
(minor) cleanup.

Thanks!

Christian Couder <christian.couder@gmail.com> writes:

> From: Elijah Newren <newren@gmail.com>
> 
> +DESCRIPTION
> +-----------
> +
> +Takes a range of commits and replays them onto a new location. Leaves
> +the working tree and the index untouched, and updates no
> +references.

Nit: in v4 you had

     Does
    +not touch the working tree or index, and does not update any
    +references. 

and the "does not update any references" sounds more natural than
"updates no references".

> The output of this command is meant to be used as input to
> +`git update-ref --stdin`, which would update the relevant branches
> +(see the OUTPUT section below).
> +
> +THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.

Nit: add "IN THE FUTURE" at the end.

> +EXAMPLES
> +--------
> +
> +To simply rebase `mybranch` onto `target`:

Nit: s/To simply rebase/Rebase

or, remove "simply" because it doesn't add much value.

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v5 00/14] Introduce new `git replay` command
  2023-10-10 12:38       ` [PATCH v5 00/14] " Christian Couder
                           ` (13 preceding siblings ...)
  2023-10-10 12:38         ` [PATCH v5 14/14] replay: stop assuming replayed branches do not diverge Christian Couder
@ 2023-10-26 13:44         ` Johannes Schindelin
  2023-10-29  6:01           ` Elijah Newren
  2023-11-02 15:06           ` Christian Couder
  2023-10-29  6:00         ` Elijah Newren
  2023-11-02 13:51         ` [PATCH v6 " Christian Couder
  16 siblings, 2 replies; 208+ messages in thread
From: Johannes Schindelin @ 2023-10-26 13:44 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Patrick Steinhardt, Elijah Newren, John Cai,
	Derrick Stolee, Phillip Wood, Calvin Wan, Toon Claes,
	Dragan Simic, Linus Arver

Hi Christian,

On Tue, 10 Oct 2023, Christian Couder wrote:

>  6:  ec51351889 !  6:  37d545d5d6 replay: don't simplify history
>     @@ Metadata
>      Author: Elijah Newren <newren@gmail.com>
>
>       ## Commit message ##
>     -    replay: don't simplify history
>     +    replay: change rev walking options
>
>          Let's set the rev walking options we need after calling
>     -    setup_revisions() instead of before. This makes it clearer which options
>     -    we need.
>     +    setup_revisions() instead of before. This enforces options we always
>     +    want.
>     +
>     +    We want the command to work from older commits to newer ones by default,
>     +    but we are Ok with letting users reverse that, using --reverse, if that's
>     +    what they really want.
>
>          Also we don't want history simplification, as we want to deal with all
>          the commits in the affected range.
>
>     +    Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
>          Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
>          Signed-off-by: Elijah Newren <newren@gmail.com>
>          Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
>     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
>         }
>
>      +  /* requirements/overrides for revs */
>     -+  revs.reverse = 1;
>     ++  revs.reverse = !revs.reverse;
>      +  revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
>      +  revs.topo_order = 1;
>      +  revs.simplify_history = 0;

This still overrides a couple of command-line options, _silently_. I would
prefer those three assignments to be moved just before the
`setup_revisions()` call.

Letting users override these settings may not make much sense, but it
makes even less sense to pretend to let them override the settings and
then just ignore them without warning. (See also
https://en.wikipedia.org/wiki/Principle_of_least_astonishment.)

Moving these three assignments before the `setup_revisions()` call would
neatly remedy that.

>  7:  cd4ed07d2d =  7:  2943f08926 replay: add an important FIXME comment about gpg signing
>  8:  e45a55917c =  8:  f81962ba41 replay: remove progress and info output
>  9:  0587a76cbb =  9:  236747497e replay: remove HEAD related sanity check
> 10:  d10368e87a = 10:  3374d5be23 replay: make it a minimal server side command
> 11:  4e09572c43 ! 11:  197d076a93 replay: use standard revision ranges
>     @@ Commit message
>          way as many other Git commands. This makes its interface more
>          standard and more flexible.
>
>     +    This also enables many revision related options accepted and
>     +    eaten by setup_revisions(). If the replay command was a high level
>     +    one or had a high level mode, it would make sense to restrict some
>     +    of the possible options, like those generating non-contiguous
>     +    history, as they could be confusing for most users.
>     +
>          Also as the interface of the command is now mostly finalized,
>          we can add some documentation as well as testcases to make sure
>          the command will continue to work as designed in the future.
>
>     +    We only document the rev-list related options among all the
>     +    revision related options that are now accepted, as the rev-list
>     +    related ones are probably the most useful for now.
>     +
>     +    Helped-by: Dragan Simic <dsimic@manjaro.org>
>     +    Helped-by: Linus Arver <linusa@google.com>
>          Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
>          Signed-off-by: Elijah Newren <newren@gmail.com>
>          Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
>     @@ Documentation/git-replay.txt (new)
>      +
>      +NAME
>      +----
>     -+git-replay - Replay commits on a different base, without touching working tree
>     ++git-replay - Replay commits on a new base, works on bare repos too
>      +
>      +
>      +SYNOPSIS

As mentioned in
https://lore.kernel.org/git/03460733-0219-c648-5757-db1958f8042e@gmx.de/,
I would like the `EXPERIMENTAL` label to be shown prominently here.
Probably not only the `SYNOPSIS` as I had originally suggested but also in
the `NAME`.

Otherwise we may end up with the same situation as with the (from my
perspective, failed) `git switch`/`git restore` experiment, where we
wanted to explore a better user experience than the overloaded `git
checkout` command, only to now be stuck with having to maintain
backward-compatibility for `git switch`/`git restore` command-line options
that were not meant to be set in stone but to be iterated on, instead. A
real-life demonstration of [Hyrum's Law](hyrumslaw.com/), if you like. Or,
from a different angle, we re-enacted https://xkcd.com/927/ in a way.

I'd like to suggest to learn from history and avoid this by tacking on a
warning label right at the top of the documentation. We may eventually
manage to iterate `git replay` to a point where it is totally capable to
supersede `git rebase`, by doing everything the latter does, except
better, who knows? But we _do_ need the liberty to make sweeping changes
to this new builtin if we want to have a prayer of doing that. And I fear
that not even mentioning the EXPERIMENTAL nature right at the top of the
manual page would just render us into that undesirable corner.

In addition, I am still a bit uneasy with introducing both the manual page
and the test script in this commit (see my comments in
https://lore.kernel.org/git/03460733-0219-c648-5757-db1958f8042e@gmx.de/).
It would be better to uphold our high standard and introduce scaffolds for
both files in the first commit, then populate the file contents
incrementally in the same the patches that introduce the corresponding
options/features/changes.

The rest of the interdiff consists mostly of context changes intermixed
with a couple of changes that I like.

Ciao,
Johannes

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v5 00/14] Introduce new `git replay` command
  2023-10-10 12:38       ` [PATCH v5 00/14] " Christian Couder
                           ` (14 preceding siblings ...)
  2023-10-26 13:44         ` [PATCH v5 00/14] Introduce new `git replay` command Johannes Schindelin
@ 2023-10-29  6:00         ` Elijah Newren
  2023-10-29 14:14           ` Johannes Schindelin
  2023-11-02 14:48           ` Christian Couder
  2023-11-02 13:51         ` [PATCH v6 " Christian Couder
  16 siblings, 2 replies; 208+ messages in thread
From: Elijah Newren @ 2023-10-29  6:00 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	John Cai, Derrick Stolee, Phillip Wood, Calvin Wan, Toon Claes,
	Dragan Simic, Linus Arver

Hi,

On Tue, Oct 10, 2023 at 5:39 AM Christian Couder
<christian.couder@gmail.com> wrote:
> * Patch 12/15 (replay: disallow revision specific options and
>   pathspecs) in version 4 has been removed, so there are now only 14
>   patches instead of 15 in the series. This follows a suggestion by
>   Dscho, and goes in the direction Elijah initially wanted before
>   Derrick Stolee argued for disallowing revision specific options and
>   pathspecs.

That's too strongly worded; and may be misleading.  My primary goal in
that discussion was that setup_revisions() should not be a disallowed
API for future consumers such as git-replay.  My secondary thought, at
the time, was that although I agreed that setup_revisions() was a
problematic API, I didn't think fixing it should be a prerequisite for
new features to make use of it.

However, your paragraph here could easily be read that I think the
setup_revisions() API is fine.  I don't.  I actually think fixing the
setup_revisions() API and preventing not only git-replay but many
other commands from accepting non-sensical flags, as suggested by
Stolee, is a very good idea.  I even brought up the example
$ git stash show --graph --relative-date --min-parents=3
     --simplify-merges --cherry --show-pulls --unpacked -v -t -8
     --format=oneline --abbrev=12 --pretty=fuller --show-notes
     --encode-email-headers --always --branches --indexed-objects stash@{0}
in the thread[1] along with the comment "guess which flags are ignored
and which aren't".  That's garbage.  This digging plus various things
Stolee said actually convinced me that perhaps prioritizing fixing the
setup_revisions() API over adding new consumers does make sense.

But, I don't feel nearly as strong about it as Stolee on
prioritization, so I'm not going to object too strongly with this
patch being tossed for now.  But I do want to note that I actually
like Stolee's suggestion that we should fix that API and tighten what
many commands accept.

[1] https://lore.kernel.org/git/f5dd91a7-ba11-917a-39e2-2737829558cb@github.com/

> * In patch 2/14 (replay: introduce new builtin) the command is now in
>   the "plumbingmanipulators" category instead of the "mainporcelain"
>   category. This is more in line with the goal of introducing it as a
>   plumbing command for now. Thanks to a suggestion from Dscho.

I do want to eventually make it a porcelain, but I think it's pretty
far from that in its current state, so this is a good change.

> * In patch 6/14 (replay: change rev walking options) the commit
>   message has been improved, including its subject, to better match
>   and explain what the patch does.

This (and multiple other changes I elided) are good; thanks for
pushing forward on this.

>   Also instead of forcing reverse
>   order we use the reverse order by default but allow it to be changed
>   using `--reverse`. Thanks to Dscho.

I can see why this might sometimes be useful for exclusively linear
history, but it seems to open a can of worms and possibly unfixable
corner cases for non-linear history.  I'd rather not do this, or at
least pull it out of this series and let us discuss it in some follow
up series.  There are some other alternatives that might handle such
usecases better.

>  6:  ec51351889 !  6:  37d545d5d6 replay: don't simplify history
...
>     +    We want the command to work from older commits to newer ones by default,
>     +    but we are Ok with letting users reverse that, using --reverse, if that's
>     +    what they really want.

As noted above, _I_ am not ok with this yet.  Given the patch
prominently bears my name, the "we" here at a minimum is wrong.  I
would rather leave this change out for now and discuss it for a
follow-on series.

>     @@ Documentation/git-replay.txt (new)
>      +
>      +NAME
>      +----
>     -+git-replay - Replay commits on a different base, without touching working tree
>     ++git-replay - Replay commits on a new base, works on bare repos too

really minor point: "works on" or "works in" or "works with" ?

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v5 00/14] Introduce new `git replay` command
  2023-10-26 13:44         ` [PATCH v5 00/14] Introduce new `git replay` command Johannes Schindelin
@ 2023-10-29  6:01           ` Elijah Newren
  2023-11-02 14:59             ` Christian Couder
  2023-11-02 15:06           ` Christian Couder
  1 sibling, 1 reply; 208+ messages in thread
From: Elijah Newren @ 2023-10-29  6:01 UTC (permalink / raw)
  To: Johannes Schindelin
  Cc: Christian Couder, git, Junio C Hamano, Patrick Steinhardt,
	John Cai, Derrick Stolee, Phillip Wood, Calvin Wan, Toon Claes,
	Dragan Simic, Linus Arver

Hi,

On Thu, Oct 26, 2023 at 6:44 AM Johannes Schindelin
<Johannes.Schindelin@gmx.de> wrote:
>
> Hi Christian,
>
> On Tue, 10 Oct 2023, Christian Couder wrote:
>
[...]
> >      +  /* requirements/overrides for revs */
> >     -+  revs.reverse = 1;
> >     ++  revs.reverse = !revs.reverse;
> >      +  revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
> >      +  revs.topo_order = 1;
> >      +  revs.simplify_history = 0;
>
> This still overrides a couple of command-line options, _silently_. I would
> prefer those three assignments to be moved just before the
> `setup_revisions()` call.
>
> Letting users override these settings may not make much sense, but it
> makes even less sense to pretend to let them override the settings and
> then just ignore them without warning. (See also
> https://en.wikipedia.org/wiki/Principle_of_least_astonishment.)
>
> Moving these three assignments before the `setup_revisions()` call would
> neatly remedy that.

I agree that warnings or error messages would be better.

But if we're talking about something short of that, I'd actually argue
the opposite of what you do here.  I intentionally moved these
assignments after setup_revisions(), and in my mind, the purpose in
doing so was to satisfy the Principle of Least Astonishment.  My
experience with git-fast-export, where some settings are made before
calling setup_revisions() and then can be overridden, and then do
completely hideous things, was much worse to me than just admitting
the flags are bad given the various assumptions the tool makes.  I
have some patches sitting around to fix fast-export that I never got
around to upstreaming, but when it came time to implement git-replay,
I made sure to fix what I viewed as the bigger problem.

[...]
> >     @@ Documentation/git-replay.txt (new)
> >      +
> >      +NAME
> >      +----
> >     -+git-replay - Replay commits on a different base, without touching working tree
> >     ++git-replay - Replay commits on a new base, works on bare repos too
> >      +
> >      +
> >      +SYNOPSIS
>
> As mentioned in
> https://lore.kernel.org/git/03460733-0219-c648-5757-db1958f8042e@gmx.de/,
> I would like the `EXPERIMENTAL` label to be shown prominently here.
> Probably not only the `SYNOPSIS` as I had originally suggested but also in
> the `NAME`.
>
> Otherwise we may end up with the same situation as with the (from my
> perspective, failed) `git switch`/`git restore` experiment, where we
> wanted to explore a better user experience than the overloaded `git
> checkout` command, only to now be stuck with having to maintain
> backward-compatibility for `git switch`/`git restore` command-line options
> that were not meant to be set in stone but to be iterated on, instead. A
> real-life demonstration of [Hyrum's Law](hyrumslaw.com/), if you like. Or,
> from a different angle, we re-enacted https://xkcd.com/927/ in a way.
>
> I'd like to suggest to learn from history and avoid this by tacking on a
> warning label right at the top of the documentation. We may eventually
> manage to iterate `git replay` to a point where it is totally capable to
> supersede `git rebase`, by doing everything the latter does, except
> better, who knows? But we _do_ need the liberty to make sweeping changes
> to this new builtin if we want to have a prayer of doing that. And I fear
> that not even mentioning the EXPERIMENTAL nature right at the top of the
> manual page would just render us into that undesirable corner.

I fully support this.  Absolutely, 100%.

Thanks both,
Elijah

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v5 00/14] Introduce new `git replay` command
  2023-10-29  6:00         ` Elijah Newren
@ 2023-10-29 14:14           ` Johannes Schindelin
  2023-10-30 17:18             ` Elijah Newren
  2023-11-02 14:48           ` Christian Couder
  1 sibling, 1 reply; 208+ messages in thread
From: Johannes Schindelin @ 2023-10-29 14:14 UTC (permalink / raw)
  To: Elijah Newren
  Cc: Christian Couder, git, Junio C Hamano, Patrick Steinhardt,
	John Cai, Derrick Stolee, Phillip Wood, Calvin Wan, Toon Claes,
	Dragan Simic, Linus Arver

[-- Attachment #1: Type: text/plain, Size: 1511 bytes --]

Hi Elijah,

On Sat, 28 Oct 2023, Elijah Newren wrote:

> On Tue, Oct 10, 2023 at 5:39 AM Christian Couder
> <christian.couder@gmail.com> wrote:
> > * Patch 12/15 (replay: disallow revision specific options and
> >   pathspecs) in version 4 has been removed, so there are now only 14
> >   patches instead of 15 in the series. This follows a suggestion by
> >   Dscho, and goes in the direction Elijah initially wanted before
> >   Derrick Stolee argued for disallowing revision specific options and
> >   pathspecs.
>
> [... snipping many parts that I agree with...]
>
> >   Also instead of forcing reverse order we use the reverse order by
> >   default but allow it to be changed using `--reverse`. Thanks to
> >   Dscho.
>
> I can see why this might sometimes be useful for exclusively linear
> history, but it seems to open a can of worms and possibly unfixable
> corner cases for non-linear history.  I'd rather not do this, or at
> least pull it out of this series and let us discuss it in some follow
> up series.  There are some other alternatives that might handle such
> usecases better.

I find myself wishing for an easy way to reverse commits, if only to
switch around the latest two commits while stopped during a rebase.

So it would have been nice for me if there had been an easy, worktree-less
way to make that happen.

I guess this would be going in the direction of reordering commits,
though, something we deliberately left for later?

Ciao,
Johannes

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v5 00/14] Introduce new `git replay` command
  2023-10-29 14:14           ` Johannes Schindelin
@ 2023-10-30 17:18             ` Elijah Newren
  2023-11-02 14:44               ` Christian Couder
  0 siblings, 1 reply; 208+ messages in thread
From: Elijah Newren @ 2023-10-30 17:18 UTC (permalink / raw)
  To: Johannes Schindelin
  Cc: Christian Couder, git, Junio C Hamano, Patrick Steinhardt,
	John Cai, Derrick Stolee, Phillip Wood, Calvin Wan, Toon Claes,
	Dragan Simic, Linus Arver

Hi Johannes,

On Sun, Oct 29, 2023 at 7:14 AM Johannes Schindelin
<Johannes.Schindelin@gmx.de> wrote:
>
> Hi Elijah,
>
> On Sat, 28 Oct 2023, Elijah Newren wrote:
>
> > On Tue, Oct 10, 2023 at 5:39 AM Christian Couder
> > <christian.couder@gmail.com> wrote:
> > > * Patch 12/15 (replay: disallow revision specific options and
> > >   pathspecs) in version 4 has been removed, so there are now only 14
> > >   patches instead of 15 in the series. This follows a suggestion by
> > >   Dscho, and goes in the direction Elijah initially wanted before
> > >   Derrick Stolee argued for disallowing revision specific options and
> > >   pathspecs.
> >
> > [... snipping many parts that I agree with...]
> >
> > >   Also instead of forcing reverse order we use the reverse order by
> > >   default but allow it to be changed using `--reverse`. Thanks to
> > >   Dscho.
> >
> > I can see why this might sometimes be useful for exclusively linear
> > history, but it seems to open a can of worms and possibly unfixable
> > corner cases for non-linear history.  I'd rather not do this, or at
> > least pull it out of this series and let us discuss it in some follow
> > up series.  There are some other alternatives that might handle such
> > usecases better.
>
> I find myself wishing for an easy way to reverse commits, if only to
> switch around the latest two commits while stopped during a rebase.
>
> So it would have been nice for me if there had been an easy, worktree-less
> way to make that happen.

Seems reasonable; we'll definitely want to keep this in mind.

> I guess this would be going in the direction of reordering commits,
> though, something we deliberately left for later?

Yes, I think that's a good framing for it.

Thanks,
Elijah

^ permalink raw reply	[flat|nested] 208+ messages in thread

* [PATCH v6 00/14] Introduce new `git replay` command
  2023-10-10 12:38       ` [PATCH v5 00/14] " Christian Couder
                           ` (15 preceding siblings ...)
  2023-10-29  6:00         ` Elijah Newren
@ 2023-11-02 13:51         ` Christian Couder
  2023-11-02 13:51           ` [PATCH v6 01/14] t6429: remove switching aspects of fast-rebase Christian Couder
                             ` (16 more replies)
  16 siblings, 17 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-02 13:51 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

# Intro

`git replay` has initially been developed entirely by Elijah Newren
mostly last year (2022) at:

https://github.com/newren/git/commits/replay

I took over this year to polish and upstream it as GitLab is
interested in replacing libgit2, and for that purpose needs a command
to do server side (so without using a worktree) rebases, cherry-picks
and reverts.

I reduced the number of commits and features in this patch series,
compared to what Elijah already developed. Especially I stopped short
of replaying merge commits and replaying interactively. These and
other features might be upstreamed in the future after this patch
series has graduated.

The focus in this series is to make it a good plumbing command that
can already be used server side and that replaces the "fast-rebase"
test-tool command. So things to make it easier to use on the command
line, and more advanced features (like replaying merges) are left out.

It looks like GitHub has actually already been using version 3 of this
patch series in production with good results. See:

https://github.blog/2023-07-27-scaling-merge-ort-across-github/
https://lore.kernel.org/git/304f2a49-5e05-7655-9f87-2011606df5db@gmx.de/

# Content of this cover letter

The "Quick Overview" and "Reasons for diverging from cherry-pick &
rebase" sections just below are describing the purpose of the new
command in the big scheme of things. They are taken from Elijah's
design notes
(https://github.com/newren/git/blob/replay/replay-design-notes.txt)
and describe what we want this command to become and the reasons for
that, not what the command is after only this patch series. Also these
design notes were written at least one year ago, so parts of those 2
sections are not true anymore. I have added Phillip Wood's or Felipe
Contreras' notes (thanks to them) where that's the case, but some now
flawed parts may have missed.

After these two sections, starting with the "Important limitations"
section, you will find sections describing what is actually in this
patch series.

More interesting material is available in Elijah's design notes like
an "Intro via examples"
(https://github.com/newren/git/blob/replay/replay-design-notes.txt#L37-L132),
a discussion about "Preserving topology, replaying merges"
(https://github.com/newren/git/blob/replay/replay-design-notes.txt#L264-L341)
and a "Current status" section describing Elijah's work
(https://github.com/newren/git/blob/replay/replay-design-notes.txt#L344-L392)
before I started working on upstreaming it.

I have not included this material here though, as the documentation
added by this patch series for the `git replay` command already
includes an "EXAMPLES" section, and other sections of Elijah's design
notes might not be interesting for now. Also this cover letter is
already pretty long.  But reviewers can refer to the links above if
they think it can help.

# Quick Overview (from Elijah's design notes)

`git replay`, at a basic level, can perhaps be thought of as a
"default-to-dry-run rebase" -- meaning no updates to the working tree,
or to the index, or to any references.  However, it differs from
rebase in that it:

  * Works for branches that aren't checked out

  * Works in a bare repository

  * Can replay multiple branches simultaneously (with or without common
    history in the range being replayed)

  * Preserves relative topology by default (merges are replayed too in
    Elijah's original work, not in this series)

  * Focuses on performance

  * Has several altered defaults as a result of the above

I sometimes think of `git replay` as "fast-replay", a patch-based
analogue to the snapshot-based fast-export & fast-import tools.

# Reasons for diverging from cherry-pick & rebase (from Elijah's
  design notes)

There are multiple reasons to diverge from the defaults in cherry-pick and
rebase.

* Server side needs

  * Both cherry-pick and rebase, via the sequencer, are heavily tied
    to updating the working tree, index, some refs, and a lot of
    control files with every commit replayed, and invoke a mess of
    hooks[1] that might be hard to avoid for backward compatibility
    reasons (at least, that's been brought up a few times on the
    list).

  * cherry-pick and rebase both fork various subprocesses
    unnecessarily, but somewhat intrinsically in part to ensure the
    same hooks are called that old scripted implementations would have
    called.

    Note: since 356ee4659bb (sequencer: try to commit without forking
    'git commit', 2017-11-24) cherry-pick and rebase do not fork
    subprocesses other than hooks for the cases covered by this patch
    series (i.e. they do not fork "git commit" for simple picks).

  * "Dry run" behavior, where there are no updates to worktree, index,
    or even refs might be important.

  * Should not assume users only want to operate on HEAD (see next
    section)

* Decapitate HEAD-centric assumptions

  * cherry-pick forces commits to be played on top of HEAD;
    inflexible.

  * rebase assumes the range of commits to be replayed is
    upstream..HEAD by default, though it allows one to replay
    upstream..otherbranch -- but it still forcibly and needlessly
    checks out 'otherbranch' before starting to replay things.

    Note: since 767a9c417eb (rebase -i: stop checking out the tip of
    the branch to rebase, 2020-01-24) it's not true that rebase
    forcibly and needlessly checks out 'otherbranch'.

  * Assuming HEAD is involved severely limits replaying multiple
    (possibly divergent) branches.

    Note: since 89fc0b53fdb (rebase: update refs from 'update-ref'
    commands, 2022-07-19) the sequencer can update multiple
    branches. The issue with divergent branch is with command line
    arguments and the todo list generation rather than the
    capabilities of the sequencer.

  * Once you stop assuming HEAD has a certain meaning, there's not
    much reason to have two separate commands anymore (except for the
    funny extra not-necessarily-compatible options both have gained
    over time).

  * (Micro issue: Assuming HEAD is involved also makes it harder for
    new users to learn what rebase means and does; it makes command
    lines hard to parse.  Not sure I want to harp on this too much, as
    I have a suspicion I might be creating a tool for experts with
    complicated use cases, but it's a minor quibble.)

* Performance

  * jj is slaughtering us on rebase speed[2].  I would like us to become
    competitive.  (I dropped a few comments in the link at [2] about why
    git is currently so bad.)

  * From [3], there was a simple 4-patch series in linux.git that took
    53 seconds to rebase.  Switching to ort dropped it to 16 seconds.
    While that sounds great, only 11 *milliseconds* were needed to do
    the actual merges.  That means almost *all* the time (>99%) was
    overhead!  Big offenders:

    * --reapply-cherry-picks should be the default

    * can_fast_forward() should be ripped out, and perhaps other extraneous
      revision walks

      Note: d42c9ffa0f (rebase: factor out branch_base calculation,
      2022-10-17) might already deal with that (according to Felipe
      Contreras).

    * avoid updating working tree, index, refs, reflogs, and control
      structures except when needed (e.g. hitting a conflict, or operation
      finished)

  * Other performance ideas (mostly for future work, not in this
    series)

    * single-file control structures instead of directory of files
      (when doing interactive things which is in Elijah's original
      work, but not in this series)

    * avoid forking subprocesses unless explicitly requested (e.g.
      --exec, --strategy, --run-hooks).  For example, definitely do not
      invoke `git commit` or `git merge`.

    * Sanitize hooks:

      * dispense with all per-commit hooks for sure (pre-commit,
        post-commit, post-checkout).

      * pre-rebase also seems to assume exactly 1 ref is written, and
        invoking it repeatedly would be stupid.  Plus, it's specific
        to "rebase".  So...ignore?  (Stolee's --ref-update option for
        rebase probably broke the pre-rebase assumptions already...)

      * post-rewrite hook might make sense, but fast-import got
        exempted, and I think of replay like a patch-based analogue
        to the snapshot-based fast-import.

    * When not running server side, resolve conflicts in a sparse-cone
      sparse-index worktree to reduce number of files written to a
      working tree.  (See below as well.)

    * [High risk of possible premature optimization] Avoid large
      numbers of newly created loose objects, when replaying large
      numbers of commits.  Two possibilities: (1) Consider using
      tmp-objdir and pack objects from the tmp-objdir at end of
      exercise, (2) Lift code from git-fast-import to immediately
      stuff new objects into a pack?

* Multiple branches and non-checked out branches

  * The ability to operate on non-checked out branches also implies
    that we should generally be able to replay when in a dirty working
    tree (exception being when we expect to update HEAD and any of the
    dirty files is one that needs to be updated by the replay).

  * Also, if we are operating locally on a non-checked out branch and
    hit a conflict, we should have a way to resolve the conflict
    without messing with the user's work on their current
    branch. (This is not is this patch series though.)

    * Idea: new worktree with sparse cone + sparse index checkout,
      containing only files in the root directory, and whatever is
      necessary to get the conflicts

    * Companion to above idea: control structures should be written to
      $GIT_COMMON_DIR/replay-${worktree}, so users can have multiple
      replay sessions, and so we know which worktrees are associated
      with which replay operations.

  - [1] https://lore.kernel.org/git/pull.749.v3.git.git.1586044818132.gitgitgadget@gmail.com/
  - [2] https://github.com/martinvonz/jj/discussions/49
  - [3] https://lore.kernel.org/git/CABPp-BE48=97k_3tnNqXPjSEfA163F8hoE+HY0Zvz1SWB2B8EA@mail.gmail.com/

# Important limitations

* The code exits with code 1 if there are any conflict. No
  resumability. No nice output. No interactivity. No special exit code
  depending on the reason.

* When a commit becomes empty as it is replayed, it is still replayed
  as an empty commit, instead of being dropped.

* No replaying merges, nor root commits. Only regular commits.

* Signed commits are not properly handled. It's not clear what to do
  to such commits when replaying on the server side.

* Notes associated with replayed commits are not updated nor
  duplicated. (Thanks to Phillip Wood for noticing.)

# Commit overview

* 1/14 t6429: remove switching aspects of fast-rebase

    Preparatory commit to make it easier to later replace the
    fast-rebase test-tool by `git replay` without breaking existing
    tests.

* 2/14 replay: introduce new builtin

    This creates a minimal `git replay` command by moving the code
    from the `fast-rebase` test helper from `t/helper/` into
    `builtin/` and doing some renames and a few other needed changes.
    Since v5, this also introduces stub documentation and tests for
    the new command as suggested by Dscho. Also in the doc,
    "EXPERIMENTAL" has been added both to the name line and the
    synopsis line of the command, which was also suggested by
    Dscho. This required to also change the "usage" message of the
    command.

* - 3/14 replay: start using parse_options API
  - 4/14 replay: die() instead of failing assert()
  - 5/14 replay: introduce pick_regular_commit()
  - 6/14 replay: change rev walking options
  - 7/14 replay: add an important FIXME comment about gpg signing
  - 8/14 replay: remove progress and info output
  - 9/14 replay: remove HEAD related sanity check

    These slowly change the command to make it behave more like
    regular commands and to start cleaning up its output. In patch
    6/14 (replay: change rev walking options) there are some changes
    compared to v5 as suggested by Elijah and Dscho. First we are now
    warning() in case a command line option would change some rev
    options that we are going to override. Second we don't let the
    `--reverse` option change the way the command works.

* 10/14 replay: make it a minimal server side command

    After the cleaning up in previous commits, it's now time to
    radically change the way it works by stopping it to do ref
    updates, to update the index and worktree, to consider HEAD as
    special. Instead just make it output commands that should be
    passed to `git update-ref --stdin`. A few test changes are new in
    this commit since v5.

* 11/14 replay: use standard revision ranges

    Start addind new interesting features and also documentation and
    tests, as the interface of the command is cristalizing into its
    final form. Since v5 this patch is not introducing the
    documentation and the tests of the new command anymore (as they
    are introduced in patch 2/14).

* - 12/14 replay: add --advance or 'cherry-pick' mode
  - 13/14 replay: add --contained to rebase contained branches

    Add new options and features to the command.

* 14/14 replay: stop assuming replayed branches do not diverge

    This adds another interesting feature, as well as related
    documentation and tests.

# Notes about `fast-rebase`, tests and documentation

The `fast-rebase` test-tool helper was developed by Elijah to
experiment with a rebasing tool that would be developed from scratch
based on his merge-ort work, could be used to test that merge-ort
work, and would not have the speed and interface limitations of `git
rebase` or `git cherry-pick`.

This `fast-rebase` helper was used before this series in:

t6429-merge-sequence-rename-caching.sh

So when `git replay` is created from `fast-rebase` in patch 2/14, the
t6429 test script is also converted to use `git replay`. This ensures
that `git replay` doesn't break too badly during the first 10 patches
in this patch series.

Tests and documentation are introduced specifically for `git replay`
as soon as patch 2/14, but they are not much improved since around
11/14 as it doesn't make much sense to document and test behavior that
we know is going to change soon.

# Possibly controversial issues 

* bare or not bare: this series works towards a plumbing command with
  the end goal of it being usable and used first on bare repos,
  contrary to existing commands like `git rebase` and `git
  cherry-pick`. The tests check that the command works on both bare
  and non-bare repo though.

* exit status: a successful, non-conflicted replay exits with code
  0. When the replay has conflicts, the exit status is 1. If the
  replay is not able to complete (or start) due to some kind of error,
  the exit status is something other than 0 or 1. There are a few
  tests checking that. It has been suggested in an internal review
  that conflicts might want to get a more specific error code as an
  error code of 1 might be quite easy to return by accident. It
  doesn't seem to me from their docs (which might want to be improved,
  I didn't look at the code) that other commands like `git merge` and
  `git rebase` exit with a special error code in case of conflict.

* make worktree and index changes optional: commit 10/14 stops
  updating the index and worktree, but it might be better especially
  for cli users to make that optional. The issue is that this would
  make the command more complex while we are developing a number of
  important features so that the command can be used on bare repos. It
  seems that this should rather be done in an iterative improvement
  after the important features have landed.

* --advance and --contained: these two advanced options might not
  belong to this first series and could perhaps be added in a followup
  series in separate commits. On the other hand the code for
  --contained seems involved with the code of --advance and it's nice
  to see soon that git replay can indeed do cherry-picking and rebase
  many refs at once, and this way fullfil these parts of its promise.

* replaying diverging branches: 14/14 the last patch in the series,
  which allow replaying diverging branches, can be seen as a
  fundamental fix or alternatively as adding an interesting
  feature. So it's debatable if it should be in its own patch along
  with its own tests as in this series, or if it should be merged into
  a previous patch and which one.

* only 2 patches: this patch series can be seen from a high level
  point of view as 1) introducing the new `git replay` command, and 2)
  using `git replay` to replace, and get rid of, the fast-rebase
  test-tool command. The fact that not much of the original
  fast-rebase code and interface is left would agree with that point
  of view. On the other hand, fast-rebase can also be seen as a first
  iteration towards `git replay`. So it can also make sense to see how
  `git replay` evolved from it.

# Changes between v5 and v6

Thanks to Dscho, Linus Arver and Dragan Simic for their suggestions on
the previous version! The few changes compared to v5 are:

* The patch series was rebased onto master at 2e8e77cbac (The
  twenty-first batch, 2023-10-23). This is to fix small conflicts
  with recently merged series, especially
  cc/repack-sift-filtered-objects-to-separate-pack.

* In patch 2/14 (replay: introduce new builtin), stub documentation
  and tests have been introduced as suggested by Dscho. They were
  introduced later (in patch 11/14) in v5.

* Also in patch 2/14, "EXPERIMENTAL" has been added both to the name
  line and the synopsis line of the command in the doc as suggested by
  Dscho.

* In patch 6/14 (replay: change rev walking options) we are now
  warning() in case a command line option would change some rev
  options that we are going to override. This follows discussions
  between Dscho and Elijah where they agreed that properly taking care
  of handling these command line options should be dealt with later in
  future patch series.

* Also in patch 6/14, we don't let the `--reverse` option change the
  way the command works. This also follows discussions between Dscho
  and Elijah about command line options related to rev walking.

* In patch 9/14 (replay: remove HEAD related sanity check) we can
  simplify a test a bit.

* In patch 10/14 (replay: make it a minimal server side command) we
  can further add tests.

* In patch 11/14 (replay: use standard revision ranges), as
  documentation and tests are not introduced by this commit anymore,
  but earlier by patch 2/14, fewer doc and test changes are made in
  this patch.

CI tests seem to pass according to:

https://github.com/chriscool/git/actions/runs/6732523160/

(A "Run ci/install-dependencies.sh" test on osx-clang initially
failed, but it worked when I just re-ran it. Hopefully this will alow
tests that couldn't be run to proceed and succeed, but I don't want to
wait for that right now.)

# Range-diff between v5 and v6

(Sorry it's very long mostly due to doc and test changes over a number
of patches.)

 1:  72c34a0eba =  1:  fac0a9dff4 t6429: remove switching aspects of fast-rebase
 2:  f85e6c823c !  2:  8a605ddef8 replay: introduce new builtin
    @@ Commit message
         For now, this is just a rename from `t/helper/test-fast-rebase.c` into
         `builtin/replay.c` with minimal changes to make it build appropriately.
     
    +    There is a stub documentation and a stub test script though.
    +
         Subsequent commits will flesh out its capabilities and make it a more
         standard regular builtin.
     
    @@ .gitignore
      /git-rerere
      /git-reset
     
    + ## Documentation/git-replay.txt (new) ##
    +@@
    ++git-replay(1)
    ++=============
    ++
    ++NAME
    ++----
    ++git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos too
    ++
    ++
    ++SYNOPSIS
    ++--------
    ++[verse]
    ++'git replay' --onto <newbase> <revision-range>... # EXPERIMENTAL
    ++
    ++DESCRIPTION
    ++-----------
    ++
    ++Takes a range of commits and replays them onto a new location.
    ++
    ++THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
    ++
    ++OPTIONS
    ++-------
    ++
    ++--onto <newbase>::
    ++  Starting point at which to create the new commits.  May be any
    ++  valid commit, and not just an existing branch name.
    ++
    ++EXIT STATUS
    ++-----------
    ++
    ++For a successful, non-conflicted replay, the exit status is 0.  When
    ++the replay has conflicts, the exit status is 1.  If the replay is not
    ++able to complete (or start) due to some kind of error, the exit status
    ++is something other than 0 or 1.
    ++
    ++GIT
    ++---
    ++Part of the linkgit:git[1] suite
    +
      ## Makefile ##
     @@ Makefile: TEST_BUILTINS_OBJS += test-dump-split-index.o
      TEST_BUILTINS_OBJS += test-dump-untracked-cache.o
      TEST_BUILTINS_OBJS += test-env-helper.o
      TEST_BUILTINS_OBJS += test-example-decorate.o
     -TEST_BUILTINS_OBJS += test-fast-rebase.o
    + TEST_BUILTINS_OBJS += test-find-pack.o
      TEST_BUILTINS_OBJS += test-fsmonitor-client.o
      TEST_BUILTINS_OBJS += test-genrandom.o
    - TEST_BUILTINS_OBJS += test-genzeros.o
     @@ Makefile: BUILTIN_OBJS += builtin/remote-fd.o
      BUILTIN_OBJS += builtin/remote.o
      BUILTIN_OBJS += builtin/repack.o
    @@ t/helper/test-tool.c: static struct test_cmd cmds[] = {
        { "env-helper", cmd__env_helper },
        { "example-decorate", cmd__example_decorate },
     -  { "fast-rebase", cmd__fast_rebase },
    +   { "find-pack", cmd__find_pack },
        { "fsmonitor-client", cmd__fsmonitor_client },
        { "genrandom", cmd__genrandom },
    -   { "genzeros", cmd__genzeros },
     
      ## t/helper/test-tool.h ##
     @@ t/helper/test-tool.h: int cmd__dump_untracked_cache(int argc, const char **argv);
    @@ t/helper/test-tool.h: int cmd__dump_untracked_cache(int argc, const char **argv)
      int cmd__env_helper(int argc, const char **argv);
      int cmd__example_decorate(int argc, const char **argv);
     -int cmd__fast_rebase(int argc, const char **argv);
    + int cmd__find_pack(int argc, const char **argv);
      int cmd__fsmonitor_client(int argc, const char **argv);
      int cmd__genrandom(int argc, const char **argv);
    - int cmd__genzeros(int argc, const char **argv);
    +
    + ## t/t3650-replay-basics.sh (new) ##
    +@@
    ++#!/bin/sh
    ++
    ++test_description='basic git replay tests'
    ++
    ++GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
    ++export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
    ++
    ++. ./test-lib.sh
    ++
    ++GIT_AUTHOR_NAME=author@name
    ++GIT_AUTHOR_EMAIL=bogus@email@address
    ++export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL
    ++
    ++test_expect_success 'setup' '
    ++  test_commit A &&
    ++  test_commit B &&
    ++
    ++  git switch -c topic1 &&
    ++  test_commit C &&
    ++  git switch -c topic2 &&
    ++  test_commit D &&
    ++  test_commit E &&
    ++  git switch topic1 &&
    ++  test_commit F &&
    ++  git switch -c topic3 &&
    ++  test_commit G &&
    ++  test_commit H &&
    ++  git switch -c topic4 main &&
    ++  test_commit I &&
    ++  test_commit J &&
    ++
    ++  git switch -c next main &&
    ++  test_commit K &&
    ++  git merge -m "Merge topic1" topic1 &&
    ++  git merge -m "Merge topic2" topic2 &&
    ++  git merge -m "Merge topic3" topic3 &&
    ++  >evil &&
    ++  git add evil &&
    ++  git commit --amend &&
    ++  git merge -m "Merge topic4" topic4 &&
    ++
    ++  git switch main &&
    ++  test_commit L &&
    ++  test_commit M &&
    ++
    ++  git switch -c conflict B &&
    ++  test_commit C.conflict C.t conflict
    ++'
    ++
    ++test_expect_success 'using replay to rebase two branches, one on top of other' '
    ++  git switch main &&
    ++
    ++  git replay --onto main topic1 topic2 >result &&
    ++
    ++  git log --format=%s $(cut -f 3 -d " " result) >actual &&
    ++  test_write_lines E D M L B A >expect &&
    ++  test_cmp expect actual
    ++'
    ++
    ++test_done
     
      ## t/t6429-merge-sequence-rename-caching.sh ##
     @@ t/t6429-merge-sequence-rename-caching.sh: test_expect_success 'caching renames does not preclude finding new ones' '
 3:  11abb9d120 =  3:  a7886952b7 replay: start using parse_options API
 4:  9e568eae84 =  4:  bab0286f0a replay: die() instead of failing assert()
 5:  e7ebf3c5ef =  5:  469e95e5bf replay: introduce pick_regular_commit()
 6:  37d545d5d6 !  6:  edafe4846f replay: change rev walking options
    @@ Commit message
     
         Let's set the rev walking options we need after calling
         setup_revisions() instead of before. This enforces options we always
    -    want.
    -
    -    We want the command to work from older commits to newer ones by default,
    -    but we are Ok with letting users reverse that, using --reverse, if that's
    -    what they really want.
    +    want for now.
     
    +    We want the command to work from older commits to newer ones by default.
         Also we don't want history simplification, as we want to deal with all
         the commits in the affected range.
     
    +    When we see an option that we are going to override, we emit a warning
    +    to avoid confusion as much as possible though.
    +
         Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
         Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
         Signed-off-by: Elijah Newren <newren@gmail.com>
    @@ Commit message
     
      ## builtin/replay.c ##
     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix)
    +   struct merge_result result;
    +   struct strbuf reflog_msg = STRBUF_INIT;
    +   struct strbuf branch_name = STRBUF_INIT;
    +-  int ret = 0;
    ++  int i, ret = 0;
    + 
    +   const char * const replay_usage[] = {
    +           N_("git replay --onto <newbase> <oldbase> <branch>"),
    +@@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix)
      
        repo_init_revisions(the_repository, &revs, prefix);
      
    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
     -
        strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
      
    ++  /*
    ++   * TODO: For now, let's warn when we see an option that we are
    ++   * going to override after setup_revisions() below. In the
    ++   * future we might want to either die() or allow them if we
    ++   * think they could be useful though.
    ++   */
    ++  for (i = 0; i < argc; i++) {
    ++          if (!strcmp(argv[i], "--reverse") || !strcmp(argv[i], "--date-order") ||
    ++              !strcmp(argv[i], "--topo-order") || !strcmp(argv[i], "--author-date-order") ||
    ++              !strcmp(argv[i], "--full-history"))
    ++                  warning(_("option '%s' will be overridden"), argv[i]);
    ++  }
    ++
        if (setup_revisions(rev_walk_args.nr, rev_walk_args.v, &revs, NULL) > 1) {
    -@@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix)
    +           ret = error(_("unhandled options"));
                goto cleanup;
        }
      
     +  /* requirements/overrides for revs */
    -+  revs.reverse = !revs.reverse;
    ++  revs.reverse = 1;
     +  revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
     +  revs.topo_order = 1;
     +  revs.simplify_history = 0;
 7:  2943f08926 =  7:  b81574744a replay: add an important FIXME comment about gpg signing
 8:  f81962ba41 =  8:  b08ad2b2f1 replay: remove progress and info output
 9:  236747497e !  9:  5099c94d2e replay: remove HEAD related sanity check
    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
                               REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
                        error(_("could not update %s"), argv[2]);
                        die("Failed to update %s", argv[2]);
    +
    + ## t/t3650-replay-basics.sh ##
    +@@ t/t3650-replay-basics.sh: test_expect_success 'setup' '
    + '
    + 
    + test_expect_success 'using replay to rebase two branches, one on top of other' '
    +-  git switch main &&
    +-
    +   git replay --onto main topic1 topic2 >result &&
    + 
    +   git log --format=%s $(cut -f 3 -d " " result) >actual &&
10:  3374d5be23 ! 10:  b4a5d1edd4 replay: make it a minimal server side command
    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
        struct merge_result result;
     -  struct strbuf reflog_msg = STRBUF_INIT;
        struct strbuf branch_name = STRBUF_INIT;
    -   int ret = 0;
    +   int i, ret = 0;
      
     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix)
        onto = peel_committish(onto_name);
    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
     +  return ret ? 0 : 1;
      }
     
    + ## t/t3650-replay-basics.sh ##
    +@@ t/t3650-replay-basics.sh: test_expect_success 'setup' '
    +   test_commit C.conflict C.t conflict
    + '
    + 
    ++test_expect_success 'setup bare' '
    ++  git clone --bare . bare
    ++'
    ++
    + test_expect_success 'using replay to rebase two branches, one on top of other' '
    +   git replay --onto main topic1 topic2 >result &&
    + 
    ++  test_line_count = 1 result &&
    ++
    +   git log --format=%s $(cut -f 3 -d " " result) >actual &&
    +   test_write_lines E D M L B A >expect &&
    +-  test_cmp expect actual
    ++  test_cmp expect actual &&
    ++
    ++  printf "update refs/heads/topic2 " >expect &&
    ++  printf "%s " $(cut -f 3 -d " " result) >>expect &&
    ++  git rev-parse topic2 >>expect &&
    ++
    ++  test_cmp expect result
    ++'
    ++
    ++test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '
    ++  git -C bare replay --onto main topic1 topic2 >result-bare &&
    ++  test_cmp expect result-bare
    + '
    + 
    + test_done
    +
      ## t/t6429-merge-sequence-rename-caching.sh ##
     @@ t/t6429-merge-sequence-rename-caching.sh: test_expect_success 'caching renames does not preclude finding new ones' '
      
11:  197d076a93 ! 11:  1d8a6ef432 replay: use standard revision ranges
    @@ Commit message
         history, as they could be confusing for most users.
     
         Also as the interface of the command is now mostly finalized,
    -    we can add some documentation as well as testcases to make sure
    +    we can add more documentation and more testcases to make sure
         the command will continue to work as designed in the future.
     
         We only document the rev-list related options among all the
    @@ Commit message
         Signed-off-by: Elijah Newren <newren@gmail.com>
         Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
     
    - ## Documentation/git-replay.txt (new) ##
    -@@
    -+git-replay(1)
    -+=============
    -+
    -+NAME
    -+----
    -+git-replay - Replay commits on a new base, works on bare repos too
    -+
    -+
    -+SYNOPSIS
    -+--------
    -+[verse]
    -+'git replay' --onto <newbase> <revision-range>...
    -+
    -+DESCRIPTION
    -+-----------
    -+
    + ## Documentation/git-replay.txt ##
    +@@ Documentation/git-replay.txt: SYNOPSIS
    + DESCRIPTION
    + -----------
    + 
    +-Takes a range of commits and replays them onto a new location.
     +Takes a range of commits and replays them onto a new location. Leaves
     +the working tree and the index untouched, and updates no
     +references. The output of this command is meant to be used as input to
     +`git update-ref --stdin`, which would update the relevant branches
     +(see the OUTPUT section below).
    -+
    -+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
    -+
    -+OPTIONS
    -+-------
    -+
    -+--onto <newbase>::
    -+  Starting point at which to create the new commits.  May be any
    -+  valid commit, and not just an existing branch name.
    + 
    + THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
    + 
    +@@ Documentation/git-replay.txt: OPTIONS
    + --onto <newbase>::
    +   Starting point at which to create the new commits.  May be any
    +   valid commit, and not just an existing branch name.
     ++
     +The update-ref command(s) in the output will update the branch(es) in
     +the revision range to point at the new commits, similar to the way how
    @@ Documentation/git-replay.txt (new)
     +
     +where the number of refs updated depends on the arguments passed and
     +the shape of the history being replayed.
    -+
    -+EXIT STATUS
    -+-----------
    -+
    -+For a successful, non-conflicted replay, the exit status is 0.  When
    -+the replay has conflicts, the exit status is 1.  If the replay is not
    -+able to complete (or start) due to some kind of error, the exit status
    -+is something other than 0 or 1.
    -+
    + 
    + EXIT STATUS
    + -----------
    +@@ Documentation/git-replay.txt: the replay has conflicts, the exit status is 1.  If the replay is not
    + able to complete (or start) due to some kind of error, the exit status
    + is something other than 0 or 1.
    + 
     +EXAMPLES
     +--------
     +
    @@ Documentation/git-replay.txt (new)
     +`origin/main`. These three branches may have commits on top of `base`
     +that they have in common, but that does not need to be the case.
     +
    -+GIT
    -+---
    -+Part of the linkgit:git[1] suite
    + GIT
    + ---
    + Part of the linkgit:git[1] suite
     
      ## builtin/replay.c ##
     @@
    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
        struct merge_options merge_opt;
        struct merge_result result;
     -  struct strbuf branch_name = STRBUF_INIT;
    -   int ret = 0;
    +   int i, ret = 0;
      
        const char * const replay_usage[] = {
     -          N_("git replay --onto <newbase> <oldbase> <branch>"),
    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
      
     -  strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
     -
    +   /*
    +    * TODO: For now, let's warn when we see an option that we are
    +    * going to override after setup_revisions() below. In the
    +@@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix)
    +                   warning(_("option '%s' will be overridden"), argv[i]);
    +   }
    + 
     -  if (setup_revisions(rev_walk_args.nr, rev_walk_args.v, &revs, NULL) > 1) {
     -          ret = error(_("unhandled options"));
     +  argc = setup_revisions(argc, argv, &revs, NULL);
    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
      
        /* Return */
     
    - ## t/t3650-replay-basics.sh (new) ##
    -@@
    -+#!/bin/sh
    -+
    -+test_description='basic git replay tests'
    -+
    -+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
    -+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
    -+
    -+. ./test-lib.sh
    -+
    -+GIT_AUTHOR_NAME=author@name
    -+GIT_AUTHOR_EMAIL=bogus@email@address
    -+export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL
    -+
    -+test_expect_success 'setup' '
    -+  test_commit A &&
    -+  test_commit B &&
    -+
    -+  git switch -c topic1 &&
    -+  test_commit C &&
    -+  git switch -c topic2 &&
    -+  test_commit D &&
    -+  test_commit E &&
    -+  git switch topic1 &&
    -+  test_commit F &&
    -+  git switch -c topic3 &&
    -+  test_commit G &&
    -+  test_commit H &&
    -+  git switch -c topic4 main &&
    -+  test_commit I &&
    -+  test_commit J &&
    -+
    -+  git switch -c next main &&
    -+  test_commit K &&
    -+  git merge -m "Merge topic1" topic1 &&
    -+  git merge -m "Merge topic2" topic2 &&
    -+  git merge -m "Merge topic3" topic3 &&
    -+  >evil &&
    -+  git add evil &&
    -+  git commit --amend &&
    -+  git merge -m "Merge topic4" topic4 &&
    -+
    -+  git switch main &&
    -+  test_commit L &&
    -+  test_commit M &&
    -+
    -+  git switch -c conflict B &&
    -+  test_commit C.conflict C.t conflict
    -+'
    -+
    -+test_expect_success 'setup bare' '
    -+  git clone --bare . bare
    -+'
    -+
    -+test_expect_success 'using replay to rebase two branches, one on top of other' '
    + ## t/t3650-replay-basics.sh ##
    +@@ t/t3650-replay-basics.sh: test_expect_success 'setup bare' '
    + '
    + 
    + test_expect_success 'using replay to rebase two branches, one on top of other' '
    +-  git replay --onto main topic1 topic2 >result &&
     +  git replay --onto main topic1..topic2 >result &&
    -+
    -+  test_line_count = 1 result &&
    -+
    -+  git log --format=%s $(cut -f 3 -d " " result) >actual &&
    -+  test_write_lines E D M L B A >expect &&
    -+  test_cmp expect actual &&
    -+
    -+  printf "update refs/heads/topic2 " >expect &&
    -+  printf "%s " $(cut -f 3 -d " " result) >>expect &&
    -+  git rev-parse topic2 >>expect &&
    -+
    -+  test_cmp expect result
    -+'
    -+
    -+test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '
    + 
    +   test_line_count = 1 result &&
    + 
    +@@ t/t3650-replay-basics.sh: test_expect_success 'using replay to rebase two branches, one on top of other' '
    + '
    + 
    + test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '
    +-  git -C bare replay --onto main topic1 topic2 >result-bare &&
     +  git -C bare replay --onto main topic1..topic2 >result-bare &&
    -+  test_cmp expect result-bare
    -+'
    -+
    +   test_cmp expect result-bare
    + '
    + 
     +test_expect_success 'using replay to rebase with a conflict' '
     +  test_expect_code 1 git replay --onto topic1 B..conflict
     +'
    @@ t/t3650-replay-basics.sh (new)
     +  test_expect_code 1 git -C bare replay --onto topic1 B..conflict
     +'
     +
    -+test_done
    + test_done
     
      ## t/t6429-merge-sequence-rename-caching.sh ##
     @@ t/t6429-merge-sequence-rename-caching.sh: test_expect_success 'caching renames does not preclude finding new ones' '
12:  e52d8b961c ! 12:  3a207e5d7f replay: add --advance or 'cherry-pick' mode
    @@ Commit message
         Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
     
      ## Documentation/git-replay.txt ##
    -@@ Documentation/git-replay.txt: git-replay - Replay commits on a new base, works on bare repos too
    +@@ Documentation/git-replay.txt: git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
      SYNOPSIS
      --------
      [verse]
    --'git replay' --onto <newbase> <revision-range>...
    -+'git replay' (--onto <newbase> | --advance <branch>) <revision-range>...
    +-'git replay' --onto <newbase> <revision-range>... # EXPERIMENTAL
    ++'git replay' (--onto <newbase> | --advance <branch>) <revision-range>... # EXPERIMENTAL
      
      DESCRIPTION
      -----------
    @@ builtin/replay.c: static struct commit *pick_regular_commit(struct commit *pickm
        struct merge_options merge_opt;
        struct merge_result result;
     +  struct strset *update_refs = NULL;
    -   int ret = 0;
    +   int i, ret = 0;
      
        const char * const replay_usage[] = {
     -          N_("git replay --onto <newbase> <revision-range>..."),
    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
     -
        repo_init_revisions(the_repository, &revs, prefix);
      
    -   argc = setup_revisions(argc, argv, &revs, NULL);
    +   /*
     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix)
        revs.topo_order = 1;
        revs.simplify_history = 0;
13:  fc79a930b5 ! 13:  23a164196b replay: add --contained to rebase contained branches
    @@ Commit message
         Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
     
      ## Documentation/git-replay.txt ##
    -@@ Documentation/git-replay.txt: git-replay - Replay commits on a new base, works on bare repos too
    +@@ Documentation/git-replay.txt: git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
      SYNOPSIS
      --------
      [verse]
    --'git replay' (--onto <newbase> | --advance <branch>) <revision-range>...
    -+'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...
    +-'git replay' (--onto <newbase> | --advance <branch>) <revision-range>... # EXPERIMENTAL
    ++'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>... # EXPERIMENTAL
      
      DESCRIPTION
      -----------
    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
        struct rev_info revs;
        struct commit *last_commit = NULL;
     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix)
    -   int ret = 0;
    +   int i, ret = 0;
      
        const char * const replay_usage[] = {
     -          N_("git replay (--onto <newbase> | --advance <branch>) <revision-range>..."),
    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
     +
        repo_init_revisions(the_repository, &revs, prefix);
      
    -   argc = setup_revisions(argc, argv, &revs, NULL);
    +   /*
     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix)
                        continue;
                while (decoration) {
14:  1160ff54e6 ! 14:  5c4b15cccd replay: stop assuming replayed branches do not diverge
    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
        struct merge_result result;
        struct strset *update_refs = NULL;
     +  kh_oid_map_t *replayed_commits;
    -   int ret = 0;
    +   int i, ret = 0;
      
        const char * const replay_usage[] = {
     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix)


Elijah Newren (14):
  t6429: remove switching aspects of fast-rebase
  replay: introduce new builtin
  replay: start using parse_options API
  replay: die() instead of failing assert()
  replay: introduce pick_regular_commit()
  replay: change rev walking options
  replay: add an important FIXME comment about gpg signing
  replay: remove progress and info output
  replay: remove HEAD related sanity check
  replay: make it a minimal server side command
  replay: use standard revision ranges
  replay: add --advance or 'cherry-pick' mode
  replay: add --contained to rebase contained branches
  replay: stop assuming replayed branches do not diverge

 .gitignore                               |   1 +
 Documentation/git-replay.txt             | 127 +++++++
 Makefile                                 |   2 +-
 builtin.h                                |   1 +
 builtin/replay.c                         | 421 +++++++++++++++++++++++
 command-list.txt                         |   1 +
 git.c                                    |   1 +
 t/helper/test-fast-rebase.c              | 241 -------------
 t/helper/test-tool.c                     |   1 -
 t/helper/test-tool.h                     |   1 -
 t/t3650-replay-basics.sh                 | 198 +++++++++++
 t/t6429-merge-sequence-rename-caching.sh |  45 +--
 12 files changed, 776 insertions(+), 264 deletions(-)
 create mode 100644 Documentation/git-replay.txt
 create mode 100644 builtin/replay.c
 delete mode 100644 t/helper/test-fast-rebase.c
 create mode 100755 t/t3650-replay-basics.sh


base-commit: 2e8e77cbac8ac17f94eee2087187fa1718e38b14
-- 
2.42.0.496.g529a7fda40


^ permalink raw reply	[flat|nested] 208+ messages in thread

* [PATCH v6 01/14] t6429: remove switching aspects of fast-rebase
  2023-11-02 13:51         ` [PATCH v6 " Christian Couder
@ 2023-11-02 13:51           ` Christian Couder
  2023-11-02 13:51           ` [PATCH v6 02/14] replay: introduce new builtin Christian Couder
                             ` (15 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-02 13:51 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

At the time t6429 was written, merge-ort was still under development,
did not have quite as many tests, and certainly was not widely deployed.
Since t6429 was exercising some codepaths just a little differently, we
thought having them also test the "merge_switch_to_result()" bits of
merge-ort was useful even though they weren't intrinsic to the real
point of these tests.

However, the value provided by doing extra testing of the
"merge_switch_to_result()" bits has decreased a bit over time, and it's
actively making it harder to refactor `test-tool fast-rebase` into `git
replay`, which we are going to do in following commits.  Dispense with
these bits.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 t/helper/test-fast-rebase.c              | 9 +--------
 t/t6429-merge-sequence-rename-caching.sh | 9 +++++++--
 2 files changed, 8 insertions(+), 10 deletions(-)

diff --git a/t/helper/test-fast-rebase.c b/t/helper/test-fast-rebase.c
index cac20a72b3..2bfab66b1b 100644
--- a/t/helper/test-fast-rebase.c
+++ b/t/helper/test-fast-rebase.c
@@ -194,7 +194,7 @@ int cmd__fast_rebase(int argc, const char **argv)
 		last_commit = create_commit(result.tree, commit, last_commit);
 	}
 
-	merge_switch_to_result(&merge_opt, head_tree, &result, 1, !result.clean);
+	merge_finalize(&merge_opt, &result);
 
 	if (result.clean < 0)
 		exit(128);
@@ -213,9 +213,6 @@ int cmd__fast_rebase(int argc, const char **argv)
 		}
 		if (create_symref("HEAD", branch_name.buf, reflog_msg.buf) < 0)
 			die(_("unable to update HEAD"));
-
-		prime_cache_tree(the_repository, the_repository->index,
-				 result.tree);
 	} else {
 		fprintf(stderr, "\nAborting: Hit a conflict.\n");
 		strbuf_addf(&reflog_msg, "rebase progress up to %s",
@@ -228,10 +225,6 @@ int cmd__fast_rebase(int argc, const char **argv)
 			die("Failed to update %s", argv[4]);
 		}
 	}
-	if (write_locked_index(&the_index, &lock,
-			       COMMIT_LOCK | SKIP_IF_UNCHANGED))
-		die(_("unable to write %s"), get_index_file());
-
 	ret = (result.clean == 0);
 cleanup:
 	strbuf_release(&reflog_msg);
diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh
index d02fa16614..75d3fd2dba 100755
--- a/t/t6429-merge-sequence-rename-caching.sh
+++ b/t/t6429-merge-sequence-rename-caching.sh
@@ -72,6 +72,7 @@ test_expect_success 'caching renames does not preclude finding new ones' '
 		git switch upstream &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream~1..topic
 
 		git ls-files >tracked-files &&
@@ -200,6 +201,7 @@ test_expect_success 'rename same file identically, then reintroduce it' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream~1..topic &&
 
 		git ls-files >tracked &&
@@ -277,6 +279,7 @@ test_expect_success 'rename same file identically, then add file to old dir' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream~1..topic &&
 
 		git ls-files >tracked &&
@@ -356,8 +359,6 @@ test_expect_success 'cached dir rename does not prevent noticing later conflict'
 		test_must_fail test-tool fast-rebase --onto HEAD upstream~1 topic >output &&
 		#git cherry-pick upstream..topic &&
 
-		grep CONFLICT..rename/rename output &&
-
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls
 	)
@@ -456,6 +457,7 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
@@ -522,6 +524,7 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
@@ -624,6 +627,7 @@ test_expect_success 'caching renames only on upstream side, part 1' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
@@ -682,6 +686,7 @@ test_expect_success 'caching renames only on upstream side, part 2' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
-- 
2.42.0.496.g529a7fda40


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v6 02/14] replay: introduce new builtin
  2023-11-02 13:51         ` [PATCH v6 " Christian Couder
  2023-11-02 13:51           ` [PATCH v6 01/14] t6429: remove switching aspects of fast-rebase Christian Couder
@ 2023-11-02 13:51           ` Christian Couder
  2023-11-02 13:51           ` [PATCH v6 03/14] replay: start using parse_options API Christian Couder
                             ` (14 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-02 13:51 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

For now, this is just a rename from `t/helper/test-fast-rebase.c` into
`builtin/replay.c` with minimal changes to make it build appropriately.

Let's add a stub documentation and a stub test script though.

Subsequent commits will flesh out the capabilities of the new command
and make it a more standard regular builtin.

Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 .gitignore                                    |  1 +
 Documentation/git-replay.txt                  | 38 ++++++++++++
 Makefile                                      |  2 +-
 builtin.h                                     |  1 +
 .../test-fast-rebase.c => builtin/replay.c    | 29 +++------
 command-list.txt                              |  1 +
 git.c                                         |  1 +
 t/helper/test-tool.c                          |  1 -
 t/helper/test-tool.h                          |  1 -
 t/t3650-replay-basics.sh                      | 60 +++++++++++++++++++
 t/t6429-merge-sequence-rename-caching.sh      | 27 +++------
 11 files changed, 121 insertions(+), 41 deletions(-)
 create mode 100644 Documentation/git-replay.txt
 rename t/helper/test-fast-rebase.c => builtin/replay.c (87%)
 create mode 100755 t/t3650-replay-basics.sh

diff --git a/.gitignore b/.gitignore
index 5e56e471b3..612c0f6a0f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -135,6 +135,7 @@
 /git-remote-ext
 /git-repack
 /git-replace
+/git-replay
 /git-request-pull
 /git-rerere
 /git-reset
diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
new file mode 100644
index 0000000000..44bf584fed
--- /dev/null
+++ b/Documentation/git-replay.txt
@@ -0,0 +1,38 @@
+git-replay(1)
+=============
+
+NAME
+----
+git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos too
+
+
+SYNOPSIS
+--------
+[verse]
+'git replay' --onto <newbase> <oldbase> <branch> # EXPERIMENTAL
+
+DESCRIPTION
+-----------
+
+Takes a range of commits and replays them onto a new location.
+
+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
+
+OPTIONS
+-------
+
+--onto <newbase>::
+	Starting point at which to create the new commits.  May be any
+	valid commit, and not just an existing branch name.
+
+EXIT STATUS
+-----------
+
+For a successful, non-conflicted replay, the exit status is 0.  When
+the replay has conflicts, the exit status is 1.  If the replay is not
+able to complete (or start) due to some kind of error, the exit status
+is something other than 0 or 1.
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Makefile b/Makefile
index 03adcb5a48..3834bc1544 100644
--- a/Makefile
+++ b/Makefile
@@ -799,7 +799,6 @@ TEST_BUILTINS_OBJS += test-dump-split-index.o
 TEST_BUILTINS_OBJS += test-dump-untracked-cache.o
 TEST_BUILTINS_OBJS += test-env-helper.o
 TEST_BUILTINS_OBJS += test-example-decorate.o
-TEST_BUILTINS_OBJS += test-fast-rebase.o
 TEST_BUILTINS_OBJS += test-find-pack.o
 TEST_BUILTINS_OBJS += test-fsmonitor-client.o
 TEST_BUILTINS_OBJS += test-genrandom.o
@@ -1290,6 +1289,7 @@ BUILTIN_OBJS += builtin/remote-fd.o
 BUILTIN_OBJS += builtin/remote.o
 BUILTIN_OBJS += builtin/repack.o
 BUILTIN_OBJS += builtin/replace.o
+BUILTIN_OBJS += builtin/replay.o
 BUILTIN_OBJS += builtin/rerere.o
 BUILTIN_OBJS += builtin/reset.o
 BUILTIN_OBJS += builtin/rev-list.o
diff --git a/builtin.h b/builtin.h
index d560baa661..28280636da 100644
--- a/builtin.h
+++ b/builtin.h
@@ -211,6 +211,7 @@ int cmd_remote(int argc, const char **argv, const char *prefix);
 int cmd_remote_ext(int argc, const char **argv, const char *prefix);
 int cmd_remote_fd(int argc, const char **argv, const char *prefix);
 int cmd_repack(int argc, const char **argv, const char *prefix);
+int cmd_replay(int argc, const char **argv, const char *prefix);
 int cmd_rerere(int argc, const char **argv, const char *prefix);
 int cmd_reset(int argc, const char **argv, const char *prefix);
 int cmd_restore(int argc, const char **argv, const char *prefix);
diff --git a/t/helper/test-fast-rebase.c b/builtin/replay.c
similarity index 87%
rename from t/helper/test-fast-rebase.c
rename to builtin/replay.c
index 2bfab66b1b..f2d8444417 100644
--- a/t/helper/test-fast-rebase.c
+++ b/builtin/replay.c
@@ -1,17 +1,11 @@
 /*
- * "git fast-rebase" builtin command
- *
- * FAST: Forking Any Subprocesses (is) Taboo
- *
- * This is meant SOLELY as a demo of what is possible.  sequencer.c and
- * rebase.c should be refactored to use the ideas here, rather than attempting
- * to extend this file to replace those (unless Phillip or Dscho say that
- * refactoring is too hard and we need a clean slate, but I'm guessing that
- * refactoring is the better route).
+ * "git replay" builtin command
  */
 
 #define USE_THE_INDEX_VARIABLE
-#include "test-tool.h"
+#include "git-compat-util.h"
+
+#include "builtin.h"
 #include "cache-tree.h"
 #include "commit.h"
 #include "environment.h"
@@ -27,7 +21,8 @@
 #include "sequencer.h"
 #include "setup.h"
 #include "strvec.h"
-#include "tree.h"
+#include <oidset.h>
+#include <tree.h>
 
 static const char *short_commit_name(struct commit *commit)
 {
@@ -94,7 +89,7 @@ static struct commit *create_commit(struct tree *tree,
 	return (struct commit *)obj;
 }
 
-int cmd__fast_rebase(int argc, const char **argv)
+int cmd_replay(int argc, const char **argv, const char *prefix)
 {
 	struct commit *onto;
 	struct commit *last_commit = NULL, *last_picked_commit = NULL;
@@ -110,14 +105,8 @@ int cmd__fast_rebase(int argc, const char **argv)
 	struct strbuf branch_name = STRBUF_INIT;
 	int ret = 0;
 
-	/*
-	 * test-tool stuff doesn't set up the git directory by default; need to
-	 * do that manually.
-	 */
-	setup_git_directory();
-
 	if (argc == 2 && !strcmp(argv[1], "-h")) {
-		printf("Sorry, I am not a psychiatrist; I can not give you the help you need.  Oh, you meant usage...\n");
+		printf("git replay --onto <newbase> <oldbase> <branch> # EXPERIMENTAL\n");
 		exit(129);
 	}
 
@@ -136,7 +125,7 @@ int cmd__fast_rebase(int argc, const char **argv)
 	if (repo_read_index(the_repository) < 0)
 		BUG("Could not read index");
 
-	repo_init_revisions(the_repository, &revs, NULL);
+	repo_init_revisions(the_repository, &revs, prefix);
 	revs.verbose_header = 1;
 	revs.max_parents = 1;
 	revs.cherry_mark = 1;
diff --git a/command-list.txt b/command-list.txt
index 54b2a50f5f..c4cd0f352b 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -160,6 +160,7 @@ git-reflog                              ancillarymanipulators           complete
 git-remote                              ancillarymanipulators           complete
 git-repack                              ancillarymanipulators           complete
 git-replace                             ancillarymanipulators           complete
+git-replay                              plumbingmanipulators
 git-request-pull                        foreignscminterface             complete
 git-rerere                              ancillaryinterrogators
 git-reset                               mainporcelain           history
diff --git a/git.c b/git.c
index c67e44dd82..7068a184b0 100644
--- a/git.c
+++ b/git.c
@@ -594,6 +594,7 @@ static struct cmd_struct commands[] = {
 	{ "remote-fd", cmd_remote_fd, NO_PARSEOPT },
 	{ "repack", cmd_repack, RUN_SETUP },
 	{ "replace", cmd_replace, RUN_SETUP },
+	{ "replay", cmd_replay, RUN_SETUP },
 	{ "rerere", cmd_rerere, RUN_SETUP },
 	{ "reset", cmd_reset, RUN_SETUP },
 	{ "restore", cmd_restore, RUN_SETUP | NEED_WORK_TREE },
diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
index 876cd2dc31..37ba996539 100644
--- a/t/helper/test-tool.c
+++ b/t/helper/test-tool.c
@@ -30,7 +30,6 @@ static struct test_cmd cmds[] = {
 	{ "dump-untracked-cache", cmd__dump_untracked_cache },
 	{ "env-helper", cmd__env_helper },
 	{ "example-decorate", cmd__example_decorate },
-	{ "fast-rebase", cmd__fast_rebase },
 	{ "find-pack", cmd__find_pack },
 	{ "fsmonitor-client", cmd__fsmonitor_client },
 	{ "genrandom", cmd__genrandom },
diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h
index 70dd4eba11..8a1a7c63da 100644
--- a/t/helper/test-tool.h
+++ b/t/helper/test-tool.h
@@ -24,7 +24,6 @@ int cmd__dump_untracked_cache(int argc, const char **argv);
 int cmd__dump_reftable(int argc, const char **argv);
 int cmd__env_helper(int argc, const char **argv);
 int cmd__example_decorate(int argc, const char **argv);
-int cmd__fast_rebase(int argc, const char **argv);
 int cmd__find_pack(int argc, const char **argv);
 int cmd__fsmonitor_client(int argc, const char **argv);
 int cmd__genrandom(int argc, const char **argv);
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
new file mode 100755
index 0000000000..36c1b5082a
--- /dev/null
+++ b/t/t3650-replay-basics.sh
@@ -0,0 +1,60 @@
+#!/bin/sh
+
+test_description='basic git replay tests'
+
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
+. ./test-lib.sh
+
+GIT_AUTHOR_NAME=author@name
+GIT_AUTHOR_EMAIL=bogus@email@address
+export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL
+
+test_expect_success 'setup' '
+	test_commit A &&
+	test_commit B &&
+
+	git switch -c topic1 &&
+	test_commit C &&
+	git switch -c topic2 &&
+	test_commit D &&
+	test_commit E &&
+	git switch topic1 &&
+	test_commit F &&
+	git switch -c topic3 &&
+	test_commit G &&
+	test_commit H &&
+	git switch -c topic4 main &&
+	test_commit I &&
+	test_commit J &&
+
+	git switch -c next main &&
+	test_commit K &&
+	git merge -m "Merge topic1" topic1 &&
+	git merge -m "Merge topic2" topic2 &&
+	git merge -m "Merge topic3" topic3 &&
+	>evil &&
+	git add evil &&
+	git commit --amend &&
+	git merge -m "Merge topic4" topic4 &&
+
+	git switch main &&
+	test_commit L &&
+	test_commit M &&
+
+	git switch -c conflict B &&
+	test_commit C.conflict C.t conflict
+'
+
+test_expect_success 'using replay to rebase two branches, one on top of other' '
+	git switch main &&
+
+	git replay --onto main topic1 topic2 >result &&
+
+	git log --format=%s $(cut -f 3 -d " " result) >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual
+'
+
+test_done
diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh
index 75d3fd2dba..7670b72008 100755
--- a/t/t6429-merge-sequence-rename-caching.sh
+++ b/t/t6429-merge-sequence-rename-caching.sh
@@ -71,9 +71,8 @@ test_expect_success 'caching renames does not preclude finding new ones' '
 
 		git switch upstream &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream~1..topic
 
 		git ls-files >tracked-files &&
 		test_line_count = 2 tracked-files &&
@@ -141,8 +140,7 @@ test_expect_success 'cherry-pick both a commit and its immediate revert' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
-		#git cherry-pick upstream~1..topic &&
+		git replay --onto HEAD upstream~1 topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 1 calls
@@ -200,9 +198,8 @@ test_expect_success 'rename same file identically, then reintroduce it' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream~1..topic &&
 
 		git ls-files >tracked &&
 		test_line_count = 2 tracked &&
@@ -278,9 +275,8 @@ test_expect_success 'rename same file identically, then add file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream~1..topic &&
 
 		git ls-files >tracked &&
 		test_line_count = 4 tracked &&
@@ -356,8 +352,7 @@ test_expect_success 'cached dir rename does not prevent noticing later conflict'
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test_must_fail test-tool fast-rebase --onto HEAD upstream~1 topic >output &&
-		#git cherry-pick upstream..topic &&
+		test_must_fail git replay --onto HEAD upstream~1 topic >output &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls
@@ -456,9 +451,8 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls &&
@@ -523,9 +517,8 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 3 calls &&
@@ -626,9 +619,8 @@ test_expect_success 'caching renames only on upstream side, part 1' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 1 calls &&
@@ -685,9 +677,8 @@ test_expect_success 'caching renames only on upstream side, part 2' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls &&
-- 
2.42.0.496.g529a7fda40


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v6 03/14] replay: start using parse_options API
  2023-11-02 13:51         ` [PATCH v6 " Christian Couder
  2023-11-02 13:51           ` [PATCH v6 01/14] t6429: remove switching aspects of fast-rebase Christian Couder
  2023-11-02 13:51           ` [PATCH v6 02/14] replay: introduce new builtin Christian Couder
@ 2023-11-02 13:51           ` Christian Couder
  2023-11-02 13:51           ` [PATCH v6 04/14] replay: die() instead of failing assert() Christian Couder
                             ` (13 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-02 13:51 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

Instead of manually parsing arguments, let's start using the parse_options
API. This way this new builtin will look more standard, and in some
upcoming commits will more easily be able to handle more command line
options.

Note that we plan to later use standard revision ranges instead of
hardcoded "<oldbase> <branch>" arguments. When we will use standard
revision ranges, it will be easier to check if there are no spurious
arguments if we keep ARGV[0], so let's call parse_options() with
PARSE_OPT_KEEP_ARGV0 even if we don't need ARGV[0] right now to avoid
some useless code churn.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 45 ++++++++++++++++++++++++++++++++-------------
 1 file changed, 32 insertions(+), 13 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index f2d8444417..afabb844d3 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -15,7 +15,7 @@
 #include "lockfile.h"
 #include "merge-ort.h"
 #include "object-name.h"
-#include "read-cache-ll.h"
+#include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
 #include "sequencer.h"
@@ -92,6 +92,7 @@ static struct commit *create_commit(struct tree *tree,
 int cmd_replay(int argc, const char **argv, const char *prefix)
 {
 	struct commit *onto;
+	const char *onto_name = NULL;
 	struct commit *last_commit = NULL, *last_picked_commit = NULL;
 	struct object_id head;
 	struct lock_file lock = LOCK_INIT;
@@ -105,16 +106,32 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct strbuf branch_name = STRBUF_INIT;
 	int ret = 0;
 
-	if (argc == 2 && !strcmp(argv[1], "-h")) {
-		printf("git replay --onto <newbase> <oldbase> <branch> # EXPERIMENTAL\n");
-		exit(129);
+	const char * const replay_usage[] = {
+		N_("git replay --onto <newbase> <oldbase> <branch> # EXPERIMENTAL"),
+		NULL
+	};
+	struct option replay_options[] = {
+		OPT_STRING(0, "onto", &onto_name,
+			   N_("revision"),
+			   N_("replay onto given commit")),
+		OPT_END()
+	};
+
+	argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
+			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
+
+	if (!onto_name) {
+		error(_("option --onto is mandatory"));
+		usage_with_options(replay_usage, replay_options);
 	}
 
-	if (argc != 5 || strcmp(argv[1], "--onto"))
-		die("usage: read the code, figure out how to use it, then do so");
+	if (argc != 3) {
+		error(_("bad number of arguments"));
+		usage_with_options(replay_usage, replay_options);
+	}
 
-	onto = peel_committish(argv[2]);
-	strbuf_addf(&branch_name, "refs/heads/%s", argv[4]);
+	onto = peel_committish(onto_name);
+	strbuf_addf(&branch_name, "refs/heads/%s", argv[2]);
 
 	/* Sanity check */
 	if (repo_get_oid(the_repository, "HEAD", &head))
@@ -126,6 +143,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		BUG("Could not read index");
 
 	repo_init_revisions(the_repository, &revs, prefix);
+
 	revs.verbose_header = 1;
 	revs.max_parents = 1;
 	revs.cherry_mark = 1;
@@ -134,7 +152,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	revs.right_only = 1;
 	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
 	revs.topo_order = 1;
-	strvec_pushl(&rev_walk_args, "", argv[4], "--not", argv[3], NULL);
+
+	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
 
 	if (setup_revisions(rev_walk_args.nr, rev_walk_args.v, &revs, NULL) > 1) {
 		ret = error(_("unhandled options"));
@@ -197,8 +216,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			       &last_commit->object.oid,
 			       &last_picked_commit->object.oid,
 			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
-			error(_("could not update %s"), argv[4]);
-			die("Failed to update %s", argv[4]);
+			error(_("could not update %s"), argv[2]);
+			die("Failed to update %s", argv[2]);
 		}
 		if (create_symref("HEAD", branch_name.buf, reflog_msg.buf) < 0)
 			die(_("unable to update HEAD"));
@@ -210,8 +229,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			       &last_commit->object.oid,
 			       &head,
 			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
-			error(_("could not update %s"), argv[4]);
-			die("Failed to update %s", argv[4]);
+			error(_("could not update %s"), argv[2]);
+			die("Failed to update %s", argv[2]);
 		}
 	}
 	ret = (result.clean == 0);
-- 
2.42.0.496.g529a7fda40


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v6 04/14] replay: die() instead of failing assert()
  2023-11-02 13:51         ` [PATCH v6 " Christian Couder
                             ` (2 preceding siblings ...)
  2023-11-02 13:51           ` [PATCH v6 03/14] replay: start using parse_options API Christian Couder
@ 2023-11-02 13:51           ` Christian Couder
  2023-11-02 13:51           ` [PATCH v6 05/14] replay: introduce pick_regular_commit() Christian Couder
                             ` (12 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-02 13:51 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

It's not a good idea for regular Git commands to use an assert() to
check for things that could happen but are not supported.

Let's die() with an explanation of the issue instead.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index afabb844d3..32dbaaf028 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -179,7 +179,12 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 
 		fprintf(stderr, "Rebasing %s...\r",
 			oid_to_hex(&commit->object.oid));
-		assert(commit->parents && !commit->parents->next);
+
+		if (!commit->parents)
+			die(_("replaying down to root commit is not supported yet!"));
+		if (commit->parents->next)
+			die(_("replaying merge commits is not supported yet!"));
+
 		base = commit->parents->item;
 
 		next_tree = repo_get_commit_tree(the_repository, commit);
-- 
2.42.0.496.g529a7fda40


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v6 05/14] replay: introduce pick_regular_commit()
  2023-11-02 13:51         ` [PATCH v6 " Christian Couder
                             ` (3 preceding siblings ...)
  2023-11-02 13:51           ` [PATCH v6 04/14] replay: die() instead of failing assert() Christian Couder
@ 2023-11-02 13:51           ` Christian Couder
  2023-11-02 13:51           ` [PATCH v6 06/14] replay: change rev walking options Christian Couder
                             ` (11 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-02 13:51 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

Let's refactor the code to handle a regular commit (a commit that is
neither a root commit nor a merge commit) into a single function instead
of keeping it inside cmd_replay().

This is good for separation of concerns, and this will help further work
in the future to replay merge commits.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 54 ++++++++++++++++++++++++++++++------------------
 1 file changed, 34 insertions(+), 20 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 32dbaaf028..5c4cbd11db 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -89,6 +89,35 @@ static struct commit *create_commit(struct tree *tree,
 	return (struct commit *)obj;
 }
 
+static struct commit *pick_regular_commit(struct commit *pickme,
+					  struct commit *last_commit,
+					  struct merge_options *merge_opt,
+					  struct merge_result *result)
+{
+	struct commit *base;
+	struct tree *pickme_tree, *base_tree;
+
+	base = pickme->parents->item;
+
+	pickme_tree = repo_get_commit_tree(the_repository, pickme);
+	base_tree = repo_get_commit_tree(the_repository, base);
+
+	merge_opt->branch2 = short_commit_name(pickme);
+	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
+
+	merge_incore_nonrecursive(merge_opt,
+				  base_tree,
+				  result->tree,
+				  pickme_tree,
+				  result);
+
+	free((char*)merge_opt->ancestor);
+	merge_opt->ancestor = NULL;
+	if (!result->clean)
+		return NULL;
+	return create_commit(result->tree, pickme, last_commit);
+}
+
 int cmd_replay(int argc, const char **argv, const char *prefix)
 {
 	struct commit *onto;
@@ -100,7 +129,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct rev_info revs;
 	struct commit *commit;
 	struct merge_options merge_opt;
-	struct tree *next_tree, *base_tree, *head_tree;
+	struct tree *head_tree;
 	struct merge_result result;
 	struct strbuf reflog_msg = STRBUF_INIT;
 	struct strbuf branch_name = STRBUF_INIT;
@@ -175,7 +204,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	result.tree = head_tree;
 	last_commit = onto;
 	while ((commit = get_revision(&revs))) {
-		struct commit *base;
+		struct commit *pick;
 
 		fprintf(stderr, "Rebasing %s...\r",
 			oid_to_hex(&commit->object.oid));
@@ -185,26 +214,11 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		if (commit->parents->next)
 			die(_("replaying merge commits is not supported yet!"));
 
-		base = commit->parents->item;
-
-		next_tree = repo_get_commit_tree(the_repository, commit);
-		base_tree = repo_get_commit_tree(the_repository, base);
-
-		merge_opt.branch2 = short_commit_name(commit);
-		merge_opt.ancestor = xstrfmt("parent of %s", merge_opt.branch2);
-
-		merge_incore_nonrecursive(&merge_opt,
-					  base_tree,
-					  result.tree,
-					  next_tree,
-					  &result);
-
-		free((char*)merge_opt.ancestor);
-		merge_opt.ancestor = NULL;
-		if (!result.clean)
+		pick = pick_regular_commit(commit, last_commit, &merge_opt, &result);
+		if (!pick)
 			break;
+		last_commit = pick;
 		last_picked_commit = commit;
-		last_commit = create_commit(result.tree, commit, last_commit);
 	}
 
 	merge_finalize(&merge_opt, &result);
-- 
2.42.0.496.g529a7fda40


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v6 06/14] replay: change rev walking options
  2023-11-02 13:51         ` [PATCH v6 " Christian Couder
                             ` (4 preceding siblings ...)
  2023-11-02 13:51           ` [PATCH v6 05/14] replay: introduce pick_regular_commit() Christian Couder
@ 2023-11-02 13:51           ` Christian Couder
  2023-11-02 13:51           ` [PATCH v6 07/14] replay: add an important FIXME comment about gpg signing Christian Couder
                             ` (10 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-02 13:51 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

Let's set the rev walking options we need after calling
setup_revisions() instead of before. This enforces options we always
want for now.

We want the command to work from older commits to newer ones by default.
Also we don't want history simplification, as we want to deal with all
the commits in the affected range.

When we see an option that we are going to override, we emit a warning
to avoid confusion as much as possible though.

Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 30 ++++++++++++++++++++----------
 1 file changed, 20 insertions(+), 10 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 5c4cbd11db..a0d27ab249 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -133,7 +133,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct merge_result result;
 	struct strbuf reflog_msg = STRBUF_INIT;
 	struct strbuf branch_name = STRBUF_INIT;
-	int ret = 0;
+	int i, ret = 0;
 
 	const char * const replay_usage[] = {
 		N_("git replay --onto <newbase> <oldbase> <branch> # EXPERIMENTAL"),
@@ -173,22 +173,32 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 
 	repo_init_revisions(the_repository, &revs, prefix);
 
-	revs.verbose_header = 1;
-	revs.max_parents = 1;
-	revs.cherry_mark = 1;
-	revs.limited = 1;
-	revs.reverse = 1;
-	revs.right_only = 1;
-	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
-	revs.topo_order = 1;
-
 	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
 
+	/*
+	 * TODO: For now, let's warn when we see an option that we are
+	 * going to override after setup_revisions() below. In the
+	 * future we might want to either die() or allow them if we
+	 * think they could be useful though.
+	 */
+	for (i = 0; i < argc; i++) {
+		if (!strcmp(argv[i], "--reverse") || !strcmp(argv[i], "--date-order") ||
+		    !strcmp(argv[i], "--topo-order") || !strcmp(argv[i], "--author-date-order") ||
+		    !strcmp(argv[i], "--full-history"))
+			warning(_("option '%s' will be overridden"), argv[i]);
+	}
+
 	if (setup_revisions(rev_walk_args.nr, rev_walk_args.v, &revs, NULL) > 1) {
 		ret = error(_("unhandled options"));
 		goto cleanup;
 	}
 
+	/* requirements/overrides for revs */
+	revs.reverse = 1;
+	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
+	revs.topo_order = 1;
+	revs.simplify_history = 0;
+
 	strvec_clear(&rev_walk_args);
 
 	if (prepare_revision_walk(&revs) < 0) {
-- 
2.42.0.496.g529a7fda40


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v6 07/14] replay: add an important FIXME comment about gpg signing
  2023-11-02 13:51         ` [PATCH v6 " Christian Couder
                             ` (5 preceding siblings ...)
  2023-11-02 13:51           ` [PATCH v6 06/14] replay: change rev walking options Christian Couder
@ 2023-11-02 13:51           ` Christian Couder
  2023-11-02 13:51           ` [PATCH v6 08/14] replay: remove progress and info output Christian Couder
                             ` (9 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-02 13:51 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

We want to be able to handle signed commits in some way in the future,
but we are not ready to do it now. So for the time being let's just add
a FIXME comment to remind us about it.

These are different ways we could handle them:

  - in case of a cli user and if there was an interactive mode, we could
    perhaps ask if the user wants to sign again
  - we could add an option to just fail if there are signed commits

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index a0d27ab249..544a546674 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -62,7 +62,7 @@ static struct commit *create_commit(struct tree *tree,
 	struct object *obj;
 	struct commit_list *parents = NULL;
 	char *author;
-	char *sign_commit = NULL;
+	char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
 	struct commit_extra_header *extra;
 	struct strbuf msg = STRBUF_INIT;
 	const char *out_enc = get_commit_output_encoding();
-- 
2.42.0.496.g529a7fda40


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v6 08/14] replay: remove progress and info output
  2023-11-02 13:51         ` [PATCH v6 " Christian Couder
                             ` (6 preceding siblings ...)
  2023-11-02 13:51           ` [PATCH v6 07/14] replay: add an important FIXME comment about gpg signing Christian Couder
@ 2023-11-02 13:51           ` Christian Couder
  2023-11-02 13:51           ` [PATCH v6 09/14] replay: remove HEAD related sanity check Christian Couder
                             ` (8 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-02 13:51 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

The replay command will be changed in a follow up commit, so that it
will not update refs directly, but instead it will print on stdout a
list of commands that can be consumed by `git update-ref --stdin`.

We don't want this output to be polluted by its current low value
output, so let's just remove the latter.

In the future, when the command gets an option to update refs by
itself, it will make a lot of sense to display a progress meter, but
we are not there yet.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 7 +------
 1 file changed, 1 insertion(+), 6 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 544a546674..74680b0c8f 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -208,7 +208,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
-	merge_opt.show_rename_progress = 1;
+	merge_opt.show_rename_progress = 0;
 	merge_opt.branch1 = "HEAD";
 	head_tree = repo_get_commit_tree(the_repository, onto);
 	result.tree = head_tree;
@@ -216,9 +216,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	while ((commit = get_revision(&revs))) {
 		struct commit *pick;
 
-		fprintf(stderr, "Rebasing %s...\r",
-			oid_to_hex(&commit->object.oid));
-
 		if (!commit->parents)
 			die(_("replaying down to root commit is not supported yet!"));
 		if (commit->parents->next)
@@ -237,7 +234,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		exit(128);
 
 	if (result.clean) {
-		fprintf(stderr, "\nDone.\n");
 		strbuf_addf(&reflog_msg, "finish rebase %s onto %s",
 			    oid_to_hex(&last_picked_commit->object.oid),
 			    oid_to_hex(&last_commit->object.oid));
@@ -251,7 +247,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		if (create_symref("HEAD", branch_name.buf, reflog_msg.buf) < 0)
 			die(_("unable to update HEAD"));
 	} else {
-		fprintf(stderr, "\nAborting: Hit a conflict.\n");
 		strbuf_addf(&reflog_msg, "rebase progress up to %s",
 			    oid_to_hex(&last_picked_commit->object.oid));
 		if (update_ref(reflog_msg.buf, "HEAD",
-- 
2.42.0.496.g529a7fda40


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v6 09/14] replay: remove HEAD related sanity check
  2023-11-02 13:51         ` [PATCH v6 " Christian Couder
                             ` (7 preceding siblings ...)
  2023-11-02 13:51           ` [PATCH v6 08/14] replay: remove progress and info output Christian Couder
@ 2023-11-02 13:51           ` Christian Couder
  2023-11-02 13:51           ` [PATCH v6 10/14] replay: make it a minimal server side command Christian Couder
                             ` (7 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-02 13:51 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

We want replay to be a command that can be used on the server side on
any branch, not just the current one, so we are going to stop updating
HEAD in a future commit.

A "sanity check" that makes sure we are replaying the current branch
doesn't make sense anymore. Let's remove it.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c         | 8 +-------
 t/t3650-replay-basics.sh | 2 --
 2 files changed, 1 insertion(+), 9 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 74680b0c8f..9331f5c6ec 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -123,7 +123,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct commit *onto;
 	const char *onto_name = NULL;
 	struct commit *last_commit = NULL, *last_picked_commit = NULL;
-	struct object_id head;
 	struct lock_file lock = LOCK_INIT;
 	struct strvec rev_walk_args = STRVEC_INIT;
 	struct rev_info revs;
@@ -162,11 +161,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	onto = peel_committish(onto_name);
 	strbuf_addf(&branch_name, "refs/heads/%s", argv[2]);
 
-	/* Sanity check */
-	if (repo_get_oid(the_repository, "HEAD", &head))
-		die(_("Cannot read HEAD"));
-	assert(oideq(&onto->object.oid, &head));
-
 	repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
 	if (repo_read_index(the_repository) < 0)
 		BUG("Could not read index");
@@ -251,7 +245,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			    oid_to_hex(&last_picked_commit->object.oid));
 		if (update_ref(reflog_msg.buf, "HEAD",
 			       &last_commit->object.oid,
-			       &head,
+			       &onto->object.oid,
 			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
 			error(_("could not update %s"), argv[2]);
 			die("Failed to update %s", argv[2]);
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 36c1b5082a..b5b9f9ade2 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -48,8 +48,6 @@ test_expect_success 'setup' '
 '
 
 test_expect_success 'using replay to rebase two branches, one on top of other' '
-	git switch main &&
-
 	git replay --onto main topic1 topic2 >result &&
 
 	git log --format=%s $(cut -f 3 -d " " result) >actual &&
-- 
2.42.0.496.g529a7fda40


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v6 10/14] replay: make it a minimal server side command
  2023-11-02 13:51         ` [PATCH v6 " Christian Couder
                             ` (8 preceding siblings ...)
  2023-11-02 13:51           ` [PATCH v6 09/14] replay: remove HEAD related sanity check Christian Couder
@ 2023-11-02 13:51           ` Christian Couder
  2023-11-02 13:51           ` [PATCH v6 11/14] replay: use standard revision ranges Christian Couder
                             ` (6 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-02 13:51 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

We want this command to be a minimal command that just does server side
picking of commits, displaying the results on stdout for higher level
scripts to consume.

So let's simplify it:
  * remove the worktree and index reading/writing,
  * remove the ref (and reflog) updating,
  * remove the assumptions tying us to HEAD, since (a) this is not a
    rebase and (b) we want to be able to pick commits in a bare repo,
    i.e. to/from branches that are not checked out and not the main
    branch,
  * remove unneeded includes,
  * handle rebasing multiple branches by printing on stdout the update
    ref commands that should be performed.

The output can be piped into `git update-ref --stdin` for the ref
updates to happen.

In the future to make it easier for users to use this command
directly maybe an option can be added to automatically pipe its output
into `git update-ref`.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 Documentation/git-replay.txt             |  5 +-
 builtin/replay.c                         | 78 ++++++++----------------
 t/t3650-replay-basics.sh                 | 19 +++++-
 t/t6429-merge-sequence-rename-caching.sh | 39 +++++++-----
 4 files changed, 72 insertions(+), 69 deletions(-)

diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
index 44bf584fed..4c852ff3bd 100644
--- a/Documentation/git-replay.txt
+++ b/Documentation/git-replay.txt
@@ -14,7 +14,10 @@ SYNOPSIS
 DESCRIPTION
 -----------
 
-Takes a range of commits and replays them onto a new location.
+Takes a range of commits and replays them onto a new location. Leaves
+the working tree and the index untouched, and updates no
+references. The output of this command is meant to be used as input to
+`git update-ref --stdin`, which would update the relevant branches.
 
 THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
 
diff --git a/builtin/replay.c b/builtin/replay.c
index 9331f5c6ec..7e3ebac3db 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -6,11 +6,7 @@
 #include "git-compat-util.h"
 
 #include "builtin.h"
-#include "cache-tree.h"
-#include "commit.h"
 #include "environment.h"
-#include "gettext.h"
-#include "hash.h"
 #include "hex.h"
 #include "lockfile.h"
 #include "merge-ort.h"
@@ -18,8 +14,6 @@
 #include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
-#include "sequencer.h"
-#include "setup.h"
 #include "strvec.h"
 #include <oidset.h>
 #include <tree.h>
@@ -102,6 +96,7 @@ static struct commit *pick_regular_commit(struct commit *pickme,
 	pickme_tree = repo_get_commit_tree(the_repository, pickme);
 	base_tree = repo_get_commit_tree(the_repository, base);
 
+	merge_opt->branch1 = short_commit_name(last_commit);
 	merge_opt->branch2 = short_commit_name(pickme);
 	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
 
@@ -122,15 +117,12 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 {
 	struct commit *onto;
 	const char *onto_name = NULL;
-	struct commit *last_commit = NULL, *last_picked_commit = NULL;
-	struct lock_file lock = LOCK_INIT;
+	struct commit *last_commit = NULL;
 	struct strvec rev_walk_args = STRVEC_INIT;
 	struct rev_info revs;
 	struct commit *commit;
 	struct merge_options merge_opt;
-	struct tree *head_tree;
 	struct merge_result result;
-	struct strbuf reflog_msg = STRBUF_INIT;
 	struct strbuf branch_name = STRBUF_INIT;
 	int i, ret = 0;
 
@@ -161,10 +153,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	onto = peel_committish(onto_name);
 	strbuf_addf(&branch_name, "refs/heads/%s", argv[2]);
 
-	repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
-	if (repo_read_index(the_repository) < 0)
-		BUG("Could not read index");
-
 	repo_init_revisions(the_repository, &revs, prefix);
 
 	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
@@ -203,58 +191,44 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
 	merge_opt.show_rename_progress = 0;
-	merge_opt.branch1 = "HEAD";
-	head_tree = repo_get_commit_tree(the_repository, onto);
-	result.tree = head_tree;
+	result.tree = repo_get_commit_tree(the_repository, onto);
 	last_commit = onto;
 	while ((commit = get_revision(&revs))) {
-		struct commit *pick;
+		const struct name_decoration *decoration;
 
 		if (!commit->parents)
 			die(_("replaying down to root commit is not supported yet!"));
 		if (commit->parents->next)
 			die(_("replaying merge commits is not supported yet!"));
 
-		pick = pick_regular_commit(commit, last_commit, &merge_opt, &result);
-		if (!pick)
+		last_commit = pick_regular_commit(commit, last_commit, &merge_opt, &result);
+		if (!last_commit)
 			break;
-		last_commit = pick;
-		last_picked_commit = commit;
+
+		decoration = get_name_decoration(&commit->object);
+		if (!decoration)
+			continue;
+
+		while (decoration) {
+			if (decoration->type == DECORATION_REF_LOCAL) {
+				printf("update %s %s %s\n",
+				       decoration->name,
+				       oid_to_hex(&last_commit->object.oid),
+				       oid_to_hex(&commit->object.oid));
+			}
+			decoration = decoration->next;
+		}
 	}
 
 	merge_finalize(&merge_opt, &result);
+	ret = result.clean;
 
-	if (result.clean < 0)
-		exit(128);
-
-	if (result.clean) {
-		strbuf_addf(&reflog_msg, "finish rebase %s onto %s",
-			    oid_to_hex(&last_picked_commit->object.oid),
-			    oid_to_hex(&last_commit->object.oid));
-		if (update_ref(reflog_msg.buf, branch_name.buf,
-			       &last_commit->object.oid,
-			       &last_picked_commit->object.oid,
-			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
-			error(_("could not update %s"), argv[2]);
-			die("Failed to update %s", argv[2]);
-		}
-		if (create_symref("HEAD", branch_name.buf, reflog_msg.buf) < 0)
-			die(_("unable to update HEAD"));
-	} else {
-		strbuf_addf(&reflog_msg, "rebase progress up to %s",
-			    oid_to_hex(&last_picked_commit->object.oid));
-		if (update_ref(reflog_msg.buf, "HEAD",
-			       &last_commit->object.oid,
-			       &onto->object.oid,
-			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
-			error(_("could not update %s"), argv[2]);
-			die("Failed to update %s", argv[2]);
-		}
-	}
-	ret = (result.clean == 0);
 cleanup:
-	strbuf_release(&reflog_msg);
 	strbuf_release(&branch_name);
 	release_revisions(&revs);
-	return ret;
+
+	/* Return */
+	if (ret < 0)
+		exit(128);
+	return ret ? 0 : 1;
 }
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index b5b9f9ade2..3567c98362 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -47,12 +47,29 @@ test_expect_success 'setup' '
 	test_commit C.conflict C.t conflict
 '
 
+test_expect_success 'setup bare' '
+	git clone --bare . bare
+'
+
 test_expect_success 'using replay to rebase two branches, one on top of other' '
 	git replay --onto main topic1 topic2 >result &&
 
+	test_line_count = 1 result &&
+
 	git log --format=%s $(cut -f 3 -d " " result) >actual &&
 	test_write_lines E D M L B A >expect &&
-	test_cmp expect actual
+	test_cmp expect actual &&
+
+	printf "update refs/heads/topic2 " >expect &&
+	printf "%s " $(cut -f 3 -d " " result) >>expect &&
+	git rev-parse topic2 >>expect &&
+
+	test_cmp expect result
+'
+
+test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '
+	git -C bare replay --onto main topic1 topic2 >result-bare &&
+	test_cmp expect result-bare
 '
 
 test_done
diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh
index 7670b72008..099aefeffc 100755
--- a/t/t6429-merge-sequence-rename-caching.sh
+++ b/t/t6429-merge-sequence-rename-caching.sh
@@ -71,8 +71,9 @@ test_expect_success 'caching renames does not preclude finding new ones' '
 
 		git switch upstream &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		git ls-files >tracked-files &&
 		test_line_count = 2 tracked-files &&
@@ -140,7 +141,9 @@ test_expect_success 'cherry-pick both a commit and its immediate revert' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 1 calls
@@ -198,8 +201,9 @@ test_expect_success 'rename same file identically, then reintroduce it' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		git ls-files >tracked &&
 		test_line_count = 2 tracked &&
@@ -275,8 +279,9 @@ test_expect_success 'rename same file identically, then add file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		git ls-files >tracked &&
 		test_line_count = 4 tracked &&
@@ -451,8 +456,9 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls &&
@@ -517,8 +523,9 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 3 calls &&
@@ -619,8 +626,9 @@ test_expect_success 'caching renames only on upstream side, part 1' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 1 calls &&
@@ -677,8 +685,9 @@ test_expect_success 'caching renames only on upstream side, part 2' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls &&
-- 
2.42.0.496.g529a7fda40


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v6 11/14] replay: use standard revision ranges
  2023-11-02 13:51         ` [PATCH v6 " Christian Couder
                             ` (9 preceding siblings ...)
  2023-11-02 13:51           ` [PATCH v6 10/14] replay: make it a minimal server side command Christian Couder
@ 2023-11-02 13:51           ` Christian Couder
  2023-11-02 13:51           ` [PATCH v6 12/14] replay: add --advance or 'cherry-pick' mode Christian Couder
                             ` (5 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-02 13:51 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

Instead of the fixed "<oldbase> <branch>" arguments, the replay
command now accepts "<revision-range>..." arguments in a similar
way as many other Git commands. This makes its interface more
standard and more flexible.

This also enables many revision related options accepted and
eaten by setup_revisions(). If the replay command was a high level
one or had a high level mode, it would make sense to restrict some
of the possible options, like those generating non-contiguous
history, as they could be confusing for most users.

Also as the interface of the command is now mostly finalized,
we can add more documentation and more testcases to make sure
the command will continue to work as designed in the future.

We only document the rev-list related options among all the
revision related options that are now accepted, as the rev-list
related ones are probably the most useful for now.

Helped-by: Dragan Simic <dsimic@manjaro.org>
Helped-by: Linus Arver <linusa@google.com>
Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 Documentation/git-replay.txt             | 55 +++++++++++++++++++++++-
 builtin/replay.c                         | 21 ++-------
 t/t3650-replay-basics.sh                 | 12 +++++-
 t/t6429-merge-sequence-rename-caching.sh | 18 ++++----
 4 files changed, 76 insertions(+), 30 deletions(-)

diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
index 4c852ff3bd..36ddd7daed 100644
--- a/Documentation/git-replay.txt
+++ b/Documentation/git-replay.txt
@@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
 SYNOPSIS
 --------
 [verse]
-'git replay' --onto <newbase> <oldbase> <branch> # EXPERIMENTAL
+'git replay' --onto <newbase> <revision-range>... # EXPERIMENTAL
 
 DESCRIPTION
 -----------
@@ -17,7 +17,8 @@ DESCRIPTION
 Takes a range of commits and replays them onto a new location. Leaves
 the working tree and the index untouched, and updates no
 references. The output of this command is meant to be used as input to
-`git update-ref --stdin`, which would update the relevant branches.
+`git update-ref --stdin`, which would update the relevant branches
+(see the OUTPUT section below).
 
 THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
 
@@ -27,6 +28,30 @@ OPTIONS
 --onto <newbase>::
 	Starting point at which to create the new commits.  May be any
 	valid commit, and not just an existing branch name.
++
+The update-ref command(s) in the output will update the branch(es) in
+the revision range to point at the new commits, similar to the way how
+`git rebase --update-refs` updates multiple branches in the affected
+range.
+
+<revision-range>::
+	Range of commits to replay; see "Specifying Ranges" in
+	linkgit:git-rev-parse and the "Commit Limiting" options below.
+
+include::rev-list-options.txt[]
+
+OUTPUT
+------
+
+When there are no conflicts, the output of this command is usable as
+input to `git update-ref --stdin`.  It is of the form:
+
+	update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
+	update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
+	update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
+
+where the number of refs updated depends on the arguments passed and
+the shape of the history being replayed.
 
 EXIT STATUS
 -----------
@@ -36,6 +61,32 @@ the replay has conflicts, the exit status is 1.  If the replay is not
 able to complete (or start) due to some kind of error, the exit status
 is something other than 0 or 1.
 
+EXAMPLES
+--------
+
+To simply rebase `mybranch` onto `target`:
+
+------------
+$ git replay --onto target origin/main..mybranch
+update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
+------------
+
+When calling `git replay`, one does not need to specify a range of
+commits to replay using the syntax `A..B`; any range expression will
+do:
+
+------------
+$ git replay --onto origin/main ^base branch1 branch2 branch3
+update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
+update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
+update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
+------------
+
+This will simultaneously rebase `branch1`, `branch2`, and `branch3`,
+all commits they have since `base`, playing them on top of
+`origin/main`. These three branches may have commits on top of `base`
+that they have in common, but that does not need to be the case.
+
 GIT
 ---
 Part of the linkgit:git[1] suite
diff --git a/builtin/replay.c b/builtin/replay.c
index 7e3ebac3db..8fe4391976 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -14,7 +14,6 @@
 #include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
-#include "strvec.h"
 #include <oidset.h>
 #include <tree.h>
 
@@ -118,16 +117,14 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct commit *onto;
 	const char *onto_name = NULL;
 	struct commit *last_commit = NULL;
-	struct strvec rev_walk_args = STRVEC_INIT;
 	struct rev_info revs;
 	struct commit *commit;
 	struct merge_options merge_opt;
 	struct merge_result result;
-	struct strbuf branch_name = STRBUF_INIT;
 	int i, ret = 0;
 
 	const char * const replay_usage[] = {
-		N_("git replay --onto <newbase> <oldbase> <branch> # EXPERIMENTAL"),
+		N_("git replay --onto <newbase> <revision-range>... # EXPERIMENTAL"),
 		NULL
 	};
 	struct option replay_options[] = {
@@ -145,18 +142,10 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		usage_with_options(replay_usage, replay_options);
 	}
 
-	if (argc != 3) {
-		error(_("bad number of arguments"));
-		usage_with_options(replay_usage, replay_options);
-	}
-
 	onto = peel_committish(onto_name);
-	strbuf_addf(&branch_name, "refs/heads/%s", argv[2]);
 
 	repo_init_revisions(the_repository, &revs, prefix);
 
-	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
-
 	/*
 	 * TODO: For now, let's warn when we see an option that we are
 	 * going to override after setup_revisions() below. In the
@@ -170,8 +159,9 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			warning(_("option '%s' will be overridden"), argv[i]);
 	}
 
-	if (setup_revisions(rev_walk_args.nr, rev_walk_args.v, &revs, NULL) > 1) {
-		ret = error(_("unhandled options"));
+	argc = setup_revisions(argc, argv, &revs, NULL);
+	if (argc > 1) {
+		ret = error(_("unrecognized argument: %s"), argv[1]);
 		goto cleanup;
 	}
 
@@ -181,8 +171,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	revs.topo_order = 1;
 	revs.simplify_history = 0;
 
-	strvec_clear(&rev_walk_args);
-
 	if (prepare_revision_walk(&revs) < 0) {
 		ret = error(_("error preparing revisions"));
 		goto cleanup;
@@ -224,7 +212,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	ret = result.clean;
 
 cleanup:
-	strbuf_release(&branch_name);
 	release_revisions(&revs);
 
 	/* Return */
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 3567c98362..a1da4f9ef9 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -52,7 +52,7 @@ test_expect_success 'setup bare' '
 '
 
 test_expect_success 'using replay to rebase two branches, one on top of other' '
-	git replay --onto main topic1 topic2 >result &&
+	git replay --onto main topic1..topic2 >result &&
 
 	test_line_count = 1 result &&
 
@@ -68,8 +68,16 @@ test_expect_success 'using replay to rebase two branches, one on top of other' '
 '
 
 test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '
-	git -C bare replay --onto main topic1 topic2 >result-bare &&
+	git -C bare replay --onto main topic1..topic2 >result-bare &&
 	test_cmp expect result-bare
 '
 
+test_expect_success 'using replay to rebase with a conflict' '
+	test_expect_code 1 git replay --onto topic1 B..conflict
+'
+
+test_expect_success 'using replay on bare repo to rebase with a conflict' '
+	test_expect_code 1 git -C bare replay --onto topic1 B..conflict
+'
+
 test_done
diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh
index 099aefeffc..0f39ed0d08 100755
--- a/t/t6429-merge-sequence-rename-caching.sh
+++ b/t/t6429-merge-sequence-rename-caching.sh
@@ -71,7 +71,7 @@ test_expect_success 'caching renames does not preclude finding new ones' '
 
 		git switch upstream &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -141,7 +141,7 @@ test_expect_success 'cherry-pick both a commit and its immediate revert' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -201,7 +201,7 @@ test_expect_success 'rename same file identically, then reintroduce it' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -279,7 +279,7 @@ test_expect_success 'rename same file identically, then add file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -357,7 +357,7 @@ test_expect_success 'cached dir rename does not prevent noticing later conflict'
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test_must_fail git replay --onto HEAD upstream~1 topic >output &&
+		test_must_fail git replay --onto HEAD upstream~1..topic >output &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls
@@ -456,7 +456,7 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -523,7 +523,7 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -626,7 +626,7 @@ test_expect_success 'caching renames only on upstream side, part 1' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -685,7 +685,7 @@ test_expect_success 'caching renames only on upstream side, part 2' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
-- 
2.42.0.496.g529a7fda40


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v6 12/14] replay: add --advance or 'cherry-pick' mode
  2023-11-02 13:51         ` [PATCH v6 " Christian Couder
                             ` (10 preceding siblings ...)
  2023-11-02 13:51           ` [PATCH v6 11/14] replay: use standard revision ranges Christian Couder
@ 2023-11-02 13:51           ` Christian Couder
  2023-11-02 13:51           ` [PATCH v6 13/14] replay: add --contained to rebase contained branches Christian Couder
                             ` (4 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-02 13:51 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

There is already a 'rebase' mode with `--onto`. Let's add an 'advance' or
'cherry-pick' mode with `--advance`. This new mode will make the target
branch advance as we replay commits onto it.

The replayed commits should have a single tip, so that it's clear where
the target branch should be advanced. If they have more than one tip,
this new mode will error out.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 Documentation/git-replay.txt |  41 ++++++--
 builtin/replay.c             | 185 +++++++++++++++++++++++++++++++++--
 t/t3650-replay-basics.sh     |  34 +++++++
 3 files changed, 243 insertions(+), 17 deletions(-)

diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
index 36ddd7daed..e0c85cebf1 100644
--- a/Documentation/git-replay.txt
+++ b/Documentation/git-replay.txt
@@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
 SYNOPSIS
 --------
 [verse]
-'git replay' --onto <newbase> <revision-range>... # EXPERIMENTAL
+'git replay' (--onto <newbase> | --advance <branch>) <revision-range>... # EXPERIMENTAL
 
 DESCRIPTION
 -----------
@@ -29,14 +29,25 @@ OPTIONS
 	Starting point at which to create the new commits.  May be any
 	valid commit, and not just an existing branch name.
 +
-The update-ref command(s) in the output will update the branch(es) in
-the revision range to point at the new commits, similar to the way how
-`git rebase --update-refs` updates multiple branches in the affected
-range.
+When `--onto` is specified, the update-ref command(s) in the output will
+update the branch(es) in the revision range to point at the new
+commits, similar to the way how `git rebase --update-refs` updates
+multiple branches in the affected range.
+
+--advance <branch>::
+	Starting point at which to create the new commits; must be a
+	branch name.
++
+When `--advance` is specified, the update-ref command(s) in the output
+will update the branch passed as an argument to `--advance` to point at
+the new commits (in other words, this mimics a cherry-pick operation).
 
 <revision-range>::
-	Range of commits to replay; see "Specifying Ranges" in
-	linkgit:git-rev-parse and the "Commit Limiting" options below.
+	Range of commits to replay. More than one <revision-range> can
+	be passed, but in `--advance <branch>` mode, they should have
+	a single tip, so that it's clear where <branch> should point
+	to. See "Specifying Ranges" in linkgit:git-rev-parse and the
+	"Commit Limiting" options below.
 
 include::rev-list-options.txt[]
 
@@ -51,7 +62,9 @@ input to `git update-ref --stdin`.  It is of the form:
 	update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
 
 where the number of refs updated depends on the arguments passed and
-the shape of the history being replayed.
+the shape of the history being replayed.  When using `--advance`, the
+number of refs updated is always one, but for `--onto`, it can be one
+or more (rebasing multiple branches simultaneously is supported).
 
 EXIT STATUS
 -----------
@@ -71,6 +84,18 @@ $ git replay --onto target origin/main..mybranch
 update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
 ------------
 
+To cherry-pick the commits from mybranch onto target:
+
+------------
+$ git replay --advance target origin/main..mybranch
+update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}
+------------
+
+Note that the first two examples replay the exact same commits and on
+top of the exact same new base, they only differ in that the first
+provides instructions to make mybranch point at the new commits and
+the second provides instructions to make target point at them.
+
 When calling `git replay`, one does not need to specify a range of
 commits to replay using the syntax `A..B`; any range expression will
 do:
diff --git a/builtin/replay.c b/builtin/replay.c
index 8fe4391976..145ce9d9a3 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -14,6 +14,7 @@
 #include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
+#include "strmap.h"
 #include <oidset.h>
 #include <tree.h>
 
@@ -82,6 +83,146 @@ static struct commit *create_commit(struct tree *tree,
 	return (struct commit *)obj;
 }
 
+struct ref_info {
+	struct commit *onto;
+	struct strset positive_refs;
+	struct strset negative_refs;
+	int positive_refexprs;
+	int negative_refexprs;
+};
+
+static void get_ref_information(struct rev_cmdline_info *cmd_info,
+				struct ref_info *ref_info)
+{
+	int i;
+
+	ref_info->onto = NULL;
+	strset_init(&ref_info->positive_refs);
+	strset_init(&ref_info->negative_refs);
+	ref_info->positive_refexprs = 0;
+	ref_info->negative_refexprs = 0;
+
+	/*
+	 * When the user specifies e.g.
+	 *   git replay origin/main..mybranch
+	 *   git replay ^origin/next mybranch1 mybranch2
+	 * we want to be able to determine where to replay the commits.  In
+	 * these examples, the branches are probably based on an old version
+	 * of either origin/main or origin/next, so we want to replay on the
+	 * newest version of that branch.  In contrast we would want to error
+	 * out if they ran
+	 *   git replay ^origin/master ^origin/next mybranch
+	 *   git replay mybranch~2..mybranch
+	 * the first of those because there's no unique base to choose, and
+	 * the second because they'd likely just be replaying commits on top
+	 * of the same commit and not making any difference.
+	 */
+	for (i = 0; i < cmd_info->nr; i++) {
+		struct rev_cmdline_entry *e = cmd_info->rev + i;
+		struct object_id oid;
+		const char *refexpr = e->name;
+		char *fullname = NULL;
+		int can_uniquely_dwim = 1;
+
+		if (*refexpr == '^')
+			refexpr++;
+		if (repo_dwim_ref(the_repository, refexpr, strlen(refexpr), &oid, &fullname, 0) != 1)
+			can_uniquely_dwim = 0;
+
+		if (e->flags & BOTTOM) {
+			if (can_uniquely_dwim)
+				strset_add(&ref_info->negative_refs, fullname);
+			if (!ref_info->negative_refexprs)
+				ref_info->onto = lookup_commit_reference_gently(the_repository,
+										&e->item->oid, 1);
+			ref_info->negative_refexprs++;
+		} else {
+			if (can_uniquely_dwim)
+				strset_add(&ref_info->positive_refs, fullname);
+			ref_info->positive_refexprs++;
+		}
+
+		free(fullname);
+	}
+}
+
+static void determine_replay_mode(struct rev_cmdline_info *cmd_info,
+				  const char *onto_name,
+				  const char **advance_name,
+				  struct commit **onto,
+				  struct strset **update_refs)
+{
+	struct ref_info rinfo;
+
+	get_ref_information(cmd_info, &rinfo);
+	if (!rinfo.positive_refexprs)
+		die(_("need some commits to replay"));
+	if (onto_name && *advance_name)
+		die(_("--onto and --advance are incompatible"));
+	else if (onto_name) {
+		*onto = peel_committish(onto_name);
+		if (rinfo.positive_refexprs <
+		    strset_get_size(&rinfo.positive_refs))
+			die(_("all positive revisions given must be references"));
+	} else if (*advance_name) {
+		struct object_id oid;
+		char *fullname = NULL;
+
+		*onto = peel_committish(*advance_name);
+		if (repo_dwim_ref(the_repository, *advance_name, strlen(*advance_name),
+			     &oid, &fullname, 0) == 1) {
+			*advance_name = fullname;
+		} else {
+			die(_("argument to --advance must be a reference"));
+		}
+		if (rinfo.positive_refexprs > 1)
+			die(_("cannot advance target with multiple sources because ordering would be ill-defined"));
+	} else {
+		int positive_refs_complete = (
+			rinfo.positive_refexprs ==
+			strset_get_size(&rinfo.positive_refs));
+		int negative_refs_complete = (
+			rinfo.negative_refexprs ==
+			strset_get_size(&rinfo.negative_refs));
+		/*
+		 * We need either positive_refs_complete or
+		 * negative_refs_complete, but not both.
+		 */
+		if (rinfo.negative_refexprs > 0 &&
+		    positive_refs_complete == negative_refs_complete)
+			die(_("cannot implicitly determine whether this is an --advance or --onto operation"));
+		if (negative_refs_complete) {
+			struct hashmap_iter iter;
+			struct strmap_entry *entry;
+
+			if (rinfo.negative_refexprs == 0)
+				die(_("all positive revisions given must be references"));
+			else if (rinfo.negative_refexprs > 1)
+				die(_("cannot implicitly determine whether this is an --advance or --onto operation"));
+			else if (rinfo.positive_refexprs > 1)
+				die(_("cannot advance target with multiple source branches because ordering would be ill-defined"));
+
+			/* Only one entry, but we have to loop to get it */
+			strset_for_each_entry(&rinfo.negative_refs,
+					      &iter, entry) {
+				*advance_name = entry->key;
+			}
+		} else { /* positive_refs_complete */
+			if (rinfo.negative_refexprs > 1)
+				die(_("cannot implicitly determine correct base for --onto"));
+			if (rinfo.negative_refexprs == 1)
+				*onto = rinfo.onto;
+		}
+	}
+	if (!*advance_name) {
+		*update_refs = xcalloc(1, sizeof(**update_refs));
+		**update_refs = rinfo.positive_refs;
+		memset(&rinfo.positive_refs, 0, sizeof(**update_refs));
+	}
+	strset_clear(&rinfo.negative_refs);
+	strset_clear(&rinfo.positive_refs);
+}
+
 static struct commit *pick_regular_commit(struct commit *pickme,
 					  struct commit *last_commit,
 					  struct merge_options *merge_opt,
@@ -114,20 +255,26 @@ static struct commit *pick_regular_commit(struct commit *pickme,
 
 int cmd_replay(int argc, const char **argv, const char *prefix)
 {
-	struct commit *onto;
+	const char *advance_name = NULL;
+	struct commit *onto = NULL;
 	const char *onto_name = NULL;
-	struct commit *last_commit = NULL;
+
 	struct rev_info revs;
+	struct commit *last_commit = NULL;
 	struct commit *commit;
 	struct merge_options merge_opt;
 	struct merge_result result;
+	struct strset *update_refs = NULL;
 	int i, ret = 0;
 
 	const char * const replay_usage[] = {
-		N_("git replay --onto <newbase> <revision-range>... # EXPERIMENTAL"),
+		N_("git replay (--onto <newbase> | --advance <branch>) <revision-range>... # EXPERIMENTAL"),
 		NULL
 	};
 	struct option replay_options[] = {
+		OPT_STRING(0, "advance", &advance_name,
+			   N_("branch"),
+			   N_("make replay advance given branch")),
 		OPT_STRING(0, "onto", &onto_name,
 			   N_("revision"),
 			   N_("replay onto given commit")),
@@ -137,13 +284,11 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
 			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
 
-	if (!onto_name) {
-		error(_("option --onto is mandatory"));
+	if (!onto_name && !advance_name) {
+		error(_("option --onto or --advance is mandatory"));
 		usage_with_options(replay_usage, replay_options);
 	}
 
-	onto = peel_committish(onto_name);
-
 	repo_init_revisions(the_repository, &revs, prefix);
 
 	/*
@@ -171,6 +316,12 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	revs.topo_order = 1;
 	revs.simplify_history = 0;
 
+	determine_replay_mode(&revs.cmdline, onto_name, &advance_name,
+			      &onto, &update_refs);
+
+	if (!onto) /* FIXME: Should handle replaying down to root commit */
+		die("Replaying down to root commit is not supported yet!");
+
 	if (prepare_revision_walk(&revs) < 0) {
 		ret = error(_("error preparing revisions"));
 		goto cleanup;
@@ -179,6 +330,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
 	merge_opt.show_rename_progress = 0;
+
 	result.tree = repo_get_commit_tree(the_repository, onto);
 	last_commit = onto;
 	while ((commit = get_revision(&revs))) {
@@ -193,12 +345,15 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		if (!last_commit)
 			break;
 
+		/* Update any necessary branches */
+		if (advance_name)
+			continue;
 		decoration = get_name_decoration(&commit->object);
 		if (!decoration)
 			continue;
-
 		while (decoration) {
-			if (decoration->type == DECORATION_REF_LOCAL) {
+			if (decoration->type == DECORATION_REF_LOCAL &&
+			    strset_contains(update_refs, decoration->name)) {
 				printf("update %s %s %s\n",
 				       decoration->name,
 				       oid_to_hex(&last_commit->object.oid),
@@ -208,10 +363,22 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		}
 	}
 
+	/* In --advance mode, advance the target ref */
+	if (result.clean == 1 && advance_name) {
+		printf("update %s %s %s\n",
+		       advance_name,
+		       oid_to_hex(&last_commit->object.oid),
+		       oid_to_hex(&onto->object.oid));
+	}
+
 	merge_finalize(&merge_opt, &result);
 	ret = result.clean;
 
 cleanup:
+	if (update_refs) {
+		strset_clear(update_refs);
+		free(update_refs);
+	}
 	release_revisions(&revs);
 
 	/* Return */
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index a1da4f9ef9..68a87e7803 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -80,4 +80,38 @@ test_expect_success 'using replay on bare repo to rebase with a conflict' '
 	test_expect_code 1 git -C bare replay --onto topic1 B..conflict
 '
 
+test_expect_success 'using replay to perform basic cherry-pick' '
+	# The differences between this test and previous ones are:
+	#   --advance vs --onto
+	# 2nd field of result is refs/heads/main vs. refs/heads/topic2
+	# 4th field of result is hash for main instead of hash for topic2
+
+	git replay --advance main topic1..topic2 >result &&
+
+	test_line_count = 1 result &&
+
+	git log --format=%s $(cut -f 3 -d " " result) >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/main " >expect &&
+	printf "%s " $(cut -f 3 -d " " result) >>expect &&
+	git rev-parse main >>expect &&
+
+	test_cmp expect result
+'
+
+test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
+	git -C bare replay --advance main topic1..topic2 >result-bare &&
+	test_cmp expect result-bare
+'
+
+test_expect_success 'replay on bare repo fails with both --advance and --onto' '
+	test_must_fail git -C bare replay --advance main --onto main topic1..topic2 >result-bare
+'
+
+test_expect_success 'replay fails when both --advance and --onto are omitted' '
+	test_must_fail git replay topic1..topic2 >result
+'
+
 test_done
-- 
2.42.0.496.g529a7fda40


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v6 13/14] replay: add --contained to rebase contained branches
  2023-11-02 13:51         ` [PATCH v6 " Christian Couder
                             ` (11 preceding siblings ...)
  2023-11-02 13:51           ` [PATCH v6 12/14] replay: add --advance or 'cherry-pick' mode Christian Couder
@ 2023-11-02 13:51           ` Christian Couder
  2023-11-02 13:51           ` [PATCH v6 14/14] replay: stop assuming replayed branches do not diverge Christian Couder
                             ` (3 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-02 13:51 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

Let's add a `--contained` option that can be used along with
`--onto` to rebase all the branches contained in the <revision-range>
argument.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 Documentation/git-replay.txt | 12 +++++++++++-
 builtin/replay.c             | 13 +++++++++++--
 t/t3650-replay-basics.sh     | 29 +++++++++++++++++++++++++++++
 3 files changed, 51 insertions(+), 3 deletions(-)

diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
index e0c85cebf1..e7551aec54 100644
--- a/Documentation/git-replay.txt
+++ b/Documentation/git-replay.txt
@@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
 SYNOPSIS
 --------
 [verse]
-'git replay' (--onto <newbase> | --advance <branch>) <revision-range>... # EXPERIMENTAL
+'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>... # EXPERIMENTAL
 
 DESCRIPTION
 -----------
@@ -96,6 +96,16 @@ top of the exact same new base, they only differ in that the first
 provides instructions to make mybranch point at the new commits and
 the second provides instructions to make target point at them.
 
+What if you have a stack of branches, one depending upon another, and
+you'd really like to rebase the whole set?
+
+------------
+$ git replay --contained --onto origin/main origin/main..tipbranch
+update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
+update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
+update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}
+------------
+
 When calling `git replay`, one does not need to specify a range of
 commits to replay using the syntax `A..B`; any range expression will
 do:
diff --git a/builtin/replay.c b/builtin/replay.c
index 145ce9d9a3..08ff9bab5e 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -258,6 +258,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	const char *advance_name = NULL;
 	struct commit *onto = NULL;
 	const char *onto_name = NULL;
+	int contained = 0;
 
 	struct rev_info revs;
 	struct commit *last_commit = NULL;
@@ -268,7 +269,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	int i, ret = 0;
 
 	const char * const replay_usage[] = {
-		N_("git replay (--onto <newbase> | --advance <branch>) <revision-range>... # EXPERIMENTAL"),
+		N_("git replay ([--contained] --onto <newbase> | --advance <branch>) "
+		   "<revision-range>... # EXPERIMENTAL"),
 		NULL
 	};
 	struct option replay_options[] = {
@@ -278,6 +280,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		OPT_STRING(0, "onto", &onto_name,
 			   N_("revision"),
 			   N_("replay onto given commit")),
+		OPT_BOOL(0, "contained", &contained,
+			 N_("advance all branches contained in revision-range")),
 		OPT_END()
 	};
 
@@ -289,6 +293,10 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		usage_with_options(replay_usage, replay_options);
 	}
 
+	if (advance_name && contained)
+		die(_("options '%s' and '%s' cannot be used together"),
+		    "--advance", "--contained");
+
 	repo_init_revisions(the_repository, &revs, prefix);
 
 	/*
@@ -353,7 +361,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			continue;
 		while (decoration) {
 			if (decoration->type == DECORATION_REF_LOCAL &&
-			    strset_contains(update_refs, decoration->name)) {
+			    (contained || strset_contains(update_refs,
+							  decoration->name))) {
 				printf("update %s %s %s\n",
 				       decoration->name,
 				       oid_to_hex(&last_commit->object.oid),
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 68a87e7803..d6286f9580 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -114,4 +114,33 @@ test_expect_success 'replay fails when both --advance and --onto are omitted' '
 	test_must_fail git replay topic1..topic2 >result
 '
 
+test_expect_success 'using replay to also rebase a contained branch' '
+	git replay --contained --onto main main..topic3 >result &&
+
+	test_line_count = 2 result &&
+	cut -f 3 -d " " result >new-branch-tips &&
+
+	git log --format=%s $(head -n 1 new-branch-tips) >actual &&
+	test_write_lines F C M L B A >expect &&
+	test_cmp expect actual &&
+
+	git log --format=%s $(tail -n 1 new-branch-tips) >actual &&
+	test_write_lines H G F C M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/topic1 " >expect &&
+	printf "%s " $(head -n 1 new-branch-tips) >>expect &&
+	git rev-parse topic1 >>expect &&
+	printf "update refs/heads/topic3 " >>expect &&
+	printf "%s " $(tail -n 1 new-branch-tips) >>expect &&
+	git rev-parse topic3 >>expect &&
+
+	test_cmp expect result
+'
+
+test_expect_success 'using replay on bare repo to also rebase a contained branch' '
+	git -C bare replay --contained --onto main main..topic3 >result-bare &&
+	test_cmp expect result-bare
+'
+
 test_done
-- 
2.42.0.496.g529a7fda40


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v6 14/14] replay: stop assuming replayed branches do not diverge
  2023-11-02 13:51         ` [PATCH v6 " Christian Couder
                             ` (12 preceding siblings ...)
  2023-11-02 13:51           ` [PATCH v6 13/14] replay: add --contained to rebase contained branches Christian Couder
@ 2023-11-02 13:51           ` Christian Couder
  2023-11-07  2:43           ` [PATCH v6 00/14] Introduce new `git replay` command Elijah Newren
                             ` (2 subsequent siblings)
  16 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-02 13:51 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

The replay command is able to replay multiple branches but when some of
them are based on other replayed branches, their commit should be
replayed onto already replayed commits.

For this purpose, let's store the replayed commit and its original
commit in a key value store, so that we can easily find and reuse a
replayed commit instead of the original one.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c         | 44 ++++++++++++++++++++++++++--------
 t/t3650-replay-basics.sh | 52 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 86 insertions(+), 10 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 08ff9bab5e..c3d53ff0cd 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -223,20 +223,33 @@ static void determine_replay_mode(struct rev_cmdline_info *cmd_info,
 	strset_clear(&rinfo.positive_refs);
 }
 
+static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,
+				    struct commit *commit,
+				    struct commit *fallback)
+{
+	khint_t pos = kh_get_oid_map(replayed_commits, commit->object.oid);
+	if (pos == kh_end(replayed_commits))
+		return fallback;
+	return kh_value(replayed_commits, pos);
+}
+
 static struct commit *pick_regular_commit(struct commit *pickme,
-					  struct commit *last_commit,
+					  kh_oid_map_t *replayed_commits,
+					  struct commit *onto,
 					  struct merge_options *merge_opt,
 					  struct merge_result *result)
 {
-	struct commit *base;
+	struct commit *base, *replayed_base;
 	struct tree *pickme_tree, *base_tree;
 
 	base = pickme->parents->item;
+	replayed_base = mapped_commit(replayed_commits, base, onto);
 
+	result->tree = repo_get_commit_tree(the_repository, replayed_base);
 	pickme_tree = repo_get_commit_tree(the_repository, pickme);
 	base_tree = repo_get_commit_tree(the_repository, base);
 
-	merge_opt->branch1 = short_commit_name(last_commit);
+	merge_opt->branch1 = short_commit_name(replayed_base);
 	merge_opt->branch2 = short_commit_name(pickme);
 	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
 
@@ -250,7 +263,7 @@ static struct commit *pick_regular_commit(struct commit *pickme,
 	merge_opt->ancestor = NULL;
 	if (!result->clean)
 		return NULL;
-	return create_commit(result->tree, pickme, last_commit);
+	return create_commit(result->tree, pickme, replayed_base);
 }
 
 int cmd_replay(int argc, const char **argv, const char *prefix)
@@ -266,6 +279,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct merge_options merge_opt;
 	struct merge_result result;
 	struct strset *update_refs = NULL;
+	kh_oid_map_t *replayed_commits;
 	int i, ret = 0;
 
 	const char * const replay_usage[] = {
@@ -338,21 +352,30 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
 	merge_opt.show_rename_progress = 0;
-
-	result.tree = repo_get_commit_tree(the_repository, onto);
 	last_commit = onto;
+	replayed_commits = kh_init_oid_map();
 	while ((commit = get_revision(&revs))) {
 		const struct name_decoration *decoration;
+		khint_t pos;
+		int hr;
 
 		if (!commit->parents)
 			die(_("replaying down to root commit is not supported yet!"));
 		if (commit->parents->next)
 			die(_("replaying merge commits is not supported yet!"));
 
-		last_commit = pick_regular_commit(commit, last_commit, &merge_opt, &result);
+		last_commit = pick_regular_commit(commit, replayed_commits, onto,
+						  &merge_opt, &result);
 		if (!last_commit)
 			break;
 
+		/* Record commit -> last_commit mapping */
+		pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr);
+		if (hr == 0)
+			BUG("Duplicate rewritten commit: %s\n",
+			    oid_to_hex(&commit->object.oid));
+		kh_value(replayed_commits, pos) = last_commit;
+
 		/* Update any necessary branches */
 		if (advance_name)
 			continue;
@@ -381,13 +404,14 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	}
 
 	merge_finalize(&merge_opt, &result);
-	ret = result.clean;
-
-cleanup:
+	kh_destroy_oid_map(replayed_commits);
 	if (update_refs) {
 		strset_clear(update_refs);
 		free(update_refs);
 	}
+	ret = result.clean;
+
+cleanup:
 	release_revisions(&revs);
 
 	/* Return */
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index d6286f9580..389670262e 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -143,4 +143,56 @@ test_expect_success 'using replay on bare repo to also rebase a contained branch
 	test_cmp expect result-bare
 '
 
+test_expect_success 'using replay to rebase multiple divergent branches' '
+	git replay --onto main ^topic1 topic2 topic4 >result &&
+
+	test_line_count = 2 result &&
+	cut -f 3 -d " " result >new-branch-tips &&
+
+	git log --format=%s $(head -n 1 new-branch-tips) >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual &&
+
+	git log --format=%s $(tail -n 1 new-branch-tips) >actual &&
+	test_write_lines J I M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/topic2 " >expect &&
+	printf "%s " $(head -n 1 new-branch-tips) >>expect &&
+	git rev-parse topic2 >>expect &&
+	printf "update refs/heads/topic4 " >>expect &&
+	printf "%s " $(tail -n 1 new-branch-tips) >>expect &&
+	git rev-parse topic4 >>expect &&
+
+	test_cmp expect result
+'
+
+test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '
+	git -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&
+
+	test_line_count = 4 result &&
+	cut -f 3 -d " " result >new-branch-tips &&
+
+	>expect &&
+	for i in 2 1 3 4
+	do
+		printf "update refs/heads/topic$i " >>expect &&
+		printf "%s " $(grep topic$i result | cut -f 3 -d " ") >>expect &&
+		git -C bare rev-parse topic$i >>expect || return 1
+	done &&
+
+	test_cmp expect result &&
+
+	test_write_lines F C M L B A >expect1 &&
+	test_write_lines E D C M L B A >expect2 &&
+	test_write_lines H G F C M L B A >expect3 &&
+	test_write_lines J I M L B A >expect4 &&
+
+	for i in 1 2 3 4
+	do
+		git -C bare log --format=%s $(grep topic$i result | cut -f 3 -d " ") >actual &&
+		test_cmp expect$i actual || return 1
+	done
+'
+
 test_done
-- 
2.42.0.496.g529a7fda40


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* Re: [PATCH v5 00/14] Introduce new `git replay` command
  2023-10-30 17:18             ` Elijah Newren
@ 2023-11-02 14:44               ` Christian Couder
  0 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-02 14:44 UTC (permalink / raw)
  To: Elijah Newren, Johannes Schindelin
  Cc: git, Junio C Hamano, Patrick Steinhardt, John Cai, Derrick Stolee,
	Phillip Wood, Calvin Wan, Toon Claes, Dragan Simic, Linus Arver

Hi Elijah and Dscho,

On Mon, Oct 30, 2023 at 6:18 PM Elijah Newren <newren@gmail.com> wrote:
> On Sun, Oct 29, 2023 at 7:14 AM Johannes Schindelin
> <Johannes.Schindelin@gmx.de> wrote:
> > On Sat, 28 Oct 2023, Elijah Newren wrote:

> > > I can see why this might sometimes be useful for exclusively linear
> > > history, but it seems to open a can of worms and possibly unfixable
> > > corner cases for non-linear history.  I'd rather not do this, or at
> > > least pull it out of this series and let us discuss it in some follow
> > > up series.  There are some other alternatives that might handle such
> > > usecases better.
> >
> > I find myself wishing for an easy way to reverse commits, if only to
> > switch around the latest two commits while stopped during a rebase.
> >
> > So it would have been nice for me if there had been an easy, worktree-less
> > way to make that happen.
>
> Seems reasonable; we'll definitely want to keep this in mind.
>
> > I guess this would be going in the direction of reordering commits,
> > though, something we deliberately left for later?
>
> Yes, I think that's a good framing for it.

Ok, in the v6 I just sent, a warning() is emitted when `--reverse` is
passed and the option has no effect.

I agree that handling such options in a better way should be left for
later patch series.

Thanks both!

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v5 00/14] Introduce new `git replay` command
  2023-10-29  6:00         ` Elijah Newren
  2023-10-29 14:14           ` Johannes Schindelin
@ 2023-11-02 14:48           ` Christian Couder
  1 sibling, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-02 14:48 UTC (permalink / raw)
  To: Elijah Newren
  Cc: git, Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	John Cai, Derrick Stolee, Phillip Wood, Calvin Wan, Toon Claes,
	Dragan Simic, Linus Arver

On Sun, Oct 29, 2023 at 7:00 AM Elijah Newren <newren@gmail.com> wrote:

> On Tue, Oct 10, 2023 at 5:39 AM Christian Couder
> <christian.couder@gmail.com> wrote:

> >     @@ Documentation/git-replay.txt (new)
> >      +
> >      +NAME
> >      +----
> >     -+git-replay - Replay commits on a different base, without touching working tree
> >     ++git-replay - Replay commits on a new base, works on bare repos too
>
> really minor point: "works on" or "works in" or "works with" ?

I have changed it to "works with", I hope it sounds better. Also I
forgot to talk about this change in the cover letter, sorry about
that.

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v5 00/14] Introduce new `git replay` command
  2023-10-29  6:01           ` Elijah Newren
@ 2023-11-02 14:59             ` Christian Couder
  2023-11-08 12:25               ` Johannes Schindelin
  0 siblings, 1 reply; 208+ messages in thread
From: Christian Couder @ 2023-11-02 14:59 UTC (permalink / raw)
  To: Elijah Newren
  Cc: Johannes Schindelin, git, Junio C Hamano, Patrick Steinhardt,
	John Cai, Derrick Stolee, Phillip Wood, Calvin Wan, Toon Claes,
	Dragan Simic, Linus Arver

On Sun, Oct 29, 2023 at 7:02 AM Elijah Newren <newren@gmail.com> wrote:

> On Thu, Oct 26, 2023 at 6:44 AM Johannes Schindelin
> <Johannes.Schindelin@gmx.de> wrote:
> > On Tue, 10 Oct 2023, Christian Couder wrote:
> >
> [...]
> > >      +  /* requirements/overrides for revs */
> > >     -+  revs.reverse = 1;
> > >     ++  revs.reverse = !revs.reverse;
> > >      +  revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
> > >      +  revs.topo_order = 1;
> > >      +  revs.simplify_history = 0;
> >
> > This still overrides a couple of command-line options, _silently_. I would
> > prefer those three assignments to be moved just before the
> > `setup_revisions()` call.
> >
> > Letting users override these settings may not make much sense, but it
> > makes even less sense to pretend to let them override the settings and
> > then just ignore them without warning. (See also
> > https://en.wikipedia.org/wiki/Principle_of_least_astonishment.)
> >
> > Moving these three assignments before the `setup_revisions()` call would
> > neatly remedy that.
>
> I agree that warnings or error messages would be better.

Ok, a warning() is emitted now in case an command-line option will be
overridden.

> But if we're talking about something short of that, I'd actually argue
> the opposite of what you do here.  I intentionally moved these
> assignments after setup_revisions(), and in my mind, the purpose in
> doing so was to satisfy the Principle of Least Astonishment.  My
> experience with git-fast-export, where some settings are made before
> calling setup_revisions() and then can be overridden, and then do
> completely hideous things, was much worse to me than just admitting
> the flags are bad given the various assumptions the tool makes.  I
> have some patches sitting around to fix fast-export that I never got
> around to upstreaming, but when it came time to implement git-replay,
> I made sure to fix what I viewed as the bigger problem.

I hope you will be able to upstream such changes.

> [...]
> > >     @@ Documentation/git-replay.txt (new)
> > >      +
> > >      +NAME
> > >      +----
> > >     -+git-replay - Replay commits on a different base, without touching working tree
> > >     ++git-replay - Replay commits on a new base, works on bare repos too
> > >      +
> > >      +
> > >      +SYNOPSIS
> >
> > As mentioned in
> > https://lore.kernel.org/git/03460733-0219-c648-5757-db1958f8042e@gmx.de/,
> > I would like the `EXPERIMENTAL` label to be shown prominently here.
> > Probably not only the `SYNOPSIS` as I had originally suggested but also in
> > the `NAME`.

Ok, I have made changes in the v6 I just sent, so that there is
EXPERIMENTAL both in the NAME and SYNOPSIS.

> > Otherwise we may end up with the same situation as with the (from my
> > perspective, failed) `git switch`/`git restore` experiment, where we
> > wanted to explore a better user experience than the overloaded `git
> > checkout` command, only to now be stuck with having to maintain
> > backward-compatibility for `git switch`/`git restore` command-line options
> > that were not meant to be set in stone but to be iterated on, instead. A
> > real-life demonstration of [Hyrum's Law](hyrumslaw.com/), if you like. Or,
> > from a different angle, we re-enacted https://xkcd.com/927/ in a way.

Nit: Hyrum's Law says:

"With a sufficient number of users of an API,
it does not matter what you promise in the contract:
all observable behaviors of your system
will be depended on by somebody."

The doc is part of the contract, which according to this law doesn't
matter. So I don't see why you use this law to suggest a doc change.

> > I'd like to suggest to learn from history and avoid this by tacking on a
> > warning label right at the top of the documentation. We may eventually
> > manage to iterate `git replay` to a point where it is totally capable to
> > supersede `git rebase`, by doing everything the latter does, except
> > better, who knows? But we _do_ need the liberty to make sweeping changes
> > to this new builtin if we want to have a prayer of doing that. And I fear
> > that not even mentioning the EXPERIMENTAL nature right at the top of the
> > manual page would just render us into that undesirable corner.
>
> I fully support this.  Absolutely, 100%.

Ok. Note that as I changed the SYNOPSIS, I also had to change the
usage string, so that it matches the SYNOPSIS, otherwise a test would
fail. So there is "EXPERIMENTAL" in the usage string too.

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v5 00/14] Introduce new `git replay` command
  2023-10-26 13:44         ` [PATCH v5 00/14] Introduce new `git replay` command Johannes Schindelin
  2023-10-29  6:01           ` Elijah Newren
@ 2023-11-02 15:06           ` Christian Couder
  2023-11-08 12:25             ` Johannes Schindelin
  1 sibling, 1 reply; 208+ messages in thread
From: Christian Couder @ 2023-11-02 15:06 UTC (permalink / raw)
  To: Johannes Schindelin, Elijah Newren
  Cc: git, Junio C Hamano, Patrick Steinhardt, John Cai, Derrick Stolee,
	Phillip Wood, Calvin Wan, Toon Claes, Dragan Simic, Linus Arver

Hi Dscho and Elijah,

On Thu, Oct 26, 2023 at 3:44 PM Johannes Schindelin
<Johannes.Schindelin@gmx.de> wrote:

> In addition, I am still a bit uneasy with introducing both the manual page
> and the test script in this commit (see my comments in
> https://lore.kernel.org/git/03460733-0219-c648-5757-db1958f8042e@gmx.de/).
> It would be better to uphold our high standard and introduce scaffolds for
> both files in the first commit, then populate the file contents
> incrementally in the same the patches that introduce the corresponding
> options/features/changes.

I have tried to improve on that in the v6 I just sent, but there are
many patches implementing changes in behavior that I think weren't
worth documenting and testing in `test-tool fast-rebase` (which had no
doc and no test) and that aren't worth documenting and testing
specifically in `git replay` either.

Thanks!

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v6 00/14] Introduce new `git replay` command
  2023-11-02 13:51         ` [PATCH v6 " Christian Couder
                             ` (13 preceding siblings ...)
  2023-11-02 13:51           ` [PATCH v6 14/14] replay: stop assuming replayed branches do not diverge Christian Couder
@ 2023-11-07  2:43           ` Elijah Newren
  2023-11-07  9:43             ` Christian Couder
  2023-11-08 12:19             ` Johannes Schindelin
  2023-11-08 12:47           ` Johannes Schindelin
  2023-11-15 14:33           ` [PATCH v7 " Christian Couder
  16 siblings, 2 replies; 208+ messages in thread
From: Elijah Newren @ 2023-11-07  2:43 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	John Cai, Derrick Stolee, Phillip Wood, Calvin Wan, Toon Claes,
	Dragan Simic, Linus Arver

Hi,

Looking good, just one comment on one small hunk...

On Thu, Nov 2, 2023 at 6:52 AM Christian Couder
<christian.couder@gmail.com> wrote:
>
[...]

>     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
>      -
>         strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
>
>     ++  /*
>     ++   * TODO: For now, let's warn when we see an option that we are
>     ++   * going to override after setup_revisions() below. In the
>     ++   * future we might want to either die() or allow them if we
>     ++   * think they could be useful though.
>     ++   */
>     ++  for (i = 0; i < argc; i++) {
>     ++          if (!strcmp(argv[i], "--reverse") || !strcmp(argv[i], "--date-order") ||
>     ++              !strcmp(argv[i], "--topo-order") || !strcmp(argv[i], "--author-date-order") ||
>     ++              !strcmp(argv[i], "--full-history"))
>     ++                  warning(_("option '%s' will be overridden"), argv[i]);
>     ++  }
>     ++

Two things:

1) Not sure it makes sense to throw a warning with --topo-order or
--full-history, since they would result in a value matching what we
would be setting anyway.

2) This seems like an inefficient way to provide this warning; could
we avoid parsing the arguments for an extra time?  Perhaps instead
  a) set the desired values here, before setup_revisions()
  b) after setup_revisions, check whether these values differ from the
desired values, if so throw a warning.
  c) set the desired values, again
?

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v6 00/14] Introduce new `git replay` command
  2023-11-07  2:43           ` [PATCH v6 00/14] Introduce new `git replay` command Elijah Newren
@ 2023-11-07  9:43             ` Christian Couder
  2023-11-15 14:51               ` Christian Couder
  2023-11-08 12:19             ` Johannes Schindelin
  1 sibling, 1 reply; 208+ messages in thread
From: Christian Couder @ 2023-11-07  9:43 UTC (permalink / raw)
  To: Elijah Newren
  Cc: git, Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	John Cai, Derrick Stolee, Phillip Wood, Calvin Wan, Toon Claes,
	Dragan Simic, Linus Arver

Hi,

On Tue, Nov 7, 2023 at 3:44 AM Elijah Newren <newren@gmail.com> wrote:
> Looking good, just one comment on one small hunk...
>
> On Thu, Nov 2, 2023 at 6:52 AM Christian Couder
> <christian.couder@gmail.com> wrote:
> >
> [...]
>
> >     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
> >      -
> >         strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
> >
> >     ++  /*
> >     ++   * TODO: For now, let's warn when we see an option that we are
> >     ++   * going to override after setup_revisions() below. In the
> >     ++   * future we might want to either die() or allow them if we
> >     ++   * think they could be useful though.
> >     ++   */
> >     ++  for (i = 0; i < argc; i++) {
> >     ++          if (!strcmp(argv[i], "--reverse") || !strcmp(argv[i], "--date-order") ||
> >     ++              !strcmp(argv[i], "--topo-order") || !strcmp(argv[i], "--author-date-order") ||
> >     ++              !strcmp(argv[i], "--full-history"))
> >     ++                  warning(_("option '%s' will be overridden"), argv[i]);
> >     ++  }
> >     ++
>
> Two things:
>
> 1) Not sure it makes sense to throw a warning with --topo-order or
> --full-history, since they would result in a value matching what we
> would be setting anyway.

Yeah, I am not sure about this either. About "--reverse" I think it
makes sense because even if the command is setting the reverse bit, it
would be possible to reverse the reverse like Dscho wanted. But I
agree "--topo-order" and "--full-history" will very unlikely be reused
for anything else in the future.

> 2) This seems like an inefficient way to provide this warning; could
> we avoid parsing the arguments for an extra time?  Perhaps instead
>   a) set the desired values here, before setup_revisions()
>   b) after setup_revisions, check whether these values differ from the
> desired values, if so throw a warning.
>   c) set the desired values, again

Yeah, that would work. The downside is that it would be more difficult
in the warning to tell the user which command line option was
overridden as there are some values changed by different options.
Maybe we can come up with a warning message that is still useful and
enough for now, as the command is supposed to be used by experienced
users. Perhaps something like:

warning(_("some rev walking options will be overridden as '%s' bit in
'struct rev_info' will be forced"), "sort_order");

Thanks!

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v6 00/14] Introduce new `git replay` command
  2023-11-07  2:43           ` [PATCH v6 00/14] Introduce new `git replay` command Elijah Newren
  2023-11-07  9:43             ` Christian Couder
@ 2023-11-08 12:19             ` Johannes Schindelin
  1 sibling, 0 replies; 208+ messages in thread
From: Johannes Schindelin @ 2023-11-08 12:19 UTC (permalink / raw)
  To: Elijah Newren
  Cc: Christian Couder, git, Junio C Hamano, Patrick Steinhardt,
	John Cai, Derrick Stolee, Phillip Wood, Calvin Wan, Toon Claes,
	Dragan Simic, Linus Arver

[-- Attachment #1: Type: text/plain, Size: 1402 bytes --]

Hi,

On Mon, 6 Nov 2023, Elijah Newren wrote:

> On Thu, Nov 2, 2023 at 6:52 AM Christian Couder
> <christian.couder@gmail.com> wrote:
> >
> [...]
>
> >     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
> >      -
> >         strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
> >
> >     ++  /*
> >     ++   * TODO: For now, let's warn when we see an option that we are
> >     ++   * going to override after setup_revisions() below. In the
> >     ++   * future we might want to either die() or allow them if we
> >     ++   * think they could be useful though.
> >     ++   */
> >     ++  for (i = 0; i < argc; i++) {
> >     ++          if (!strcmp(argv[i], "--reverse") || !strcmp(argv[i], "--date-order") ||
> >     ++              !strcmp(argv[i], "--topo-order") || !strcmp(argv[i], "--author-date-order") ||
> >     ++              !strcmp(argv[i], "--full-history"))
> >     ++                  warning(_("option '%s' will be overridden"), argv[i]);
> >     ++  }
> >     ++
>
> [...] This seems like an inefficient way to provide this warning;

Not only inefficient: think about the false positive when looking at a
command-line containing `--grep --reverse`. In this instance, `--reverse`
is an argument of the `--grep` option, but would be mistaken for an option
in its own right.

Ciao,
Johannes

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v5 00/14] Introduce new `git replay` command
  2023-11-02 14:59             ` Christian Couder
@ 2023-11-08 12:25               ` Johannes Schindelin
  0 siblings, 0 replies; 208+ messages in thread
From: Johannes Schindelin @ 2023-11-08 12:25 UTC (permalink / raw)
  To: Christian Couder
  Cc: Elijah Newren, git, Junio C Hamano, Patrick Steinhardt, John Cai,
	Derrick Stolee, Phillip Wood, Calvin Wan, Toon Claes,
	Dragan Simic, Linus Arver

[-- Attachment #1: Type: text/plain, Size: 2028 bytes --]

Hi Christian,

On Thu, 2 Nov 2023, Christian Couder wrote:

> On Sun, Oct 29, 2023 at 7:02 AM Elijah Newren <newren@gmail.com> wrote:
>
> > On Thu, Oct 26, 2023 at 6:44 AM Johannes Schindelin
> > <Johannes.Schindelin@gmx.de> wrote:
> > >
> > > As mentioned in
> > > https://lore.kernel.org/git/03460733-0219-c648-5757-db1958f8042e@gmx.de/,
> > > I would like the `EXPERIMENTAL` label to be shown prominently here.
> > > Probably not only the `SYNOPSIS` as I had originally suggested but
> > > also in the `NAME`.
>
> Ok, I have made changes in the v6 I just sent, so that there is
> EXPERIMENTAL both in the NAME and SYNOPSIS.
>
> > > Otherwise we may end up with the same situation as with the (from my
> > > perspective, failed) `git switch`/`git restore` experiment, where we
> > > wanted to explore a better user experience than the overloaded `git
> > > checkout` command, only to now be stuck with having to maintain
> > > backward-compatibility for `git switch`/`git restore` command-line options
> > > that were not meant to be set in stone but to be iterated on, instead. A
> > > real-life demonstration of [Hyrum's Law](hyrumslaw.com/), if you like. Or,
> > > from a different angle, we re-enacted https://xkcd.com/927/ in a way.
>
> Nit: Hyrum's Law says:
>
> "With a sufficient number of users of an API,
> it does not matter what you promise in the contract:
> all observable behaviors of your system
> will be depended on by somebody."
>
> The doc is part of the contract, which according to this law doesn't
> matter. So I don't see why you use this law to suggest a doc change.

You're right. In addition to the documentation (where we definitely need
to state the experimental nature of the command), we may want to consider
adding the `EXPERIMENTAL` label not only to the output of `git replay -h`,
but show also a warning for every `git replay` invocation cautioning users
against depending on the current command-line options of this command.

Ciao,
Johannes

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v5 00/14] Introduce new `git replay` command
  2023-11-02 15:06           ` Christian Couder
@ 2023-11-08 12:25             ` Johannes Schindelin
  0 siblings, 0 replies; 208+ messages in thread
From: Johannes Schindelin @ 2023-11-08 12:25 UTC (permalink / raw)
  To: Christian Couder
  Cc: Elijah Newren, git, Junio C Hamano, Patrick Steinhardt, John Cai,
	Derrick Stolee, Phillip Wood, Calvin Wan, Toon Claes,
	Dragan Simic, Linus Arver

[-- Attachment #1: Type: text/plain, Size: 1050 bytes --]

Hi Christian,

On Thu, 2 Nov 2023, Christian Couder wrote:

> On Thu, Oct 26, 2023 at 3:44 PM Johannes Schindelin
> <Johannes.Schindelin@gmx.de> wrote:
>
> > In addition, I am still a bit uneasy with introducing both the manual page
> > and the test script in this commit (see my comments in
> > https://lore.kernel.org/git/03460733-0219-c648-5757-db1958f8042e@gmx.de/).
> > It would be better to uphold our high standard and introduce scaffolds for
> > both files in the first commit, then populate the file contents
> > incrementally in the same the patches that introduce the corresponding
> > options/features/changes.
>
> I have tried to improve on that in the v6 I just sent, but there are
> many patches implementing changes in behavior that I think weren't
> worth documenting and testing in `test-tool fast-rebase` (which had no
> doc and no test) and that aren't worth documenting and testing
> specifically in `git replay` either.

Thank you for putting in the effort. I appreciate it very much.

Ciao,
Johannes

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v6 00/14] Introduce new `git replay` command
  2023-11-02 13:51         ` [PATCH v6 " Christian Couder
                             ` (14 preceding siblings ...)
  2023-11-07  2:43           ` [PATCH v6 00/14] Introduce new `git replay` command Elijah Newren
@ 2023-11-08 12:47           ` Johannes Schindelin
  2023-11-15 14:46             ` Christian Couder
  2023-11-15 14:33           ` [PATCH v7 " Christian Couder
  16 siblings, 1 reply; 208+ messages in thread
From: Johannes Schindelin @ 2023-11-08 12:47 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Patrick Steinhardt, Elijah Newren, John Cai,
	Derrick Stolee, Phillip Wood, Calvin Wan, Toon Claes,
	Dragan Simic, Linus Arver

Hi Christian,

On Thu, 2 Nov 2023, Christian Couder wrote:

> # Range-diff between v5 and v6
>
> (Sorry it's very long mostly due to doc and test changes over a number
> of patches.)

I am grateful for the detailed range-diff that let's me focus exclusively
on what changed between iterations.

>  1:  72c34a0eba =  1:  fac0a9dff4 t6429: remove switching aspects of fast-rebase
>  2:  f85e6c823c !  2:  8a605ddef8 replay: introduce new builtin
>     @@ Commit message
>          For now, this is just a rename from `t/helper/test-fast-rebase.c` into
>          `builtin/replay.c` with minimal changes to make it build appropriately.
>
>     +    There is a stub documentation and a stub test script though.
>     +
>          Subsequent commits will flesh out its capabilities and make it a more
>          standard regular builtin.
>
>     @@ .gitignore
>       /git-rerere
>       /git-reset
>
>     + ## Documentation/git-replay.txt (new) ##
>     +@@
>     ++git-replay(1)
>     ++=============
>     ++
>     ++NAME
>     ++----
>     ++git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos too
>     ++
>     ++
>     ++SYNOPSIS
>     ++--------
>     ++[verse]
>     ++'git replay' --onto <newbase> <revision-range>... # EXPERIMENTAL

Technically, at this stage `git replay` requires precisely 5 arguments, so
the `<revision>...` is incorrect and should be `<upstream> <branch>`
instead. But it's not worth a new iteration to fix this.

>     ++
>     ++DESCRIPTION
>     ++-----------
>     ++
>     ++Takes a range of commits and replays them onto a new location.
>     ++
>     ++THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
>     ++
>     ++OPTIONS
>     ++-------
>     ++
>     ++--onto <newbase>::
>     ++  Starting point at which to create the new commits.  May be any
>     ++  valid commit, and not just an existing branch name.
>     ++

Traditionally, this would be a place to describe the `<revision>` argument
(or, in this patch, to reflect the current state of `builtin/replay.c`,
the `<upstream> <branch>` arguments instead).

>     ++EXIT STATUS
>     ++-----------
>     ++
>     ++For a successful, non-conflicted replay, the exit status is 0.  When
>     ++the replay has conflicts, the exit status is 1.  If the replay is not
>     ++able to complete (or start) due to some kind of error, the exit status
>     ++is something other than 0 or 1.
>     ++
>     ++GIT
>     ++---
>     ++Part of the linkgit:git[1] suite
>     +
>       ## Makefile ##
> [... snipping non-controversial changes ...]
>  6:  37d545d5d6 !  6:  edafe4846f replay: change rev walking options
> [... snipping changes Elijah already talked about ...]
>  7:  2943f08926 =  7:  b81574744a replay: add an important FIXME comment about gpg signing
>  8:  f81962ba41 =  8:  b08ad2b2f1 replay: remove progress and info output
>  9:  236747497e !  9:  5099c94d2e replay: remove HEAD related sanity check
> {... snipping non-controversial changes ...]

Thank you!

Ciao,
Johannes

^ permalink raw reply	[flat|nested] 208+ messages in thread

* [PATCH v7 00/14] Introduce new `git replay` command
  2023-11-02 13:51         ` [PATCH v6 " Christian Couder
                             ` (15 preceding siblings ...)
  2023-11-08 12:47           ` Johannes Schindelin
@ 2023-11-15 14:33           ` Christian Couder
  2023-11-15 14:33             ` [PATCH v7 01/14] t6429: remove switching aspects of fast-rebase Christian Couder
                               ` (15 more replies)
  16 siblings, 16 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-15 14:33 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

# Intro

`git replay` has initially been developed entirely by Elijah Newren
mostly last year (2022) at:

https://github.com/newren/git/commits/replay

I took over this year to polish and upstream it as GitLab is
interested in replacing libgit2, and for that purpose needs a command
to do server side (so without using a worktree) rebases, cherry-picks
and reverts.

I reduced the number of commits and features in this patch series,
compared to what Elijah already developed. Especially I stopped short
of replaying merge commits and replaying interactively. These and
other features might be upstreamed in the future after this patch
series has graduated.

The focus in this series is to make it a good plumbing command that
can already be used server side and that replaces the "fast-rebase"
test-tool command. So things to make it easier to use on the command
line, and more advanced features (like replaying merges) are left out.

It looks like GitHub has actually already been using version 3 of this
patch series in production with good results. See:

https://github.blog/2023-07-27-scaling-merge-ort-across-github/
https://lore.kernel.org/git/304f2a49-5e05-7655-9f87-2011606df5db@gmx.de/

# Content of this cover letter

The "Quick Overview" and "Reasons for diverging from cherry-pick &
rebase" sections just below are describing the purpose of the new
command in the big scheme of things. They are taken from Elijah's
design notes
(https://github.com/newren/git/blob/replay/replay-design-notes.txt)
and describe what we want this command to become and the reasons for
that, not what the command is after only this patch series. Also these
design notes were written at least one year ago, so parts of those 2
sections are not true anymore. I have added Phillip Wood's or Felipe
Contreras' notes (thanks to them) where that's the case, but some now
flawed parts may have missed.

After these two sections, starting with the "Important limitations"
section, you will find sections describing what is actually in this
patch series.

More interesting material is available in Elijah's design notes like
an "Intro via examples"
(https://github.com/newren/git/blob/replay/replay-design-notes.txt#L37-L132),
a discussion about "Preserving topology, replaying merges"
(https://github.com/newren/git/blob/replay/replay-design-notes.txt#L264-L341)
and a "Current status" section describing Elijah's work
(https://github.com/newren/git/blob/replay/replay-design-notes.txt#L344-L392)
before I started working on upstreaming it.

I have not included this material here though, as the documentation
added by this patch series for the `git replay` command already
includes an "EXAMPLES" section, and other sections of Elijah's design
notes might not be interesting for now. Also this cover letter is
already pretty long.  But reviewers can refer to the links above if
they think it can help.

# Quick Overview (from Elijah's design notes)

`git replay`, at a basic level, can perhaps be thought of as a
"default-to-dry-run rebase" -- meaning no updates to the working tree,
or to the index, or to any references.  However, it differs from
rebase in that it:

  * Works for branches that aren't checked out

  * Works in a bare repository

  * Can replay multiple branches simultaneously (with or without common
    history in the range being replayed)

  * Preserves relative topology by default (merges are replayed too in
    Elijah's original work, not in this series)

  * Focuses on performance

  * Has several altered defaults as a result of the above

I sometimes think of `git replay` as "fast-replay", a patch-based
analogue to the snapshot-based fast-export & fast-import tools.

# Reasons for diverging from cherry-pick & rebase (from Elijah's
  design notes)

There are multiple reasons to diverge from the defaults in cherry-pick and
rebase.

* Server side needs

  * Both cherry-pick and rebase, via the sequencer, are heavily tied
    to updating the working tree, index, some refs, and a lot of
    control files with every commit replayed, and invoke a mess of
    hooks[1] that might be hard to avoid for backward compatibility
    reasons (at least, that's been brought up a few times on the
    list).

  * cherry-pick and rebase both fork various subprocesses
    unnecessarily, but somewhat intrinsically in part to ensure the
    same hooks are called that old scripted implementations would have
    called.

    Note: since 356ee4659bb (sequencer: try to commit without forking
    'git commit', 2017-11-24) cherry-pick and rebase do not fork
    subprocesses other than hooks for the cases covered by this patch
    series (i.e. they do not fork "git commit" for simple picks).

  * "Dry run" behavior, where there are no updates to worktree, index,
    or even refs might be important.

  * Should not assume users only want to operate on HEAD (see next
    section)

* Decapitate HEAD-centric assumptions

  * cherry-pick forces commits to be played on top of HEAD;
    inflexible.

  * rebase assumes the range of commits to be replayed is
    upstream..HEAD by default, though it allows one to replay
    upstream..otherbranch -- but it still forcibly and needlessly
    checks out 'otherbranch' before starting to replay things.

    Note: since 767a9c417eb (rebase -i: stop checking out the tip of
    the branch to rebase, 2020-01-24) it's not true that rebase
    forcibly and needlessly checks out 'otherbranch'.

  * Assuming HEAD is involved severely limits replaying multiple
    (possibly divergent) branches.

    Note: since 89fc0b53fdb (rebase: update refs from 'update-ref'
    commands, 2022-07-19) the sequencer can update multiple
    branches. The issue with divergent branch is with command line
    arguments and the todo list generation rather than the
    capabilities of the sequencer.

  * Once you stop assuming HEAD has a certain meaning, there's not
    much reason to have two separate commands anymore (except for the
    funny extra not-necessarily-compatible options both have gained
    over time).

  * (Micro issue: Assuming HEAD is involved also makes it harder for
    new users to learn what rebase means and does; it makes command
    lines hard to parse.  Not sure I want to harp on this too much, as
    I have a suspicion I might be creating a tool for experts with
    complicated use cases, but it's a minor quibble.)

* Performance

  * jj is slaughtering us on rebase speed[2].  I would like us to become
    competitive.  (I dropped a few comments in the link at [2] about why
    git is currently so bad.)

  * From [3], there was a simple 4-patch series in linux.git that took
    53 seconds to rebase.  Switching to ort dropped it to 16 seconds.
    While that sounds great, only 11 *milliseconds* were needed to do
    the actual merges.  That means almost *all* the time (>99%) was
    overhead!  Big offenders:

    * --reapply-cherry-picks should be the default

    * can_fast_forward() should be ripped out, and perhaps other extraneous
      revision walks

      Note: d42c9ffa0f (rebase: factor out branch_base calculation,
      2022-10-17) might already deal with that (according to Felipe
      Contreras).

    * avoid updating working tree, index, refs, reflogs, and control
      structures except when needed (e.g. hitting a conflict, or operation
      finished)

  * Other performance ideas (mostly for future work, not in this
    series)

    * single-file control structures instead of directory of files
      (when doing interactive things which is in Elijah's original
      work, but not in this series)

    * avoid forking subprocesses unless explicitly requested (e.g.
      --exec, --strategy, --run-hooks).  For example, definitely do not
      invoke `git commit` or `git merge`.

    * Sanitize hooks:

      * dispense with all per-commit hooks for sure (pre-commit,
        post-commit, post-checkout).

      * pre-rebase also seems to assume exactly 1 ref is written, and
        invoking it repeatedly would be stupid.  Plus, it's specific
        to "rebase".  So...ignore?  (Stolee's --ref-update option for
        rebase probably broke the pre-rebase assumptions already...)

      * post-rewrite hook might make sense, but fast-import got
        exempted, and I think of replay like a patch-based analogue
        to the snapshot-based fast-import.

    * When not running server side, resolve conflicts in a sparse-cone
      sparse-index worktree to reduce number of files written to a
      working tree.  (See below as well.)

    * [High risk of possible premature optimization] Avoid large
      numbers of newly created loose objects, when replaying large
      numbers of commits.  Two possibilities: (1) Consider using
      tmp-objdir and pack objects from the tmp-objdir at end of
      exercise, (2) Lift code from git-fast-import to immediately
      stuff new objects into a pack?

* Multiple branches and non-checked out branches

  * The ability to operate on non-checked out branches also implies
    that we should generally be able to replay when in a dirty working
    tree (exception being when we expect to update HEAD and any of the
    dirty files is one that needs to be updated by the replay).

  * Also, if we are operating locally on a non-checked out branch and
    hit a conflict, we should have a way to resolve the conflict
    without messing with the user's work on their current
    branch. (This is not is this patch series though.)

    * Idea: new worktree with sparse cone + sparse index checkout,
      containing only files in the root directory, and whatever is
      necessary to get the conflicts

    * Companion to above idea: control structures should be written to
      $GIT_COMMON_DIR/replay-${worktree}, so users can have multiple
      replay sessions, and so we know which worktrees are associated
      with which replay operations.

  - [1] https://lore.kernel.org/git/pull.749.v3.git.git.1586044818132.gitgitgadget@gmail.com/
  - [2] https://github.com/martinvonz/jj/discussions/49
  - [3] https://lore.kernel.org/git/CABPp-BE48=97k_3tnNqXPjSEfA163F8hoE+HY0Zvz1SWB2B8EA@mail.gmail.com/

# Important limitations

* The code exits with code 1 if there are any conflict. No
  resumability. No nice output. No interactivity. No special exit code
  depending on the reason.

* When a commit becomes empty as it is replayed, it is still replayed
  as an empty commit, instead of being dropped.

* No replaying merges, nor root commits. Only regular commits.

* Signed commits are not properly handled. It's not clear what to do
  to such commits when replaying on the server side.

* Notes associated with replayed commits are not updated nor
  duplicated. (Thanks to Phillip Wood for noticing.)

# Commit overview

* 1/14 t6429: remove switching aspects of fast-rebase

    Preparatory commit to make it easier to later replace the
    fast-rebase test-tool by `git replay` without breaking existing
    tests.

* 2/14 replay: introduce new builtin

    This creates a minimal `git replay` command by moving the code
    from the `fast-rebase` test helper from `t/helper/` into
    `builtin/` and doing some renames and a few other needed changes.
    Since v6, there are only a few doc improvements as suggested by
    Dscho.

* - 3/14 replay: start using parse_options API
  - 4/14 replay: die() instead of failing assert()
  - 5/14 replay: introduce pick_regular_commit()
  - 6/14 replay: change rev walking options
  - 7/14 replay: add an important FIXME comment about gpg signing
  - 8/14 replay: remove progress and info output
  - 9/14 replay: remove HEAD related sanity check

    These slowly change the command to make it behave more like
    regular commands and to start cleaning up its output. In patch
    6/14 (replay: change rev walking options) there are some changes
    compared to v6 as suggested by Elijah and Dscho. We are now
    setting the rev walking bits we want before the call to
    setup_revisions(). And then after that call we check if these bits
    have been changed, and if that's the case we warn that we are
    going to override those changes and we override the bits.

* 10/14 replay: make it a minimal server side command

    After the cleaning up in previous commits, it's now time to
    radically change the way it works by stopping it to do ref
    updates, to update the index and worktree, to consider HEAD as
    special. Instead just make it output commands that should be
    passed to `git update-ref --stdin`.

* 11/14 replay: use standard revision ranges

    Start adding new interesting features and also documentation and
    tests, as the interface of the command is cristalizing into its
    final form. Since v6 "Takes a range of commits" has been replaced
    with "Takes ranges of commits" to reflect the fact that it can
    accept more than one <revision-range>.

* - 12/14 replay: add --advance or 'cherry-pick' mode
  - 13/14 replay: add --contained to rebase contained branches

    Add new options and features to the command.

* 14/14 replay: stop assuming replayed branches do not diverge

    This adds another interesting feature, as well as related
    documentation and tests.

# Notes about `fast-rebase`, tests and documentation

The `fast-rebase` test-tool helper was developed by Elijah to
experiment with a rebasing tool that would be developed from scratch
based on his merge-ort work, could be used to test that merge-ort
work, and would not have the speed and interface limitations of `git
rebase` or `git cherry-pick`.

This `fast-rebase` helper was used before this series in:

t6429-merge-sequence-rename-caching.sh

So when `git replay` is created from `fast-rebase` in patch 2/14, the
t6429 test script is also converted to use `git replay`. This ensures
that `git replay` doesn't break too badly during the first 10 patches
in this patch series.

Tests and documentation are introduced specifically for `git replay`
as soon as patch 2/14, but they are not much improved since around
11/14 as it doesn't make much sense to document and test behavior that
we know is going to change soon.

# Possibly controversial issues 

* bare or not bare: this series works towards a plumbing command with
  the end goal of it being usable and used first on bare repos,
  contrary to existing commands like `git rebase` and `git
  cherry-pick`. The tests check that the command works on both bare
  and non-bare repo though.

* exit status: a successful, non-conflicted replay exits with code
  0. When the replay has conflicts, the exit status is 1. If the
  replay is not able to complete (or start) due to some kind of error,
  the exit status is something other than 0 or 1. There are a few
  tests checking that. It has been suggested in an internal review
  that conflicts might want to get a more specific error code as an
  error code of 1 might be quite easy to return by accident. It
  doesn't seem to me from their docs (which might want to be improved,
  I didn't look at the code) that other commands like `git merge` and
  `git rebase` exit with a special error code in case of conflict.

* make worktree and index changes optional: commit 10/14 stops
  updating the index and worktree, but it might be better especially
  for cli users to make that optional. The issue is that this would
  make the command more complex while we are developing a number of
  important features so that the command can be used on bare repos. It
  seems that this should rather be done in an iterative improvement
  after the important features have landed.

* --advance and --contained: these two advanced options might not
  belong to this first series and could perhaps be added in a followup
  series in separate commits. On the other hand the code for
  --contained seems involved with the code of --advance and it's nice
  to see soon that git replay can indeed do cherry-picking and rebase
  many refs at once, and this way fullfil these parts of its promise.

* replaying diverging branches: 14/14 the last patch in the series,
  which allow replaying diverging branches, can be seen as a
  fundamental fix or alternatively as adding an interesting
  feature. So it's debatable if it should be in its own patch along
  with its own tests as in this series, or if it should be merged into
  a previous patch and which one.

* only 2 patches: this patch series can be seen from a high level
  point of view as 1) introducing the new `git replay` command, and 2)
  using `git replay` to replace, and get rid of, the fast-rebase
  test-tool command. The fact that not much of the original
  fast-rebase code and interface is left would agree with that point
  of view. On the other hand, fast-rebase can also be seen as a first
  iteration towards `git replay`. So it can also make sense to see how
  `git replay` evolved from it.

# Changes between v6 and v7

Thanks to Dscho, Linus Arver and Dragan Simic for their suggestions on
the previous version! The few changes compared to v6 are:

* The patch series was rebased onto master at dadef801b3 (Git
  2.43-rc1, 2023-11-08). This is to make it stand on a possibly more
  stable base.

* In patch 2/14 (replay: introduce new builtin), there are a few doc
  improvements as suggested by Dscho, as we now talk about the
  <oldbase> and <branch> arguments in the description.

* In patch 6/14 (replay: change rev walking options), as suggested by
  Elijah and Dscho, we are now setting the rev walking bits we want
  before the call to setup_revisions(). And then after that call we
  check if these bits have been changed, and if that's the case we
  warn that we are going to override those changes and we override the
  bits.

* In patch 11/14 (replay: use standard revision ranges), "Takes a
  range of commits" has been replaced with "Takes ranges of commits"
  to reflect the fact that it can accept more than one
  <revision-range>.

CI tests seem to pass according to:

https://github.com/chriscool/git/actions/runs/6878406898

(Except the "sparse" test, but failure doesn't seem to be related. And
sorry I stopped waiting for the MacOS and ASAN tests to finish after
23 minutes.)

# Range-diff between v6 and v7

(Sorry it looks like patch 6/14 in v7 is considered to be completely
different from what it was in v6, so the range-diff is not showing
differences between them.)

 1:  fac0a9dff4 =  1:  cddcd967b2 t6429: remove switching aspects of fast-rebase
 2:  bec2eb8928 !  2:  c8476fb093 replay: introduce new builtin
    @@ Documentation/git-replay.txt (new)
     +DESCRIPTION
     +-----------
     +
    -+Takes a range of commits and replays them onto a new location.
    ++Takes a range of commits, specified by <oldbase> and <branch>, and
    ++replays them onto a new location (see `--onto` option below).
     +
     +THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
     +
 3:  b0cdfdc0c3 =  3:  43322abd1e replay: start using parse_options API
 4:  c3403f0b9d =  4:  6524c7f045 replay: die() instead of failing assert()
 5:  4188eeac30 =  5:  05d0efa3cb replay: introduce pick_regular_commit()
 6:  b7b4d9001e <  -:  ---------- replay: change rev walking options
 -:  ---------- >  6:  c7a5aad3d6 replay: change rev walking options
 7:  c57577a9b8 =  7:  01f35f924b replay: add an important FIXME comment about gpg signing
 8:  e78be50f3d =  8:  1498b24bad replay: remove progress and info output
 9:  e4c79b676f =  9:  6786fc147b replay: remove HEAD related sanity check
10:  8d89f1b733 ! 10:  9a24dbb530 replay: make it a minimal server side command
    @@ Commit message
         Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
     
      ## Documentation/git-replay.txt ##
    -@@ Documentation/git-replay.txt: SYNOPSIS
    - DESCRIPTION
    +@@ Documentation/git-replay.txt: DESCRIPTION
      -----------
      
    --Takes a range of commits and replays them onto a new location.
    -+Takes a range of commits and replays them onto a new location. Leaves
    -+the working tree and the index untouched, and updates no
    -+references. The output of this command is meant to be used as input to
    + Takes a range of commits, specified by <oldbase> and <branch>, and
    +-replays them onto a new location (see `--onto` option below).
    ++replays them onto a new location (see `--onto` option below). Leaves
    ++the working tree and the index untouched, and updates no references.
    ++The output of this command is meant to be used as input to
     +`git update-ref --stdin`, which would update the relevant branches.
      
      THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
        struct merge_result result;
     -  struct strbuf reflog_msg = STRBUF_INIT;
        struct strbuf branch_name = STRBUF_INIT;
    -   int i, ret = 0;
    +   int ret = 0;
      
     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix)
        onto = peel_committish(onto_name);
11:  3d433a1322 ! 11:  ad6ca2fbef replay: use standard revision ranges
    @@ Documentation/git-replay.txt: git-replay - EXPERIMENTAL: Replay commits on a new
      
      DESCRIPTION
      -----------
    -@@ Documentation/git-replay.txt: DESCRIPTION
    - Takes a range of commits and replays them onto a new location. Leaves
    - the working tree and the index untouched, and updates no
    - references. The output of this command is meant to be used as input to
    + 
    +-Takes a range of commits, specified by <oldbase> and <branch>, and
    +-replays them onto a new location (see `--onto` option below). Leaves
    ++Takes ranges of commits and replays them onto a new location. Leaves
    + the working tree and the index untouched, and updates no references.
    + The output of this command is meant to be used as input to
     -`git update-ref --stdin`, which would update the relevant branches.
     +`git update-ref --stdin`, which would update the relevant branches
     +(see the OUTPUT section below).
    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
        struct merge_options merge_opt;
        struct merge_result result;
     -  struct strbuf branch_name = STRBUF_INIT;
    -   int i, ret = 0;
    +   int ret = 0;
      
        const char * const replay_usage[] = {
     -          N_("git replay --onto <newbase> <oldbase> <branch> # EXPERIMENTAL"),
    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
     -  strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
     -
        /*
    -    * TODO: For now, let's warn when we see an option that we are
    -    * going to override after setup_revisions() below. In the
    +    * Set desired values for rev walking options here. If they
    +    * are changed by some user specified option in setup_revisions()
     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix)
    -                   warning(_("option '%s' will be overridden"), argv[i]);
    -   }
    +   revs.topo_order = 1;
    +   revs.simplify_history = 0;
      
     -  if (setup_revisions(rev_walk_args.nr, rev_walk_args.v, &revs, NULL) > 1) {
     -          ret = error(_("unhandled options"));
    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
        }
      
     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix)
    -   revs.topo_order = 1;
    -   revs.simplify_history = 0;
    +           revs.simplify_history = 0;
    +   }
      
     -  strvec_clear(&rev_walk_args);
     -
12:  cca8105382 ! 12:  081864ed5f replay: add --advance or 'cherry-pick' mode
    @@ builtin/replay.c: static struct commit *pick_regular_commit(struct commit *pickm
        struct merge_options merge_opt;
        struct merge_result result;
     +  struct strset *update_refs = NULL;
    -   int i, ret = 0;
    +   int ret = 0;
      
        const char * const replay_usage[] = {
     -          N_("git replay --onto <newbase> <revision-range>... # EXPERIMENTAL"),
    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
      
        /*
     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix)
    -   revs.topo_order = 1;
    -   revs.simplify_history = 0;
    +           revs.simplify_history = 0;
    +   }
      
     +  determine_replay_mode(&revs.cmdline, onto_name, &advance_name,
     +                        &onto, &update_refs);
13:  92287a2cc8 ! 13:  19c4016c7c replay: add --contained to rebase contained branches
    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
        struct rev_info revs;
        struct commit *last_commit = NULL;
     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix)
    -   int i, ret = 0;
    +   int ret = 0;
      
        const char * const replay_usage[] = {
     -          N_("git replay (--onto <newbase> | --advance <branch>) <revision-range>... # EXPERIMENTAL"),
14:  529a7fda40 ! 14:  29556bcc86 replay: stop assuming replayed branches do not diverge
    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
        struct merge_result result;
        struct strset *update_refs = NULL;
     +  kh_oid_map_t *replayed_commits;
    -   int i, ret = 0;
    +   int ret = 0;
      
        const char * const replay_usage[] = {
     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix)


Elijah Newren (14):
  t6429: remove switching aspects of fast-rebase
  replay: introduce new builtin
  replay: start using parse_options API
  replay: die() instead of failing assert()
  replay: introduce pick_regular_commit()
  replay: change rev walking options
  replay: add an important FIXME comment about gpg signing
  replay: remove progress and info output
  replay: remove HEAD related sanity check
  replay: make it a minimal server side command
  replay: use standard revision ranges
  replay: add --advance or 'cherry-pick' mode
  replay: add --contained to rebase contained branches
  replay: stop assuming replayed branches do not diverge

 .gitignore                               |   1 +
 Documentation/git-replay.txt             | 127 +++++++
 Makefile                                 |   2 +-
 builtin.h                                |   1 +
 builtin/replay.c                         | 445 +++++++++++++++++++++++
 command-list.txt                         |   1 +
 git.c                                    |   1 +
 t/helper/test-fast-rebase.c              | 241 ------------
 t/helper/test-tool.c                     |   1 -
 t/helper/test-tool.h                     |   1 -
 t/t3650-replay-basics.sh                 | 198 ++++++++++
 t/t6429-merge-sequence-rename-caching.sh |  45 ++-
 12 files changed, 800 insertions(+), 264 deletions(-)
 create mode 100644 Documentation/git-replay.txt
 create mode 100644 builtin/replay.c
 delete mode 100644 t/helper/test-fast-rebase.c
 create mode 100755 t/t3650-replay-basics.sh


base-commit: dadef801b365989099a9929e995589e455c51fed
prerequisite-patch-id: 6df236178013b77ca82d22653c1ab78477e081ce
-- 
2.43.0.rc1.15.g29556bcc86


^ permalink raw reply	[flat|nested] 208+ messages in thread

* [PATCH v7 01/14] t6429: remove switching aspects of fast-rebase
  2023-11-15 14:33           ` [PATCH v7 " Christian Couder
@ 2023-11-15 14:33             ` Christian Couder
  2023-11-15 14:33             ` [PATCH v7 02/14] replay: introduce new builtin Christian Couder
                               ` (14 subsequent siblings)
  15 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-15 14:33 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

At the time t6429 was written, merge-ort was still under development,
did not have quite as many tests, and certainly was not widely deployed.
Since t6429 was exercising some codepaths just a little differently, we
thought having them also test the "merge_switch_to_result()" bits of
merge-ort was useful even though they weren't intrinsic to the real
point of these tests.

However, the value provided by doing extra testing of the
"merge_switch_to_result()" bits has decreased a bit over time, and it's
actively making it harder to refactor `test-tool fast-rebase` into `git
replay`, which we are going to do in following commits.  Dispense with
these bits.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 t/helper/test-fast-rebase.c              | 9 +--------
 t/t6429-merge-sequence-rename-caching.sh | 9 +++++++--
 2 files changed, 8 insertions(+), 10 deletions(-)

diff --git a/t/helper/test-fast-rebase.c b/t/helper/test-fast-rebase.c
index cac20a72b3..2bfab66b1b 100644
--- a/t/helper/test-fast-rebase.c
+++ b/t/helper/test-fast-rebase.c
@@ -194,7 +194,7 @@ int cmd__fast_rebase(int argc, const char **argv)
 		last_commit = create_commit(result.tree, commit, last_commit);
 	}
 
-	merge_switch_to_result(&merge_opt, head_tree, &result, 1, !result.clean);
+	merge_finalize(&merge_opt, &result);
 
 	if (result.clean < 0)
 		exit(128);
@@ -213,9 +213,6 @@ int cmd__fast_rebase(int argc, const char **argv)
 		}
 		if (create_symref("HEAD", branch_name.buf, reflog_msg.buf) < 0)
 			die(_("unable to update HEAD"));
-
-		prime_cache_tree(the_repository, the_repository->index,
-				 result.tree);
 	} else {
 		fprintf(stderr, "\nAborting: Hit a conflict.\n");
 		strbuf_addf(&reflog_msg, "rebase progress up to %s",
@@ -228,10 +225,6 @@ int cmd__fast_rebase(int argc, const char **argv)
 			die("Failed to update %s", argv[4]);
 		}
 	}
-	if (write_locked_index(&the_index, &lock,
-			       COMMIT_LOCK | SKIP_IF_UNCHANGED))
-		die(_("unable to write %s"), get_index_file());
-
 	ret = (result.clean == 0);
 cleanup:
 	strbuf_release(&reflog_msg);
diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh
index d02fa16614..75d3fd2dba 100755
--- a/t/t6429-merge-sequence-rename-caching.sh
+++ b/t/t6429-merge-sequence-rename-caching.sh
@@ -72,6 +72,7 @@ test_expect_success 'caching renames does not preclude finding new ones' '
 		git switch upstream &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream~1..topic
 
 		git ls-files >tracked-files &&
@@ -200,6 +201,7 @@ test_expect_success 'rename same file identically, then reintroduce it' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream~1..topic &&
 
 		git ls-files >tracked &&
@@ -277,6 +279,7 @@ test_expect_success 'rename same file identically, then add file to old dir' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream~1..topic &&
 
 		git ls-files >tracked &&
@@ -356,8 +359,6 @@ test_expect_success 'cached dir rename does not prevent noticing later conflict'
 		test_must_fail test-tool fast-rebase --onto HEAD upstream~1 topic >output &&
 		#git cherry-pick upstream..topic &&
 
-		grep CONFLICT..rename/rename output &&
-
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls
 	)
@@ -456,6 +457,7 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
@@ -522,6 +524,7 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
@@ -624,6 +627,7 @@ test_expect_success 'caching renames only on upstream side, part 1' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
@@ -682,6 +686,7 @@ test_expect_success 'caching renames only on upstream side, part 2' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
-- 
2.43.0.rc1.15.g29556bcc86


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v7 02/14] replay: introduce new builtin
  2023-11-15 14:33           ` [PATCH v7 " Christian Couder
  2023-11-15 14:33             ` [PATCH v7 01/14] t6429: remove switching aspects of fast-rebase Christian Couder
@ 2023-11-15 14:33             ` Christian Couder
  2023-11-15 14:33             ` [PATCH v7 03/14] replay: start using parse_options API Christian Couder
                               ` (13 subsequent siblings)
  15 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-15 14:33 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

For now, this is just a rename from `t/helper/test-fast-rebase.c` into
`builtin/replay.c` with minimal changes to make it build appropriately.

Let's add a stub documentation and a stub test script though.

Subsequent commits will flesh out the capabilities of the new command
and make it a more standard regular builtin.

Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 .gitignore                                    |  1 +
 Documentation/git-replay.txt                  | 39 ++++++++++++
 Makefile                                      |  2 +-
 builtin.h                                     |  1 +
 .../test-fast-rebase.c => builtin/replay.c    | 29 +++------
 command-list.txt                              |  1 +
 git.c                                         |  1 +
 t/helper/test-tool.c                          |  1 -
 t/helper/test-tool.h                          |  1 -
 t/t3650-replay-basics.sh                      | 60 +++++++++++++++++++
 t/t6429-merge-sequence-rename-caching.sh      | 27 +++------
 11 files changed, 122 insertions(+), 41 deletions(-)
 create mode 100644 Documentation/git-replay.txt
 rename t/helper/test-fast-rebase.c => builtin/replay.c (87%)
 create mode 100755 t/t3650-replay-basics.sh

diff --git a/.gitignore b/.gitignore
index 5e56e471b3..612c0f6a0f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -135,6 +135,7 @@
 /git-remote-ext
 /git-repack
 /git-replace
+/git-replay
 /git-request-pull
 /git-rerere
 /git-reset
diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
new file mode 100644
index 0000000000..0349058b66
--- /dev/null
+++ b/Documentation/git-replay.txt
@@ -0,0 +1,39 @@
+git-replay(1)
+=============
+
+NAME
+----
+git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos too
+
+
+SYNOPSIS
+--------
+[verse]
+'git replay' --onto <newbase> <oldbase> <branch> # EXPERIMENTAL
+
+DESCRIPTION
+-----------
+
+Takes a range of commits, specified by <oldbase> and <branch>, and
+replays them onto a new location (see `--onto` option below).
+
+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
+
+OPTIONS
+-------
+
+--onto <newbase>::
+	Starting point at which to create the new commits.  May be any
+	valid commit, and not just an existing branch name.
+
+EXIT STATUS
+-----------
+
+For a successful, non-conflicted replay, the exit status is 0.  When
+the replay has conflicts, the exit status is 1.  If the replay is not
+able to complete (or start) due to some kind of error, the exit status
+is something other than 0 or 1.
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Makefile b/Makefile
index 03adcb5a48..3834bc1544 100644
--- a/Makefile
+++ b/Makefile
@@ -799,7 +799,6 @@ TEST_BUILTINS_OBJS += test-dump-split-index.o
 TEST_BUILTINS_OBJS += test-dump-untracked-cache.o
 TEST_BUILTINS_OBJS += test-env-helper.o
 TEST_BUILTINS_OBJS += test-example-decorate.o
-TEST_BUILTINS_OBJS += test-fast-rebase.o
 TEST_BUILTINS_OBJS += test-find-pack.o
 TEST_BUILTINS_OBJS += test-fsmonitor-client.o
 TEST_BUILTINS_OBJS += test-genrandom.o
@@ -1290,6 +1289,7 @@ BUILTIN_OBJS += builtin/remote-fd.o
 BUILTIN_OBJS += builtin/remote.o
 BUILTIN_OBJS += builtin/repack.o
 BUILTIN_OBJS += builtin/replace.o
+BUILTIN_OBJS += builtin/replay.o
 BUILTIN_OBJS += builtin/rerere.o
 BUILTIN_OBJS += builtin/reset.o
 BUILTIN_OBJS += builtin/rev-list.o
diff --git a/builtin.h b/builtin.h
index d560baa661..28280636da 100644
--- a/builtin.h
+++ b/builtin.h
@@ -211,6 +211,7 @@ int cmd_remote(int argc, const char **argv, const char *prefix);
 int cmd_remote_ext(int argc, const char **argv, const char *prefix);
 int cmd_remote_fd(int argc, const char **argv, const char *prefix);
 int cmd_repack(int argc, const char **argv, const char *prefix);
+int cmd_replay(int argc, const char **argv, const char *prefix);
 int cmd_rerere(int argc, const char **argv, const char *prefix);
 int cmd_reset(int argc, const char **argv, const char *prefix);
 int cmd_restore(int argc, const char **argv, const char *prefix);
diff --git a/t/helper/test-fast-rebase.c b/builtin/replay.c
similarity index 87%
rename from t/helper/test-fast-rebase.c
rename to builtin/replay.c
index 2bfab66b1b..f2d8444417 100644
--- a/t/helper/test-fast-rebase.c
+++ b/builtin/replay.c
@@ -1,17 +1,11 @@
 /*
- * "git fast-rebase" builtin command
- *
- * FAST: Forking Any Subprocesses (is) Taboo
- *
- * This is meant SOLELY as a demo of what is possible.  sequencer.c and
- * rebase.c should be refactored to use the ideas here, rather than attempting
- * to extend this file to replace those (unless Phillip or Dscho say that
- * refactoring is too hard and we need a clean slate, but I'm guessing that
- * refactoring is the better route).
+ * "git replay" builtin command
  */
 
 #define USE_THE_INDEX_VARIABLE
-#include "test-tool.h"
+#include "git-compat-util.h"
+
+#include "builtin.h"
 #include "cache-tree.h"
 #include "commit.h"
 #include "environment.h"
@@ -27,7 +21,8 @@
 #include "sequencer.h"
 #include "setup.h"
 #include "strvec.h"
-#include "tree.h"
+#include <oidset.h>
+#include <tree.h>
 
 static const char *short_commit_name(struct commit *commit)
 {
@@ -94,7 +89,7 @@ static struct commit *create_commit(struct tree *tree,
 	return (struct commit *)obj;
 }
 
-int cmd__fast_rebase(int argc, const char **argv)
+int cmd_replay(int argc, const char **argv, const char *prefix)
 {
 	struct commit *onto;
 	struct commit *last_commit = NULL, *last_picked_commit = NULL;
@@ -110,14 +105,8 @@ int cmd__fast_rebase(int argc, const char **argv)
 	struct strbuf branch_name = STRBUF_INIT;
 	int ret = 0;
 
-	/*
-	 * test-tool stuff doesn't set up the git directory by default; need to
-	 * do that manually.
-	 */
-	setup_git_directory();
-
 	if (argc == 2 && !strcmp(argv[1], "-h")) {
-		printf("Sorry, I am not a psychiatrist; I can not give you the help you need.  Oh, you meant usage...\n");
+		printf("git replay --onto <newbase> <oldbase> <branch> # EXPERIMENTAL\n");
 		exit(129);
 	}
 
@@ -136,7 +125,7 @@ int cmd__fast_rebase(int argc, const char **argv)
 	if (repo_read_index(the_repository) < 0)
 		BUG("Could not read index");
 
-	repo_init_revisions(the_repository, &revs, NULL);
+	repo_init_revisions(the_repository, &revs, prefix);
 	revs.verbose_header = 1;
 	revs.max_parents = 1;
 	revs.cherry_mark = 1;
diff --git a/command-list.txt b/command-list.txt
index 54b2a50f5f..c4cd0f352b 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -160,6 +160,7 @@ git-reflog                              ancillarymanipulators           complete
 git-remote                              ancillarymanipulators           complete
 git-repack                              ancillarymanipulators           complete
 git-replace                             ancillarymanipulators           complete
+git-replay                              plumbingmanipulators
 git-request-pull                        foreignscminterface             complete
 git-rerere                              ancillaryinterrogators
 git-reset                               mainporcelain           history
diff --git a/git.c b/git.c
index c67e44dd82..7068a184b0 100644
--- a/git.c
+++ b/git.c
@@ -594,6 +594,7 @@ static struct cmd_struct commands[] = {
 	{ "remote-fd", cmd_remote_fd, NO_PARSEOPT },
 	{ "repack", cmd_repack, RUN_SETUP },
 	{ "replace", cmd_replace, RUN_SETUP },
+	{ "replay", cmd_replay, RUN_SETUP },
 	{ "rerere", cmd_rerere, RUN_SETUP },
 	{ "reset", cmd_reset, RUN_SETUP },
 	{ "restore", cmd_restore, RUN_SETUP | NEED_WORK_TREE },
diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
index 876cd2dc31..37ba996539 100644
--- a/t/helper/test-tool.c
+++ b/t/helper/test-tool.c
@@ -30,7 +30,6 @@ static struct test_cmd cmds[] = {
 	{ "dump-untracked-cache", cmd__dump_untracked_cache },
 	{ "env-helper", cmd__env_helper },
 	{ "example-decorate", cmd__example_decorate },
-	{ "fast-rebase", cmd__fast_rebase },
 	{ "find-pack", cmd__find_pack },
 	{ "fsmonitor-client", cmd__fsmonitor_client },
 	{ "genrandom", cmd__genrandom },
diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h
index 70dd4eba11..8a1a7c63da 100644
--- a/t/helper/test-tool.h
+++ b/t/helper/test-tool.h
@@ -24,7 +24,6 @@ int cmd__dump_untracked_cache(int argc, const char **argv);
 int cmd__dump_reftable(int argc, const char **argv);
 int cmd__env_helper(int argc, const char **argv);
 int cmd__example_decorate(int argc, const char **argv);
-int cmd__fast_rebase(int argc, const char **argv);
 int cmd__find_pack(int argc, const char **argv);
 int cmd__fsmonitor_client(int argc, const char **argv);
 int cmd__genrandom(int argc, const char **argv);
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
new file mode 100755
index 0000000000..36c1b5082a
--- /dev/null
+++ b/t/t3650-replay-basics.sh
@@ -0,0 +1,60 @@
+#!/bin/sh
+
+test_description='basic git replay tests'
+
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
+. ./test-lib.sh
+
+GIT_AUTHOR_NAME=author@name
+GIT_AUTHOR_EMAIL=bogus@email@address
+export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL
+
+test_expect_success 'setup' '
+	test_commit A &&
+	test_commit B &&
+
+	git switch -c topic1 &&
+	test_commit C &&
+	git switch -c topic2 &&
+	test_commit D &&
+	test_commit E &&
+	git switch topic1 &&
+	test_commit F &&
+	git switch -c topic3 &&
+	test_commit G &&
+	test_commit H &&
+	git switch -c topic4 main &&
+	test_commit I &&
+	test_commit J &&
+
+	git switch -c next main &&
+	test_commit K &&
+	git merge -m "Merge topic1" topic1 &&
+	git merge -m "Merge topic2" topic2 &&
+	git merge -m "Merge topic3" topic3 &&
+	>evil &&
+	git add evil &&
+	git commit --amend &&
+	git merge -m "Merge topic4" topic4 &&
+
+	git switch main &&
+	test_commit L &&
+	test_commit M &&
+
+	git switch -c conflict B &&
+	test_commit C.conflict C.t conflict
+'
+
+test_expect_success 'using replay to rebase two branches, one on top of other' '
+	git switch main &&
+
+	git replay --onto main topic1 topic2 >result &&
+
+	git log --format=%s $(cut -f 3 -d " " result) >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual
+'
+
+test_done
diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh
index 75d3fd2dba..7670b72008 100755
--- a/t/t6429-merge-sequence-rename-caching.sh
+++ b/t/t6429-merge-sequence-rename-caching.sh
@@ -71,9 +71,8 @@ test_expect_success 'caching renames does not preclude finding new ones' '
 
 		git switch upstream &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream~1..topic
 
 		git ls-files >tracked-files &&
 		test_line_count = 2 tracked-files &&
@@ -141,8 +140,7 @@ test_expect_success 'cherry-pick both a commit and its immediate revert' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
-		#git cherry-pick upstream~1..topic &&
+		git replay --onto HEAD upstream~1 topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 1 calls
@@ -200,9 +198,8 @@ test_expect_success 'rename same file identically, then reintroduce it' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream~1..topic &&
 
 		git ls-files >tracked &&
 		test_line_count = 2 tracked &&
@@ -278,9 +275,8 @@ test_expect_success 'rename same file identically, then add file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream~1..topic &&
 
 		git ls-files >tracked &&
 		test_line_count = 4 tracked &&
@@ -356,8 +352,7 @@ test_expect_success 'cached dir rename does not prevent noticing later conflict'
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test_must_fail test-tool fast-rebase --onto HEAD upstream~1 topic >output &&
-		#git cherry-pick upstream..topic &&
+		test_must_fail git replay --onto HEAD upstream~1 topic >output &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls
@@ -456,9 +451,8 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls &&
@@ -523,9 +517,8 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 3 calls &&
@@ -626,9 +619,8 @@ test_expect_success 'caching renames only on upstream side, part 1' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 1 calls &&
@@ -685,9 +677,8 @@ test_expect_success 'caching renames only on upstream side, part 2' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls &&
-- 
2.43.0.rc1.15.g29556bcc86


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v7 03/14] replay: start using parse_options API
  2023-11-15 14:33           ` [PATCH v7 " Christian Couder
  2023-11-15 14:33             ` [PATCH v7 01/14] t6429: remove switching aspects of fast-rebase Christian Couder
  2023-11-15 14:33             ` [PATCH v7 02/14] replay: introduce new builtin Christian Couder
@ 2023-11-15 14:33             ` Christian Couder
  2023-11-15 14:33             ` [PATCH v7 04/14] replay: die() instead of failing assert() Christian Couder
                               ` (12 subsequent siblings)
  15 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-15 14:33 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

Instead of manually parsing arguments, let's start using the parse_options
API. This way this new builtin will look more standard, and in some
upcoming commits will more easily be able to handle more command line
options.

Note that we plan to later use standard revision ranges instead of
hardcoded "<oldbase> <branch>" arguments. When we will use standard
revision ranges, it will be easier to check if there are no spurious
arguments if we keep ARGV[0], so let's call parse_options() with
PARSE_OPT_KEEP_ARGV0 even if we don't need ARGV[0] right now to avoid
some useless code churn.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 45 ++++++++++++++++++++++++++++++++-------------
 1 file changed, 32 insertions(+), 13 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index f2d8444417..afabb844d3 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -15,7 +15,7 @@
 #include "lockfile.h"
 #include "merge-ort.h"
 #include "object-name.h"
-#include "read-cache-ll.h"
+#include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
 #include "sequencer.h"
@@ -92,6 +92,7 @@ static struct commit *create_commit(struct tree *tree,
 int cmd_replay(int argc, const char **argv, const char *prefix)
 {
 	struct commit *onto;
+	const char *onto_name = NULL;
 	struct commit *last_commit = NULL, *last_picked_commit = NULL;
 	struct object_id head;
 	struct lock_file lock = LOCK_INIT;
@@ -105,16 +106,32 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct strbuf branch_name = STRBUF_INIT;
 	int ret = 0;
 
-	if (argc == 2 && !strcmp(argv[1], "-h")) {
-		printf("git replay --onto <newbase> <oldbase> <branch> # EXPERIMENTAL\n");
-		exit(129);
+	const char * const replay_usage[] = {
+		N_("git replay --onto <newbase> <oldbase> <branch> # EXPERIMENTAL"),
+		NULL
+	};
+	struct option replay_options[] = {
+		OPT_STRING(0, "onto", &onto_name,
+			   N_("revision"),
+			   N_("replay onto given commit")),
+		OPT_END()
+	};
+
+	argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
+			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
+
+	if (!onto_name) {
+		error(_("option --onto is mandatory"));
+		usage_with_options(replay_usage, replay_options);
 	}
 
-	if (argc != 5 || strcmp(argv[1], "--onto"))
-		die("usage: read the code, figure out how to use it, then do so");
+	if (argc != 3) {
+		error(_("bad number of arguments"));
+		usage_with_options(replay_usage, replay_options);
+	}
 
-	onto = peel_committish(argv[2]);
-	strbuf_addf(&branch_name, "refs/heads/%s", argv[4]);
+	onto = peel_committish(onto_name);
+	strbuf_addf(&branch_name, "refs/heads/%s", argv[2]);
 
 	/* Sanity check */
 	if (repo_get_oid(the_repository, "HEAD", &head))
@@ -126,6 +143,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		BUG("Could not read index");
 
 	repo_init_revisions(the_repository, &revs, prefix);
+
 	revs.verbose_header = 1;
 	revs.max_parents = 1;
 	revs.cherry_mark = 1;
@@ -134,7 +152,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	revs.right_only = 1;
 	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
 	revs.topo_order = 1;
-	strvec_pushl(&rev_walk_args, "", argv[4], "--not", argv[3], NULL);
+
+	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
 
 	if (setup_revisions(rev_walk_args.nr, rev_walk_args.v, &revs, NULL) > 1) {
 		ret = error(_("unhandled options"));
@@ -197,8 +216,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			       &last_commit->object.oid,
 			       &last_picked_commit->object.oid,
 			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
-			error(_("could not update %s"), argv[4]);
-			die("Failed to update %s", argv[4]);
+			error(_("could not update %s"), argv[2]);
+			die("Failed to update %s", argv[2]);
 		}
 		if (create_symref("HEAD", branch_name.buf, reflog_msg.buf) < 0)
 			die(_("unable to update HEAD"));
@@ -210,8 +229,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			       &last_commit->object.oid,
 			       &head,
 			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
-			error(_("could not update %s"), argv[4]);
-			die("Failed to update %s", argv[4]);
+			error(_("could not update %s"), argv[2]);
+			die("Failed to update %s", argv[2]);
 		}
 	}
 	ret = (result.clean == 0);
-- 
2.43.0.rc1.15.g29556bcc86


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v7 04/14] replay: die() instead of failing assert()
  2023-11-15 14:33           ` [PATCH v7 " Christian Couder
                               ` (2 preceding siblings ...)
  2023-11-15 14:33             ` [PATCH v7 03/14] replay: start using parse_options API Christian Couder
@ 2023-11-15 14:33             ` Christian Couder
  2023-11-15 14:33             ` [PATCH v7 05/14] replay: introduce pick_regular_commit() Christian Couder
                               ` (11 subsequent siblings)
  15 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-15 14:33 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

It's not a good idea for regular Git commands to use an assert() to
check for things that could happen but are not supported.

Let's die() with an explanation of the issue instead.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index afabb844d3..32dbaaf028 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -179,7 +179,12 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 
 		fprintf(stderr, "Rebasing %s...\r",
 			oid_to_hex(&commit->object.oid));
-		assert(commit->parents && !commit->parents->next);
+
+		if (!commit->parents)
+			die(_("replaying down to root commit is not supported yet!"));
+		if (commit->parents->next)
+			die(_("replaying merge commits is not supported yet!"));
+
 		base = commit->parents->item;
 
 		next_tree = repo_get_commit_tree(the_repository, commit);
-- 
2.43.0.rc1.15.g29556bcc86


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v7 05/14] replay: introduce pick_regular_commit()
  2023-11-15 14:33           ` [PATCH v7 " Christian Couder
                               ` (3 preceding siblings ...)
  2023-11-15 14:33             ` [PATCH v7 04/14] replay: die() instead of failing assert() Christian Couder
@ 2023-11-15 14:33             ` Christian Couder
  2023-11-15 14:33             ` [PATCH v7 06/14] replay: change rev walking options Christian Couder
                               ` (10 subsequent siblings)
  15 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-15 14:33 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

Let's refactor the code to handle a regular commit (a commit that is
neither a root commit nor a merge commit) into a single function instead
of keeping it inside cmd_replay().

This is good for separation of concerns, and this will help further work
in the future to replay merge commits.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 54 ++++++++++++++++++++++++++++++------------------
 1 file changed, 34 insertions(+), 20 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 32dbaaf028..5c4cbd11db 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -89,6 +89,35 @@ static struct commit *create_commit(struct tree *tree,
 	return (struct commit *)obj;
 }
 
+static struct commit *pick_regular_commit(struct commit *pickme,
+					  struct commit *last_commit,
+					  struct merge_options *merge_opt,
+					  struct merge_result *result)
+{
+	struct commit *base;
+	struct tree *pickme_tree, *base_tree;
+
+	base = pickme->parents->item;
+
+	pickme_tree = repo_get_commit_tree(the_repository, pickme);
+	base_tree = repo_get_commit_tree(the_repository, base);
+
+	merge_opt->branch2 = short_commit_name(pickme);
+	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
+
+	merge_incore_nonrecursive(merge_opt,
+				  base_tree,
+				  result->tree,
+				  pickme_tree,
+				  result);
+
+	free((char*)merge_opt->ancestor);
+	merge_opt->ancestor = NULL;
+	if (!result->clean)
+		return NULL;
+	return create_commit(result->tree, pickme, last_commit);
+}
+
 int cmd_replay(int argc, const char **argv, const char *prefix)
 {
 	struct commit *onto;
@@ -100,7 +129,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct rev_info revs;
 	struct commit *commit;
 	struct merge_options merge_opt;
-	struct tree *next_tree, *base_tree, *head_tree;
+	struct tree *head_tree;
 	struct merge_result result;
 	struct strbuf reflog_msg = STRBUF_INIT;
 	struct strbuf branch_name = STRBUF_INIT;
@@ -175,7 +204,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	result.tree = head_tree;
 	last_commit = onto;
 	while ((commit = get_revision(&revs))) {
-		struct commit *base;
+		struct commit *pick;
 
 		fprintf(stderr, "Rebasing %s...\r",
 			oid_to_hex(&commit->object.oid));
@@ -185,26 +214,11 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		if (commit->parents->next)
 			die(_("replaying merge commits is not supported yet!"));
 
-		base = commit->parents->item;
-
-		next_tree = repo_get_commit_tree(the_repository, commit);
-		base_tree = repo_get_commit_tree(the_repository, base);
-
-		merge_opt.branch2 = short_commit_name(commit);
-		merge_opt.ancestor = xstrfmt("parent of %s", merge_opt.branch2);
-
-		merge_incore_nonrecursive(&merge_opt,
-					  base_tree,
-					  result.tree,
-					  next_tree,
-					  &result);
-
-		free((char*)merge_opt.ancestor);
-		merge_opt.ancestor = NULL;
-		if (!result.clean)
+		pick = pick_regular_commit(commit, last_commit, &merge_opt, &result);
+		if (!pick)
 			break;
+		last_commit = pick;
 		last_picked_commit = commit;
-		last_commit = create_commit(result.tree, commit, last_commit);
 	}
 
 	merge_finalize(&merge_opt, &result);
-- 
2.43.0.rc1.15.g29556bcc86


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v7 06/14] replay: change rev walking options
  2023-11-15 14:33           ` [PATCH v7 " Christian Couder
                               ` (4 preceding siblings ...)
  2023-11-15 14:33             ` [PATCH v7 05/14] replay: introduce pick_regular_commit() Christian Couder
@ 2023-11-15 14:33             ` Christian Couder
  2023-11-15 14:33             ` [PATCH v7 07/14] replay: add an important FIXME comment about gpg signing Christian Couder
                               ` (9 subsequent siblings)
  15 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-15 14:33 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

Let's force the rev walking options we need after calling
setup_revisions() instead of before.

This might override some user supplied rev walking command line options
though. So let's detect that and warn users by:

  a) setting the desired values, before setup_revisions(),
  b) checking after setup_revisions() whether these values differ from
     the desired values,
  c) if so throwing a warning and setting the desired values again.

We want the command to work from older commits to newer ones by default.
Also we don't want history simplification, as we want to deal with all
the commits in the affected range.

Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 48 +++++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 41 insertions(+), 7 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 5c4cbd11db..8302d35eca 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -173,22 +173,56 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 
 	repo_init_revisions(the_repository, &revs, prefix);
 
-	revs.verbose_header = 1;
-	revs.max_parents = 1;
-	revs.cherry_mark = 1;
-	revs.limited = 1;
+	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
+
+	/*
+	 * Set desired values for rev walking options here. If they
+	 * are changed by some user specified option in setup_revisions()
+	 * below, we will detect that below and then warn.
+	 *
+	 * TODO: In the future we might want to either die(), or allow
+	 * some options changing these values if we think they could
+	 * be useful.
+	 */
 	revs.reverse = 1;
-	revs.right_only = 1;
 	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
 	revs.topo_order = 1;
-
-	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
+	revs.simplify_history = 0;
 
 	if (setup_revisions(rev_walk_args.nr, rev_walk_args.v, &revs, NULL) > 1) {
 		ret = error(_("unhandled options"));
 		goto cleanup;
 	}
 
+	/*
+	 * Detect and warn if we override some user specified rev
+	 * walking options.
+	 */
+	if (revs.reverse != 1) {
+		warning(_("some rev walking options will be overridden as "
+			  "'%s' bit in 'struct rev_info' will be forced"),
+			"reverse");
+		revs.reverse = 1;
+	}
+	if (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) {
+		warning(_("some rev walking options will be overridden as "
+			  "'%s' bit in 'struct rev_info' will be forced"),
+			"sort_order");
+		revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
+	}
+	if (revs.topo_order != 1) {
+		warning(_("some rev walking options will be overridden as "
+			  "'%s' bit in 'struct rev_info' will be forced"),
+			"topo_order");
+		revs.topo_order = 1;
+	}
+	if (revs.simplify_history != 0) {
+		warning(_("some rev walking options will be overridden as "
+			  "'%s' bit in 'struct rev_info' will be forced"),
+			"simplify_history");
+		revs.simplify_history = 0;
+	}
+
 	strvec_clear(&rev_walk_args);
 
 	if (prepare_revision_walk(&revs) < 0) {
-- 
2.43.0.rc1.15.g29556bcc86


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v7 07/14] replay: add an important FIXME comment about gpg signing
  2023-11-15 14:33           ` [PATCH v7 " Christian Couder
                               ` (5 preceding siblings ...)
  2023-11-15 14:33             ` [PATCH v7 06/14] replay: change rev walking options Christian Couder
@ 2023-11-15 14:33             ` Christian Couder
  2023-11-15 14:33             ` [PATCH v7 08/14] replay: remove progress and info output Christian Couder
                               ` (8 subsequent siblings)
  15 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-15 14:33 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

We want to be able to handle signed commits in some way in the future,
but we are not ready to do it now. So for the time being let's just add
a FIXME comment to remind us about it.

These are different ways we could handle them:

  - in case of a cli user and if there was an interactive mode, we could
    perhaps ask if the user wants to sign again
  - we could add an option to just fail if there are signed commits

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 8302d35eca..2e1df83027 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -62,7 +62,7 @@ static struct commit *create_commit(struct tree *tree,
 	struct object *obj;
 	struct commit_list *parents = NULL;
 	char *author;
-	char *sign_commit = NULL;
+	char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
 	struct commit_extra_header *extra;
 	struct strbuf msg = STRBUF_INIT;
 	const char *out_enc = get_commit_output_encoding();
-- 
2.43.0.rc1.15.g29556bcc86


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v7 08/14] replay: remove progress and info output
  2023-11-15 14:33           ` [PATCH v7 " Christian Couder
                               ` (6 preceding siblings ...)
  2023-11-15 14:33             ` [PATCH v7 07/14] replay: add an important FIXME comment about gpg signing Christian Couder
@ 2023-11-15 14:33             ` Christian Couder
  2023-11-15 14:33             ` [PATCH v7 09/14] replay: remove HEAD related sanity check Christian Couder
                               ` (7 subsequent siblings)
  15 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-15 14:33 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

The replay command will be changed in a follow up commit, so that it
will not update refs directly, but instead it will print on stdout a
list of commands that can be consumed by `git update-ref --stdin`.

We don't want this output to be polluted by its current low value
output, so let's just remove the latter.

In the future, when the command gets an option to update refs by
itself, it will make a lot of sense to display a progress meter, but
we are not there yet.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 7 +------
 1 file changed, 1 insertion(+), 6 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 2e1df83027..1035435705 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -232,7 +232,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
-	merge_opt.show_rename_progress = 1;
+	merge_opt.show_rename_progress = 0;
 	merge_opt.branch1 = "HEAD";
 	head_tree = repo_get_commit_tree(the_repository, onto);
 	result.tree = head_tree;
@@ -240,9 +240,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	while ((commit = get_revision(&revs))) {
 		struct commit *pick;
 
-		fprintf(stderr, "Rebasing %s...\r",
-			oid_to_hex(&commit->object.oid));
-
 		if (!commit->parents)
 			die(_("replaying down to root commit is not supported yet!"));
 		if (commit->parents->next)
@@ -261,7 +258,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		exit(128);
 
 	if (result.clean) {
-		fprintf(stderr, "\nDone.\n");
 		strbuf_addf(&reflog_msg, "finish rebase %s onto %s",
 			    oid_to_hex(&last_picked_commit->object.oid),
 			    oid_to_hex(&last_commit->object.oid));
@@ -275,7 +271,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		if (create_symref("HEAD", branch_name.buf, reflog_msg.buf) < 0)
 			die(_("unable to update HEAD"));
 	} else {
-		fprintf(stderr, "\nAborting: Hit a conflict.\n");
 		strbuf_addf(&reflog_msg, "rebase progress up to %s",
 			    oid_to_hex(&last_picked_commit->object.oid));
 		if (update_ref(reflog_msg.buf, "HEAD",
-- 
2.43.0.rc1.15.g29556bcc86


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v7 09/14] replay: remove HEAD related sanity check
  2023-11-15 14:33           ` [PATCH v7 " Christian Couder
                               ` (7 preceding siblings ...)
  2023-11-15 14:33             ` [PATCH v7 08/14] replay: remove progress and info output Christian Couder
@ 2023-11-15 14:33             ` Christian Couder
  2023-11-15 14:33             ` [PATCH v7 10/14] replay: make it a minimal server side command Christian Couder
                               ` (6 subsequent siblings)
  15 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-15 14:33 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

We want replay to be a command that can be used on the server side on
any branch, not just the current one, so we are going to stop updating
HEAD in a future commit.

A "sanity check" that makes sure we are replaying the current branch
doesn't make sense anymore. Let's remove it.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c         | 8 +-------
 t/t3650-replay-basics.sh | 2 --
 2 files changed, 1 insertion(+), 9 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 1035435705..30292d219d 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -123,7 +123,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct commit *onto;
 	const char *onto_name = NULL;
 	struct commit *last_commit = NULL, *last_picked_commit = NULL;
-	struct object_id head;
 	struct lock_file lock = LOCK_INIT;
 	struct strvec rev_walk_args = STRVEC_INIT;
 	struct rev_info revs;
@@ -162,11 +161,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	onto = peel_committish(onto_name);
 	strbuf_addf(&branch_name, "refs/heads/%s", argv[2]);
 
-	/* Sanity check */
-	if (repo_get_oid(the_repository, "HEAD", &head))
-		die(_("Cannot read HEAD"));
-	assert(oideq(&onto->object.oid, &head));
-
 	repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
 	if (repo_read_index(the_repository) < 0)
 		BUG("Could not read index");
@@ -275,7 +269,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			    oid_to_hex(&last_picked_commit->object.oid));
 		if (update_ref(reflog_msg.buf, "HEAD",
 			       &last_commit->object.oid,
-			       &head,
+			       &onto->object.oid,
 			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
 			error(_("could not update %s"), argv[2]);
 			die("Failed to update %s", argv[2]);
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 36c1b5082a..b5b9f9ade2 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -48,8 +48,6 @@ test_expect_success 'setup' '
 '
 
 test_expect_success 'using replay to rebase two branches, one on top of other' '
-	git switch main &&
-
 	git replay --onto main topic1 topic2 >result &&
 
 	git log --format=%s $(cut -f 3 -d " " result) >actual &&
-- 
2.43.0.rc1.15.g29556bcc86


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v7 10/14] replay: make it a minimal server side command
  2023-11-15 14:33           ` [PATCH v7 " Christian Couder
                               ` (8 preceding siblings ...)
  2023-11-15 14:33             ` [PATCH v7 09/14] replay: remove HEAD related sanity check Christian Couder
@ 2023-11-15 14:33             ` Christian Couder
  2023-11-15 14:33             ` [PATCH v7 11/14] replay: use standard revision ranges Christian Couder
                               ` (5 subsequent siblings)
  15 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-15 14:33 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

We want this command to be a minimal command that just does server side
picking of commits, displaying the results on stdout for higher level
scripts to consume.

So let's simplify it:
  * remove the worktree and index reading/writing,
  * remove the ref (and reflog) updating,
  * remove the assumptions tying us to HEAD, since (a) this is not a
    rebase and (b) we want to be able to pick commits in a bare repo,
    i.e. to/from branches that are not checked out and not the main
    branch,
  * remove unneeded includes,
  * handle rebasing multiple branches by printing on stdout the update
    ref commands that should be performed.

The output can be piped into `git update-ref --stdin` for the ref
updates to happen.

In the future to make it easier for users to use this command
directly maybe an option can be added to automatically pipe its output
into `git update-ref`.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 Documentation/git-replay.txt             |  5 +-
 builtin/replay.c                         | 78 ++++++++----------------
 t/t3650-replay-basics.sh                 | 19 +++++-
 t/t6429-merge-sequence-rename-caching.sh | 39 +++++++-----
 4 files changed, 72 insertions(+), 69 deletions(-)

diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
index 0349058b66..87a85a7f57 100644
--- a/Documentation/git-replay.txt
+++ b/Documentation/git-replay.txt
@@ -15,7 +15,10 @@ DESCRIPTION
 -----------
 
 Takes a range of commits, specified by <oldbase> and <branch>, and
-replays them onto a new location (see `--onto` option below).
+replays them onto a new location (see `--onto` option below). Leaves
+the working tree and the index untouched, and updates no references.
+The output of this command is meant to be used as input to
+`git update-ref --stdin`, which would update the relevant branches.
 
 THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
 
diff --git a/builtin/replay.c b/builtin/replay.c
index 30292d219d..73a25e9e85 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -6,11 +6,7 @@
 #include "git-compat-util.h"
 
 #include "builtin.h"
-#include "cache-tree.h"
-#include "commit.h"
 #include "environment.h"
-#include "gettext.h"
-#include "hash.h"
 #include "hex.h"
 #include "lockfile.h"
 #include "merge-ort.h"
@@ -18,8 +14,6 @@
 #include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
-#include "sequencer.h"
-#include "setup.h"
 #include "strvec.h"
 #include <oidset.h>
 #include <tree.h>
@@ -102,6 +96,7 @@ static struct commit *pick_regular_commit(struct commit *pickme,
 	pickme_tree = repo_get_commit_tree(the_repository, pickme);
 	base_tree = repo_get_commit_tree(the_repository, base);
 
+	merge_opt->branch1 = short_commit_name(last_commit);
 	merge_opt->branch2 = short_commit_name(pickme);
 	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
 
@@ -122,15 +117,12 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 {
 	struct commit *onto;
 	const char *onto_name = NULL;
-	struct commit *last_commit = NULL, *last_picked_commit = NULL;
-	struct lock_file lock = LOCK_INIT;
+	struct commit *last_commit = NULL;
 	struct strvec rev_walk_args = STRVEC_INIT;
 	struct rev_info revs;
 	struct commit *commit;
 	struct merge_options merge_opt;
-	struct tree *head_tree;
 	struct merge_result result;
-	struct strbuf reflog_msg = STRBUF_INIT;
 	struct strbuf branch_name = STRBUF_INIT;
 	int ret = 0;
 
@@ -161,10 +153,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	onto = peel_committish(onto_name);
 	strbuf_addf(&branch_name, "refs/heads/%s", argv[2]);
 
-	repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
-	if (repo_read_index(the_repository) < 0)
-		BUG("Could not read index");
-
 	repo_init_revisions(the_repository, &revs, prefix);
 
 	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
@@ -227,58 +215,44 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
 	merge_opt.show_rename_progress = 0;
-	merge_opt.branch1 = "HEAD";
-	head_tree = repo_get_commit_tree(the_repository, onto);
-	result.tree = head_tree;
+	result.tree = repo_get_commit_tree(the_repository, onto);
 	last_commit = onto;
 	while ((commit = get_revision(&revs))) {
-		struct commit *pick;
+		const struct name_decoration *decoration;
 
 		if (!commit->parents)
 			die(_("replaying down to root commit is not supported yet!"));
 		if (commit->parents->next)
 			die(_("replaying merge commits is not supported yet!"));
 
-		pick = pick_regular_commit(commit, last_commit, &merge_opt, &result);
-		if (!pick)
+		last_commit = pick_regular_commit(commit, last_commit, &merge_opt, &result);
+		if (!last_commit)
 			break;
-		last_commit = pick;
-		last_picked_commit = commit;
+
+		decoration = get_name_decoration(&commit->object);
+		if (!decoration)
+			continue;
+
+		while (decoration) {
+			if (decoration->type == DECORATION_REF_LOCAL) {
+				printf("update %s %s %s\n",
+				       decoration->name,
+				       oid_to_hex(&last_commit->object.oid),
+				       oid_to_hex(&commit->object.oid));
+			}
+			decoration = decoration->next;
+		}
 	}
 
 	merge_finalize(&merge_opt, &result);
+	ret = result.clean;
 
-	if (result.clean < 0)
-		exit(128);
-
-	if (result.clean) {
-		strbuf_addf(&reflog_msg, "finish rebase %s onto %s",
-			    oid_to_hex(&last_picked_commit->object.oid),
-			    oid_to_hex(&last_commit->object.oid));
-		if (update_ref(reflog_msg.buf, branch_name.buf,
-			       &last_commit->object.oid,
-			       &last_picked_commit->object.oid,
-			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
-			error(_("could not update %s"), argv[2]);
-			die("Failed to update %s", argv[2]);
-		}
-		if (create_symref("HEAD", branch_name.buf, reflog_msg.buf) < 0)
-			die(_("unable to update HEAD"));
-	} else {
-		strbuf_addf(&reflog_msg, "rebase progress up to %s",
-			    oid_to_hex(&last_picked_commit->object.oid));
-		if (update_ref(reflog_msg.buf, "HEAD",
-			       &last_commit->object.oid,
-			       &onto->object.oid,
-			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
-			error(_("could not update %s"), argv[2]);
-			die("Failed to update %s", argv[2]);
-		}
-	}
-	ret = (result.clean == 0);
 cleanup:
-	strbuf_release(&reflog_msg);
 	strbuf_release(&branch_name);
 	release_revisions(&revs);
-	return ret;
+
+	/* Return */
+	if (ret < 0)
+		exit(128);
+	return ret ? 0 : 1;
 }
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index b5b9f9ade2..3567c98362 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -47,12 +47,29 @@ test_expect_success 'setup' '
 	test_commit C.conflict C.t conflict
 '
 
+test_expect_success 'setup bare' '
+	git clone --bare . bare
+'
+
 test_expect_success 'using replay to rebase two branches, one on top of other' '
 	git replay --onto main topic1 topic2 >result &&
 
+	test_line_count = 1 result &&
+
 	git log --format=%s $(cut -f 3 -d " " result) >actual &&
 	test_write_lines E D M L B A >expect &&
-	test_cmp expect actual
+	test_cmp expect actual &&
+
+	printf "update refs/heads/topic2 " >expect &&
+	printf "%s " $(cut -f 3 -d " " result) >>expect &&
+	git rev-parse topic2 >>expect &&
+
+	test_cmp expect result
+'
+
+test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '
+	git -C bare replay --onto main topic1 topic2 >result-bare &&
+	test_cmp expect result-bare
 '
 
 test_done
diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh
index 7670b72008..099aefeffc 100755
--- a/t/t6429-merge-sequence-rename-caching.sh
+++ b/t/t6429-merge-sequence-rename-caching.sh
@@ -71,8 +71,9 @@ test_expect_success 'caching renames does not preclude finding new ones' '
 
 		git switch upstream &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		git ls-files >tracked-files &&
 		test_line_count = 2 tracked-files &&
@@ -140,7 +141,9 @@ test_expect_success 'cherry-pick both a commit and its immediate revert' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 1 calls
@@ -198,8 +201,9 @@ test_expect_success 'rename same file identically, then reintroduce it' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		git ls-files >tracked &&
 		test_line_count = 2 tracked &&
@@ -275,8 +279,9 @@ test_expect_success 'rename same file identically, then add file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		git ls-files >tracked &&
 		test_line_count = 4 tracked &&
@@ -451,8 +456,9 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls &&
@@ -517,8 +523,9 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 3 calls &&
@@ -619,8 +626,9 @@ test_expect_success 'caching renames only on upstream side, part 1' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 1 calls &&
@@ -677,8 +685,9 @@ test_expect_success 'caching renames only on upstream side, part 2' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls &&
-- 
2.43.0.rc1.15.g29556bcc86


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v7 11/14] replay: use standard revision ranges
  2023-11-15 14:33           ` [PATCH v7 " Christian Couder
                               ` (9 preceding siblings ...)
  2023-11-15 14:33             ` [PATCH v7 10/14] replay: make it a minimal server side command Christian Couder
@ 2023-11-15 14:33             ` Christian Couder
  2023-11-15 14:33             ` [PATCH v7 12/14] replay: add --advance or 'cherry-pick' mode Christian Couder
                               ` (4 subsequent siblings)
  15 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-15 14:33 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

Instead of the fixed "<oldbase> <branch>" arguments, the replay
command now accepts "<revision-range>..." arguments in a similar
way as many other Git commands. This makes its interface more
standard and more flexible.

This also enables many revision related options accepted and
eaten by setup_revisions(). If the replay command was a high level
one or had a high level mode, it would make sense to restrict some
of the possible options, like those generating non-contiguous
history, as they could be confusing for most users.

Also as the interface of the command is now mostly finalized,
we can add more documentation and more testcases to make sure
the command will continue to work as designed in the future.

We only document the rev-list related options among all the
revision related options that are now accepted, as the rev-list
related ones are probably the most useful for now.

Helped-by: Dragan Simic <dsimic@manjaro.org>
Helped-by: Linus Arver <linusa@google.com>
Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 Documentation/git-replay.txt             | 58 ++++++++++++++++++++++--
 builtin/replay.c                         | 21 ++-------
 t/t3650-replay-basics.sh                 | 12 ++++-
 t/t6429-merge-sequence-rename-caching.sh | 18 ++++----
 4 files changed, 77 insertions(+), 32 deletions(-)

diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
index 87a85a7f57..fab4ea0178 100644
--- a/Documentation/git-replay.txt
+++ b/Documentation/git-replay.txt
@@ -9,16 +9,16 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
 SYNOPSIS
 --------
 [verse]
-'git replay' --onto <newbase> <oldbase> <branch> # EXPERIMENTAL
+'git replay' --onto <newbase> <revision-range>... # EXPERIMENTAL
 
 DESCRIPTION
 -----------
 
-Takes a range of commits, specified by <oldbase> and <branch>, and
-replays them onto a new location (see `--onto` option below). Leaves
+Takes ranges of commits and replays them onto a new location. Leaves
 the working tree and the index untouched, and updates no references.
 The output of this command is meant to be used as input to
-`git update-ref --stdin`, which would update the relevant branches.
+`git update-ref --stdin`, which would update the relevant branches
+(see the OUTPUT section below).
 
 THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
 
@@ -28,6 +28,30 @@ OPTIONS
 --onto <newbase>::
 	Starting point at which to create the new commits.  May be any
 	valid commit, and not just an existing branch name.
++
+The update-ref command(s) in the output will update the branch(es) in
+the revision range to point at the new commits, similar to the way how
+`git rebase --update-refs` updates multiple branches in the affected
+range.
+
+<revision-range>::
+	Range of commits to replay; see "Specifying Ranges" in
+	linkgit:git-rev-parse and the "Commit Limiting" options below.
+
+include::rev-list-options.txt[]
+
+OUTPUT
+------
+
+When there are no conflicts, the output of this command is usable as
+input to `git update-ref --stdin`.  It is of the form:
+
+	update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
+	update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
+	update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
+
+where the number of refs updated depends on the arguments passed and
+the shape of the history being replayed.
 
 EXIT STATUS
 -----------
@@ -37,6 +61,32 @@ the replay has conflicts, the exit status is 1.  If the replay is not
 able to complete (or start) due to some kind of error, the exit status
 is something other than 0 or 1.
 
+EXAMPLES
+--------
+
+To simply rebase `mybranch` onto `target`:
+
+------------
+$ git replay --onto target origin/main..mybranch
+update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
+------------
+
+When calling `git replay`, one does not need to specify a range of
+commits to replay using the syntax `A..B`; any range expression will
+do:
+
+------------
+$ git replay --onto origin/main ^base branch1 branch2 branch3
+update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
+update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
+update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
+------------
+
+This will simultaneously rebase `branch1`, `branch2`, and `branch3`,
+all commits they have since `base`, playing them on top of
+`origin/main`. These three branches may have commits on top of `base`
+that they have in common, but that does not need to be the case.
+
 GIT
 ---
 Part of the linkgit:git[1] suite
diff --git a/builtin/replay.c b/builtin/replay.c
index 73a25e9e85..7a660020d1 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -14,7 +14,6 @@
 #include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
-#include "strvec.h"
 #include <oidset.h>
 #include <tree.h>
 
@@ -118,16 +117,14 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct commit *onto;
 	const char *onto_name = NULL;
 	struct commit *last_commit = NULL;
-	struct strvec rev_walk_args = STRVEC_INIT;
 	struct rev_info revs;
 	struct commit *commit;
 	struct merge_options merge_opt;
 	struct merge_result result;
-	struct strbuf branch_name = STRBUF_INIT;
 	int ret = 0;
 
 	const char * const replay_usage[] = {
-		N_("git replay --onto <newbase> <oldbase> <branch> # EXPERIMENTAL"),
+		N_("git replay --onto <newbase> <revision-range>... # EXPERIMENTAL"),
 		NULL
 	};
 	struct option replay_options[] = {
@@ -145,18 +142,10 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		usage_with_options(replay_usage, replay_options);
 	}
 
-	if (argc != 3) {
-		error(_("bad number of arguments"));
-		usage_with_options(replay_usage, replay_options);
-	}
-
 	onto = peel_committish(onto_name);
-	strbuf_addf(&branch_name, "refs/heads/%s", argv[2]);
 
 	repo_init_revisions(the_repository, &revs, prefix);
 
-	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
-
 	/*
 	 * Set desired values for rev walking options here. If they
 	 * are changed by some user specified option in setup_revisions()
@@ -171,8 +160,9 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	revs.topo_order = 1;
 	revs.simplify_history = 0;
 
-	if (setup_revisions(rev_walk_args.nr, rev_walk_args.v, &revs, NULL) > 1) {
-		ret = error(_("unhandled options"));
+	argc = setup_revisions(argc, argv, &revs, NULL);
+	if (argc > 1) {
+		ret = error(_("unrecognized argument: %s"), argv[1]);
 		goto cleanup;
 	}
 
@@ -205,8 +195,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		revs.simplify_history = 0;
 	}
 
-	strvec_clear(&rev_walk_args);
-
 	if (prepare_revision_walk(&revs) < 0) {
 		ret = error(_("error preparing revisions"));
 		goto cleanup;
@@ -248,7 +236,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	ret = result.clean;
 
 cleanup:
-	strbuf_release(&branch_name);
 	release_revisions(&revs);
 
 	/* Return */
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 3567c98362..a1da4f9ef9 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -52,7 +52,7 @@ test_expect_success 'setup bare' '
 '
 
 test_expect_success 'using replay to rebase two branches, one on top of other' '
-	git replay --onto main topic1 topic2 >result &&
+	git replay --onto main topic1..topic2 >result &&
 
 	test_line_count = 1 result &&
 
@@ -68,8 +68,16 @@ test_expect_success 'using replay to rebase two branches, one on top of other' '
 '
 
 test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '
-	git -C bare replay --onto main topic1 topic2 >result-bare &&
+	git -C bare replay --onto main topic1..topic2 >result-bare &&
 	test_cmp expect result-bare
 '
 
+test_expect_success 'using replay to rebase with a conflict' '
+	test_expect_code 1 git replay --onto topic1 B..conflict
+'
+
+test_expect_success 'using replay on bare repo to rebase with a conflict' '
+	test_expect_code 1 git -C bare replay --onto topic1 B..conflict
+'
+
 test_done
diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh
index 099aefeffc..0f39ed0d08 100755
--- a/t/t6429-merge-sequence-rename-caching.sh
+++ b/t/t6429-merge-sequence-rename-caching.sh
@@ -71,7 +71,7 @@ test_expect_success 'caching renames does not preclude finding new ones' '
 
 		git switch upstream &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -141,7 +141,7 @@ test_expect_success 'cherry-pick both a commit and its immediate revert' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -201,7 +201,7 @@ test_expect_success 'rename same file identically, then reintroduce it' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -279,7 +279,7 @@ test_expect_success 'rename same file identically, then add file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -357,7 +357,7 @@ test_expect_success 'cached dir rename does not prevent noticing later conflict'
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test_must_fail git replay --onto HEAD upstream~1 topic >output &&
+		test_must_fail git replay --onto HEAD upstream~1..topic >output &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls
@@ -456,7 +456,7 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -523,7 +523,7 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -626,7 +626,7 @@ test_expect_success 'caching renames only on upstream side, part 1' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -685,7 +685,7 @@ test_expect_success 'caching renames only on upstream side, part 2' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
-- 
2.43.0.rc1.15.g29556bcc86


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v7 12/14] replay: add --advance or 'cherry-pick' mode
  2023-11-15 14:33           ` [PATCH v7 " Christian Couder
                               ` (10 preceding siblings ...)
  2023-11-15 14:33             ` [PATCH v7 11/14] replay: use standard revision ranges Christian Couder
@ 2023-11-15 14:33             ` Christian Couder
  2023-11-15 14:33             ` [PATCH v7 13/14] replay: add --contained to rebase contained branches Christian Couder
                               ` (3 subsequent siblings)
  15 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-15 14:33 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

There is already a 'rebase' mode with `--onto`. Let's add an 'advance' or
'cherry-pick' mode with `--advance`. This new mode will make the target
branch advance as we replay commits onto it.

The replayed commits should have a single tip, so that it's clear where
the target branch should be advanced. If they have more than one tip,
this new mode will error out.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 Documentation/git-replay.txt |  41 ++++++--
 builtin/replay.c             | 185 +++++++++++++++++++++++++++++++++--
 t/t3650-replay-basics.sh     |  34 +++++++
 3 files changed, 243 insertions(+), 17 deletions(-)

diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
index fab4ea0178..6daa5b4275 100644
--- a/Documentation/git-replay.txt
+++ b/Documentation/git-replay.txt
@@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
 SYNOPSIS
 --------
 [verse]
-'git replay' --onto <newbase> <revision-range>... # EXPERIMENTAL
+'git replay' (--onto <newbase> | --advance <branch>) <revision-range>... # EXPERIMENTAL
 
 DESCRIPTION
 -----------
@@ -29,14 +29,25 @@ OPTIONS
 	Starting point at which to create the new commits.  May be any
 	valid commit, and not just an existing branch name.
 +
-The update-ref command(s) in the output will update the branch(es) in
-the revision range to point at the new commits, similar to the way how
-`git rebase --update-refs` updates multiple branches in the affected
-range.
+When `--onto` is specified, the update-ref command(s) in the output will
+update the branch(es) in the revision range to point at the new
+commits, similar to the way how `git rebase --update-refs` updates
+multiple branches in the affected range.
+
+--advance <branch>::
+	Starting point at which to create the new commits; must be a
+	branch name.
++
+When `--advance` is specified, the update-ref command(s) in the output
+will update the branch passed as an argument to `--advance` to point at
+the new commits (in other words, this mimics a cherry-pick operation).
 
 <revision-range>::
-	Range of commits to replay; see "Specifying Ranges" in
-	linkgit:git-rev-parse and the "Commit Limiting" options below.
+	Range of commits to replay. More than one <revision-range> can
+	be passed, but in `--advance <branch>` mode, they should have
+	a single tip, so that it's clear where <branch> should point
+	to. See "Specifying Ranges" in linkgit:git-rev-parse and the
+	"Commit Limiting" options below.
 
 include::rev-list-options.txt[]
 
@@ -51,7 +62,9 @@ input to `git update-ref --stdin`.  It is of the form:
 	update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
 
 where the number of refs updated depends on the arguments passed and
-the shape of the history being replayed.
+the shape of the history being replayed.  When using `--advance`, the
+number of refs updated is always one, but for `--onto`, it can be one
+or more (rebasing multiple branches simultaneously is supported).
 
 EXIT STATUS
 -----------
@@ -71,6 +84,18 @@ $ git replay --onto target origin/main..mybranch
 update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
 ------------
 
+To cherry-pick the commits from mybranch onto target:
+
+------------
+$ git replay --advance target origin/main..mybranch
+update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}
+------------
+
+Note that the first two examples replay the exact same commits and on
+top of the exact same new base, they only differ in that the first
+provides instructions to make mybranch point at the new commits and
+the second provides instructions to make target point at them.
+
 When calling `git replay`, one does not need to specify a range of
 commits to replay using the syntax `A..B`; any range expression will
 do:
diff --git a/builtin/replay.c b/builtin/replay.c
index 7a660020d1..1a419cb7fd 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -14,6 +14,7 @@
 #include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
+#include "strmap.h"
 #include <oidset.h>
 #include <tree.h>
 
@@ -82,6 +83,146 @@ static struct commit *create_commit(struct tree *tree,
 	return (struct commit *)obj;
 }
 
+struct ref_info {
+	struct commit *onto;
+	struct strset positive_refs;
+	struct strset negative_refs;
+	int positive_refexprs;
+	int negative_refexprs;
+};
+
+static void get_ref_information(struct rev_cmdline_info *cmd_info,
+				struct ref_info *ref_info)
+{
+	int i;
+
+	ref_info->onto = NULL;
+	strset_init(&ref_info->positive_refs);
+	strset_init(&ref_info->negative_refs);
+	ref_info->positive_refexprs = 0;
+	ref_info->negative_refexprs = 0;
+
+	/*
+	 * When the user specifies e.g.
+	 *   git replay origin/main..mybranch
+	 *   git replay ^origin/next mybranch1 mybranch2
+	 * we want to be able to determine where to replay the commits.  In
+	 * these examples, the branches are probably based on an old version
+	 * of either origin/main or origin/next, so we want to replay on the
+	 * newest version of that branch.  In contrast we would want to error
+	 * out if they ran
+	 *   git replay ^origin/master ^origin/next mybranch
+	 *   git replay mybranch~2..mybranch
+	 * the first of those because there's no unique base to choose, and
+	 * the second because they'd likely just be replaying commits on top
+	 * of the same commit and not making any difference.
+	 */
+	for (i = 0; i < cmd_info->nr; i++) {
+		struct rev_cmdline_entry *e = cmd_info->rev + i;
+		struct object_id oid;
+		const char *refexpr = e->name;
+		char *fullname = NULL;
+		int can_uniquely_dwim = 1;
+
+		if (*refexpr == '^')
+			refexpr++;
+		if (repo_dwim_ref(the_repository, refexpr, strlen(refexpr), &oid, &fullname, 0) != 1)
+			can_uniquely_dwim = 0;
+
+		if (e->flags & BOTTOM) {
+			if (can_uniquely_dwim)
+				strset_add(&ref_info->negative_refs, fullname);
+			if (!ref_info->negative_refexprs)
+				ref_info->onto = lookup_commit_reference_gently(the_repository,
+										&e->item->oid, 1);
+			ref_info->negative_refexprs++;
+		} else {
+			if (can_uniquely_dwim)
+				strset_add(&ref_info->positive_refs, fullname);
+			ref_info->positive_refexprs++;
+		}
+
+		free(fullname);
+	}
+}
+
+static void determine_replay_mode(struct rev_cmdline_info *cmd_info,
+				  const char *onto_name,
+				  const char **advance_name,
+				  struct commit **onto,
+				  struct strset **update_refs)
+{
+	struct ref_info rinfo;
+
+	get_ref_information(cmd_info, &rinfo);
+	if (!rinfo.positive_refexprs)
+		die(_("need some commits to replay"));
+	if (onto_name && *advance_name)
+		die(_("--onto and --advance are incompatible"));
+	else if (onto_name) {
+		*onto = peel_committish(onto_name);
+		if (rinfo.positive_refexprs <
+		    strset_get_size(&rinfo.positive_refs))
+			die(_("all positive revisions given must be references"));
+	} else if (*advance_name) {
+		struct object_id oid;
+		char *fullname = NULL;
+
+		*onto = peel_committish(*advance_name);
+		if (repo_dwim_ref(the_repository, *advance_name, strlen(*advance_name),
+			     &oid, &fullname, 0) == 1) {
+			*advance_name = fullname;
+		} else {
+			die(_("argument to --advance must be a reference"));
+		}
+		if (rinfo.positive_refexprs > 1)
+			die(_("cannot advance target with multiple sources because ordering would be ill-defined"));
+	} else {
+		int positive_refs_complete = (
+			rinfo.positive_refexprs ==
+			strset_get_size(&rinfo.positive_refs));
+		int negative_refs_complete = (
+			rinfo.negative_refexprs ==
+			strset_get_size(&rinfo.negative_refs));
+		/*
+		 * We need either positive_refs_complete or
+		 * negative_refs_complete, but not both.
+		 */
+		if (rinfo.negative_refexprs > 0 &&
+		    positive_refs_complete == negative_refs_complete)
+			die(_("cannot implicitly determine whether this is an --advance or --onto operation"));
+		if (negative_refs_complete) {
+			struct hashmap_iter iter;
+			struct strmap_entry *entry;
+
+			if (rinfo.negative_refexprs == 0)
+				die(_("all positive revisions given must be references"));
+			else if (rinfo.negative_refexprs > 1)
+				die(_("cannot implicitly determine whether this is an --advance or --onto operation"));
+			else if (rinfo.positive_refexprs > 1)
+				die(_("cannot advance target with multiple source branches because ordering would be ill-defined"));
+
+			/* Only one entry, but we have to loop to get it */
+			strset_for_each_entry(&rinfo.negative_refs,
+					      &iter, entry) {
+				*advance_name = entry->key;
+			}
+		} else { /* positive_refs_complete */
+			if (rinfo.negative_refexprs > 1)
+				die(_("cannot implicitly determine correct base for --onto"));
+			if (rinfo.negative_refexprs == 1)
+				*onto = rinfo.onto;
+		}
+	}
+	if (!*advance_name) {
+		*update_refs = xcalloc(1, sizeof(**update_refs));
+		**update_refs = rinfo.positive_refs;
+		memset(&rinfo.positive_refs, 0, sizeof(**update_refs));
+	}
+	strset_clear(&rinfo.negative_refs);
+	strset_clear(&rinfo.positive_refs);
+}
+
 static struct commit *pick_regular_commit(struct commit *pickme,
 					  struct commit *last_commit,
 					  struct merge_options *merge_opt,
@@ -114,20 +255,26 @@ static struct commit *pick_regular_commit(struct commit *pickme,
 
 int cmd_replay(int argc, const char **argv, const char *prefix)
 {
-	struct commit *onto;
+	const char *advance_name = NULL;
+	struct commit *onto = NULL;
 	const char *onto_name = NULL;
-	struct commit *last_commit = NULL;
+
 	struct rev_info revs;
+	struct commit *last_commit = NULL;
 	struct commit *commit;
 	struct merge_options merge_opt;
 	struct merge_result result;
+	struct strset *update_refs = NULL;
 	int ret = 0;
 
 	const char * const replay_usage[] = {
-		N_("git replay --onto <newbase> <revision-range>... # EXPERIMENTAL"),
+		N_("git replay (--onto <newbase> | --advance <branch>) <revision-range>... # EXPERIMENTAL"),
 		NULL
 	};
 	struct option replay_options[] = {
+		OPT_STRING(0, "advance", &advance_name,
+			   N_("branch"),
+			   N_("make replay advance given branch")),
 		OPT_STRING(0, "onto", &onto_name,
 			   N_("revision"),
 			   N_("replay onto given commit")),
@@ -137,13 +284,11 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
 			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
 
-	if (!onto_name) {
-		error(_("option --onto is mandatory"));
+	if (!onto_name && !advance_name) {
+		error(_("option --onto or --advance is mandatory"));
 		usage_with_options(replay_usage, replay_options);
 	}
 
-	onto = peel_committish(onto_name);
-
 	repo_init_revisions(the_repository, &revs, prefix);
 
 	/*
@@ -195,6 +340,12 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		revs.simplify_history = 0;
 	}
 
+	determine_replay_mode(&revs.cmdline, onto_name, &advance_name,
+			      &onto, &update_refs);
+
+	if (!onto) /* FIXME: Should handle replaying down to root commit */
+		die("Replaying down to root commit is not supported yet!");
+
 	if (prepare_revision_walk(&revs) < 0) {
 		ret = error(_("error preparing revisions"));
 		goto cleanup;
@@ -203,6 +354,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
 	merge_opt.show_rename_progress = 0;
+
 	result.tree = repo_get_commit_tree(the_repository, onto);
 	last_commit = onto;
 	while ((commit = get_revision(&revs))) {
@@ -217,12 +369,15 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		if (!last_commit)
 			break;
 
+		/* Update any necessary branches */
+		if (advance_name)
+			continue;
 		decoration = get_name_decoration(&commit->object);
 		if (!decoration)
 			continue;
-
 		while (decoration) {
-			if (decoration->type == DECORATION_REF_LOCAL) {
+			if (decoration->type == DECORATION_REF_LOCAL &&
+			    strset_contains(update_refs, decoration->name)) {
 				printf("update %s %s %s\n",
 				       decoration->name,
 				       oid_to_hex(&last_commit->object.oid),
@@ -232,10 +387,22 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		}
 	}
 
+	/* In --advance mode, advance the target ref */
+	if (result.clean == 1 && advance_name) {
+		printf("update %s %s %s\n",
+		       advance_name,
+		       oid_to_hex(&last_commit->object.oid),
+		       oid_to_hex(&onto->object.oid));
+	}
+
 	merge_finalize(&merge_opt, &result);
 	ret = result.clean;
 
 cleanup:
+	if (update_refs) {
+		strset_clear(update_refs);
+		free(update_refs);
+	}
 	release_revisions(&revs);
 
 	/* Return */
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index a1da4f9ef9..68a87e7803 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -80,4 +80,38 @@ test_expect_success 'using replay on bare repo to rebase with a conflict' '
 	test_expect_code 1 git -C bare replay --onto topic1 B..conflict
 '
 
+test_expect_success 'using replay to perform basic cherry-pick' '
+	# The differences between this test and previous ones are:
+	#   --advance vs --onto
+	# 2nd field of result is refs/heads/main vs. refs/heads/topic2
+	# 4th field of result is hash for main instead of hash for topic2
+
+	git replay --advance main topic1..topic2 >result &&
+
+	test_line_count = 1 result &&
+
+	git log --format=%s $(cut -f 3 -d " " result) >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/main " >expect &&
+	printf "%s " $(cut -f 3 -d " " result) >>expect &&
+	git rev-parse main >>expect &&
+
+	test_cmp expect result
+'
+
+test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
+	git -C bare replay --advance main topic1..topic2 >result-bare &&
+	test_cmp expect result-bare
+'
+
+test_expect_success 'replay on bare repo fails with both --advance and --onto' '
+	test_must_fail git -C bare replay --advance main --onto main topic1..topic2 >result-bare
+'
+
+test_expect_success 'replay fails when both --advance and --onto are omitted' '
+	test_must_fail git replay topic1..topic2 >result
+'
+
 test_done
-- 
2.43.0.rc1.15.g29556bcc86


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v7 13/14] replay: add --contained to rebase contained branches
  2023-11-15 14:33           ` [PATCH v7 " Christian Couder
                               ` (11 preceding siblings ...)
  2023-11-15 14:33             ` [PATCH v7 12/14] replay: add --advance or 'cherry-pick' mode Christian Couder
@ 2023-11-15 14:33             ` Christian Couder
  2023-11-15 14:33             ` [PATCH v7 14/14] replay: stop assuming replayed branches do not diverge Christian Couder
                               ` (2 subsequent siblings)
  15 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-15 14:33 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

Let's add a `--contained` option that can be used along with
`--onto` to rebase all the branches contained in the <revision-range>
argument.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 Documentation/git-replay.txt | 12 +++++++++++-
 builtin/replay.c             | 13 +++++++++++--
 t/t3650-replay-basics.sh     | 29 +++++++++++++++++++++++++++++
 3 files changed, 51 insertions(+), 3 deletions(-)

diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
index 6daa5b4275..133d7af9ee 100644
--- a/Documentation/git-replay.txt
+++ b/Documentation/git-replay.txt
@@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
 SYNOPSIS
 --------
 [verse]
-'git replay' (--onto <newbase> | --advance <branch>) <revision-range>... # EXPERIMENTAL
+'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>... # EXPERIMENTAL
 
 DESCRIPTION
 -----------
@@ -96,6 +96,16 @@ top of the exact same new base, they only differ in that the first
 provides instructions to make mybranch point at the new commits and
 the second provides instructions to make target point at them.
 
+What if you have a stack of branches, one depending upon another, and
+you'd really like to rebase the whole set?
+
+------------
+$ git replay --contained --onto origin/main origin/main..tipbranch
+update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
+update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
+update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}
+------------
+
 When calling `git replay`, one does not need to specify a range of
 commits to replay using the syntax `A..B`; any range expression will
 do:
diff --git a/builtin/replay.c b/builtin/replay.c
index 1a419cb7fd..e14e33bcc5 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -258,6 +258,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	const char *advance_name = NULL;
 	struct commit *onto = NULL;
 	const char *onto_name = NULL;
+	int contained = 0;
 
 	struct rev_info revs;
 	struct commit *last_commit = NULL;
@@ -268,7 +269,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	int ret = 0;
 
 	const char * const replay_usage[] = {
-		N_("git replay (--onto <newbase> | --advance <branch>) <revision-range>... # EXPERIMENTAL"),
+		N_("git replay ([--contained] --onto <newbase> | --advance <branch>) "
+		   "<revision-range>... # EXPERIMENTAL"),
 		NULL
 	};
 	struct option replay_options[] = {
@@ -278,6 +280,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		OPT_STRING(0, "onto", &onto_name,
 			   N_("revision"),
 			   N_("replay onto given commit")),
+		OPT_BOOL(0, "contained", &contained,
+			 N_("advance all branches contained in revision-range")),
 		OPT_END()
 	};
 
@@ -289,6 +293,10 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		usage_with_options(replay_usage, replay_options);
 	}
 
+	if (advance_name && contained)
+		die(_("options '%s' and '%s' cannot be used together"),
+		    "--advance", "--contained");
+
 	repo_init_revisions(the_repository, &revs, prefix);
 
 	/*
@@ -377,7 +385,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			continue;
 		while (decoration) {
 			if (decoration->type == DECORATION_REF_LOCAL &&
-			    strset_contains(update_refs, decoration->name)) {
+			    (contained || strset_contains(update_refs,
+							  decoration->name))) {
 				printf("update %s %s %s\n",
 				       decoration->name,
 				       oid_to_hex(&last_commit->object.oid),
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 68a87e7803..d6286f9580 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -114,4 +114,33 @@ test_expect_success 'replay fails when both --advance and --onto are omitted' '
 	test_must_fail git replay topic1..topic2 >result
 '
 
+test_expect_success 'using replay to also rebase a contained branch' '
+	git replay --contained --onto main main..topic3 >result &&
+
+	test_line_count = 2 result &&
+	cut -f 3 -d " " result >new-branch-tips &&
+
+	git log --format=%s $(head -n 1 new-branch-tips) >actual &&
+	test_write_lines F C M L B A >expect &&
+	test_cmp expect actual &&
+
+	git log --format=%s $(tail -n 1 new-branch-tips) >actual &&
+	test_write_lines H G F C M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/topic1 " >expect &&
+	printf "%s " $(head -n 1 new-branch-tips) >>expect &&
+	git rev-parse topic1 >>expect &&
+	printf "update refs/heads/topic3 " >>expect &&
+	printf "%s " $(tail -n 1 new-branch-tips) >>expect &&
+	git rev-parse topic3 >>expect &&
+
+	test_cmp expect result
+'
+
+test_expect_success 'using replay on bare repo to also rebase a contained branch' '
+	git -C bare replay --contained --onto main main..topic3 >result-bare &&
+	test_cmp expect result-bare
+'
+
 test_done
-- 
2.43.0.rc1.15.g29556bcc86


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v7 14/14] replay: stop assuming replayed branches do not diverge
  2023-11-15 14:33           ` [PATCH v7 " Christian Couder
                               ` (12 preceding siblings ...)
  2023-11-15 14:33             ` [PATCH v7 13/14] replay: add --contained to rebase contained branches Christian Couder
@ 2023-11-15 14:33             ` Christian Couder
  2023-11-16  8:53             ` [PATCH v7 00/14] Introduce new `git replay` command Johannes Schindelin
  2023-11-24 11:10             ` [PATCH v8 " Christian Couder
  15 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-15 14:33 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

The replay command is able to replay multiple branches but when some of
them are based on other replayed branches, their commit should be
replayed onto already replayed commits.

For this purpose, let's store the replayed commit and its original
commit in a key value store, so that we can easily find and reuse a
replayed commit instead of the original one.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c         | 44 ++++++++++++++++++++++++++--------
 t/t3650-replay-basics.sh | 52 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 86 insertions(+), 10 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index e14e33bcc5..f37e511d8e 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -223,20 +223,33 @@ static void determine_replay_mode(struct rev_cmdline_info *cmd_info,
 	strset_clear(&rinfo.positive_refs);
 }
 
+static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,
+				    struct commit *commit,
+				    struct commit *fallback)
+{
+	khint_t pos = kh_get_oid_map(replayed_commits, commit->object.oid);
+	if (pos == kh_end(replayed_commits))
+		return fallback;
+	return kh_value(replayed_commits, pos);
+}
+
 static struct commit *pick_regular_commit(struct commit *pickme,
-					  struct commit *last_commit,
+					  kh_oid_map_t *replayed_commits,
+					  struct commit *onto,
 					  struct merge_options *merge_opt,
 					  struct merge_result *result)
 {
-	struct commit *base;
+	struct commit *base, *replayed_base;
 	struct tree *pickme_tree, *base_tree;
 
 	base = pickme->parents->item;
+	replayed_base = mapped_commit(replayed_commits, base, onto);
 
+	result->tree = repo_get_commit_tree(the_repository, replayed_base);
 	pickme_tree = repo_get_commit_tree(the_repository, pickme);
 	base_tree = repo_get_commit_tree(the_repository, base);
 
-	merge_opt->branch1 = short_commit_name(last_commit);
+	merge_opt->branch1 = short_commit_name(replayed_base);
 	merge_opt->branch2 = short_commit_name(pickme);
 	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
 
@@ -250,7 +263,7 @@ static struct commit *pick_regular_commit(struct commit *pickme,
 	merge_opt->ancestor = NULL;
 	if (!result->clean)
 		return NULL;
-	return create_commit(result->tree, pickme, last_commit);
+	return create_commit(result->tree, pickme, replayed_base);
 }
 
 int cmd_replay(int argc, const char **argv, const char *prefix)
@@ -266,6 +279,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct merge_options merge_opt;
 	struct merge_result result;
 	struct strset *update_refs = NULL;
+	kh_oid_map_t *replayed_commits;
 	int ret = 0;
 
 	const char * const replay_usage[] = {
@@ -362,21 +376,30 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
 	merge_opt.show_rename_progress = 0;
-
-	result.tree = repo_get_commit_tree(the_repository, onto);
 	last_commit = onto;
+	replayed_commits = kh_init_oid_map();
 	while ((commit = get_revision(&revs))) {
 		const struct name_decoration *decoration;
+		khint_t pos;
+		int hr;
 
 		if (!commit->parents)
 			die(_("replaying down to root commit is not supported yet!"));
 		if (commit->parents->next)
 			die(_("replaying merge commits is not supported yet!"));
 
-		last_commit = pick_regular_commit(commit, last_commit, &merge_opt, &result);
+		last_commit = pick_regular_commit(commit, replayed_commits, onto,
+						  &merge_opt, &result);
 		if (!last_commit)
 			break;
 
+		/* Record commit -> last_commit mapping */
+		pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr);
+		if (hr == 0)
+			BUG("Duplicate rewritten commit: %s\n",
+			    oid_to_hex(&commit->object.oid));
+		kh_value(replayed_commits, pos) = last_commit;
+
 		/* Update any necessary branches */
 		if (advance_name)
 			continue;
@@ -405,13 +428,14 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	}
 
 	merge_finalize(&merge_opt, &result);
-	ret = result.clean;
-
-cleanup:
+	kh_destroy_oid_map(replayed_commits);
 	if (update_refs) {
 		strset_clear(update_refs);
 		free(update_refs);
 	}
+	ret = result.clean;
+
+cleanup:
 	release_revisions(&revs);
 
 	/* Return */
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index d6286f9580..389670262e 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -143,4 +143,56 @@ test_expect_success 'using replay on bare repo to also rebase a contained branch
 	test_cmp expect result-bare
 '
 
+test_expect_success 'using replay to rebase multiple divergent branches' '
+	git replay --onto main ^topic1 topic2 topic4 >result &&
+
+	test_line_count = 2 result &&
+	cut -f 3 -d " " result >new-branch-tips &&
+
+	git log --format=%s $(head -n 1 new-branch-tips) >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual &&
+
+	git log --format=%s $(tail -n 1 new-branch-tips) >actual &&
+	test_write_lines J I M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/topic2 " >expect &&
+	printf "%s " $(head -n 1 new-branch-tips) >>expect &&
+	git rev-parse topic2 >>expect &&
+	printf "update refs/heads/topic4 " >>expect &&
+	printf "%s " $(tail -n 1 new-branch-tips) >>expect &&
+	git rev-parse topic4 >>expect &&
+
+	test_cmp expect result
+'
+
+test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '
+	git -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&
+
+	test_line_count = 4 result &&
+	cut -f 3 -d " " result >new-branch-tips &&
+
+	>expect &&
+	for i in 2 1 3 4
+	do
+		printf "update refs/heads/topic$i " >>expect &&
+		printf "%s " $(grep topic$i result | cut -f 3 -d " ") >>expect &&
+		git -C bare rev-parse topic$i >>expect || return 1
+	done &&
+
+	test_cmp expect result &&
+
+	test_write_lines F C M L B A >expect1 &&
+	test_write_lines E D C M L B A >expect2 &&
+	test_write_lines H G F C M L B A >expect3 &&
+	test_write_lines J I M L B A >expect4 &&
+
+	for i in 1 2 3 4
+	do
+		git -C bare log --format=%s $(grep topic$i result | cut -f 3 -d " ") >actual &&
+		test_cmp expect$i actual || return 1
+	done
+'
+
 test_done
-- 
2.43.0.rc1.15.g29556bcc86


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* Re: [PATCH v6 00/14] Introduce new `git replay` command
  2023-11-08 12:47           ` Johannes Schindelin
@ 2023-11-15 14:46             ` Christian Couder
  2023-11-16  8:45               ` Johannes Schindelin
  0 siblings, 1 reply; 208+ messages in thread
From: Christian Couder @ 2023-11-15 14:46 UTC (permalink / raw)
  To: Johannes Schindelin
  Cc: git, Junio C Hamano, Patrick Steinhardt, Elijah Newren, John Cai,
	Derrick Stolee, Phillip Wood, Calvin Wan, Toon Claes,
	Dragan Simic, Linus Arver

Hi Dscho,

On Wed, Nov 8, 2023 at 1:47 PM Johannes Schindelin
<Johannes.Schindelin@gmx.de> wrote:
> On Thu, 2 Nov 2023, Christian Couder wrote:

> >     + ## Documentation/git-replay.txt (new) ##
> >     +@@
> >     ++git-replay(1)
> >     ++=============
> >     ++
> >     ++NAME
> >     ++----
> >     ++git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos too
> >     ++
> >     ++
> >     ++SYNOPSIS
> >     ++--------
> >     ++[verse]
> >     ++'git replay' --onto <newbase> <revision-range>... # EXPERIMENTAL
>
> Technically, at this stage `git replay` requires precisely 5 arguments, so
> the `<revision>...` is incorrect and should be `<upstream> <branch>`
> instead. But it's not worth a new iteration to fix this.

It was actually:

'git replay' --onto <newbase> <oldbase> <branch> # EXPERIMENTAL

(you can see it there:
https://lore.kernel.org/git/20231102135151.843758-3-christian.couder@gmail.com/)

But I made a mistake in the range-diff command as I ran it against a
previous development version instead of the current one.

> >     ++
> >     ++DESCRIPTION
> >     ++-----------
> >     ++
> >     ++Takes a range of commits and replays them onto a new location.
> >     ++
> >     ++THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
> >     ++
> >     ++OPTIONS
> >     ++-------
> >     ++
> >     ++--onto <newbase>::
> >     ++  Starting point at which to create the new commits.  May be any
> >     ++  valid commit, and not just an existing branch name.
> >     ++
>
> Traditionally, this would be a place to describe the `<revision>` argument
> (or, in this patch, to reflect the current state of `builtin/replay.c`,
> the `<upstream> <branch>` arguments instead).

I have fixed that in the v7 I just sent with the following:

+SYNOPSIS
+--------
+[verse]
+'git replay' --onto <newbase> <oldbase> <branch> # EXPERIMENTAL
+
+DESCRIPTION
+-----------
+
+Takes a range of commits, specified by <oldbase> and <branch>, and
+replays them onto a new location (see `--onto` option below).
+
+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
+

Thanks for your review!

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v6 00/14] Introduce new `git replay` command
  2023-11-07  9:43             ` Christian Couder
@ 2023-11-15 14:51               ` Christian Couder
  0 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-15 14:51 UTC (permalink / raw)
  To: Elijah Newren, Johannes Schindelin
  Cc: git, Junio C Hamano, Patrick Steinhardt, John Cai, Derrick Stolee,
	Phillip Wood, Calvin Wan, Toon Claes, Dragan Simic, Linus Arver

Hi Elijah and Dscho,

On Tue, Nov 7, 2023 at 10:43 AM Christian Couder
<christian.couder@gmail.com> wrote:
> On Tue, Nov 7, 2023 at 3:44 AM Elijah Newren <newren@gmail.com> wrote:
> > Looking good, just one comment on one small hunk...
> >
> > On Thu, Nov 2, 2023 at 6:52 AM Christian Couder
> > <christian.couder@gmail.com> wrote:
> > >
> > [...]
> >
> > >     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
> > >      -
> > >         strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
> > >
> > >     ++  /*
> > >     ++   * TODO: For now, let's warn when we see an option that we are
> > >     ++   * going to override after setup_revisions() below. In the
> > >     ++   * future we might want to either die() or allow them if we
> > >     ++   * think they could be useful though.
> > >     ++   */
> > >     ++  for (i = 0; i < argc; i++) {
> > >     ++          if (!strcmp(argv[i], "--reverse") || !strcmp(argv[i], "--date-order") ||
> > >     ++              !strcmp(argv[i], "--topo-order") || !strcmp(argv[i], "--author-date-order") ||
> > >     ++              !strcmp(argv[i], "--full-history"))
> > >     ++                  warning(_("option '%s' will be overridden"), argv[i]);
> > >     ++  }
> > >     ++

> > 2) This seems like an inefficient way to provide this warning; could
> > we avoid parsing the arguments for an extra time?  Perhaps instead
> >   a) set the desired values here, before setup_revisions()
> >   b) after setup_revisions, check whether these values differ from the
> > desired values, if so throw a warning.
> >   c) set the desired values, again
>
> Yeah, that would work. The downside is that it would be more difficult
> in the warning to tell the user which command line option was
> overridden as there are some values changed by different options.
> Maybe we can come up with a warning message that is still useful and
> enough for now, as the command is supposed to be used by experienced
> users. Perhaps something like:
>
> warning(_("some rev walking options will be overridden as '%s' bit in
> 'struct rev_info' will be forced"), "sort_order");

I have implemented this in the v7 I just sent.

Thanks!

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v6 00/14] Introduce new `git replay` command
  2023-11-15 14:46             ` Christian Couder
@ 2023-11-16  8:45               ` Johannes Schindelin
  2023-11-16  8:52                 ` Christian Couder
  0 siblings, 1 reply; 208+ messages in thread
From: Johannes Schindelin @ 2023-11-16  8:45 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Patrick Steinhardt, Elijah Newren, John Cai,
	Derrick Stolee, Phillip Wood, Calvin Wan, Toon Claes,
	Dragan Simic, Linus Arver

[-- Attachment #1: Type: text/plain, Size: 2090 bytes --]

Hi Christian,

On Wed, 15 Nov 2023, Christian Couder wrote:

> On Wed, Nov 8, 2023 at 1:47 PM Johannes Schindelin
> <Johannes.Schindelin@gmx.de> wrote:
> > On Thu, 2 Nov 2023, Christian Couder wrote:
>
> > >     + ## Documentation/git-replay.txt (new) ##
> > >     +@@
> > >     ++git-replay(1)
> > >     ++=============
> > >     ++
> > >     ++NAME
> > >     ++----
> > >     ++git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos too
> > >     ++
> > >     ++
> > >     ++SYNOPSIS
> > >     ++--------
> > >     ++[verse]
> > >     ++'git replay' --onto <newbase> <revision-range>... # EXPERIMENTAL
> >
> > Technically, at this stage `git replay` requires precisely 5 arguments, so
> > the `<revision>...` is incorrect and should be `<upstream> <branch>`
> > instead. But it's not worth a new iteration to fix this.
>
> It was actually:
>
> 'git replay' --onto <newbase> <oldbase> <branch> # EXPERIMENTAL

Right.

> > >     ++
> > >     ++DESCRIPTION
> > >     ++-----------
> > >     ++
> > >     ++Takes a range of commits and replays them onto a new location.
> > >     ++
> > >     ++THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
> > >     ++
> > >     ++OPTIONS
> > >     ++-------
> > >     ++
> > >     ++--onto <newbase>::
> > >     ++  Starting point at which to create the new commits.  May be any
> > >     ++  valid commit, and not just an existing branch name.
> > >     ++
> >
> > Traditionally, this would be a place to describe the `<revision>` argument
> > (or, in this patch, to reflect the current state of `builtin/replay.c`,
> > the `<upstream> <branch>` arguments instead).
>
> I have fixed that in the v7 I just sent with the following:
>
> +SYNOPSIS
> +--------
> +[verse]
> +'git replay' --onto <newbase> <oldbase> <branch> # EXPERIMENTAL

I still think that the following would serve us better:

	[verse]
	(EXPERIMENTAL!) 'git replay' --onto <newbase> <oldbase> <branch>

But if nobody else feels as strongly, I won't bring this up again.

Ciao,
Johannes

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v6 00/14] Introduce new `git replay` command
  2023-11-16  8:45               ` Johannes Schindelin
@ 2023-11-16  8:52                 ` Christian Couder
  0 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-16  8:52 UTC (permalink / raw)
  To: Johannes Schindelin
  Cc: git, Junio C Hamano, Patrick Steinhardt, Elijah Newren, John Cai,
	Derrick Stolee, Phillip Wood, Calvin Wan, Toon Claes,
	Dragan Simic, Linus Arver

Hi Dscho,

On Thu, Nov 16, 2023 at 9:45 AM Johannes Schindelin
<Johannes.Schindelin@gmx.de> wrote:
> On Wed, 15 Nov 2023, Christian Couder wrote:

> > I have fixed that in the v7 I just sent with the following:
> >
> > +SYNOPSIS
> > +--------
> > +[verse]
> > +'git replay' --onto <newbase> <oldbase> <branch> # EXPERIMENTAL
>
> I still think that the following would serve us better:
>
>         [verse]
>         (EXPERIMENTAL!) 'git replay' --onto <newbase> <oldbase> <branch>
>
> But if nobody else feels as strongly, I won't bring this up again.

For the tests to pass, the SYNOPSIS should be the same as the first
line of the `git replay -h` output. After this series it is:

usage: git replay ([--contained] --onto <newbase> | --advance
<branch>) <revision-range>... # EXPERIMENTAL

As the usage is supposed to describe how the command should be used, I
think it makes sense for "EXPERIMENTAL" to be in a shell comment after
the command.

Thanks,
Christian.

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v7 00/14] Introduce new `git replay` command
  2023-11-15 14:33           ` [PATCH v7 " Christian Couder
                               ` (13 preceding siblings ...)
  2023-11-15 14:33             ` [PATCH v7 14/14] replay: stop assuming replayed branches do not diverge Christian Couder
@ 2023-11-16  8:53             ` Johannes Schindelin
  2023-11-23 19:32               ` Elijah Newren
  2023-11-24 11:10             ` [PATCH v8 " Christian Couder
  15 siblings, 1 reply; 208+ messages in thread
From: Johannes Schindelin @ 2023-11-16  8:53 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Patrick Steinhardt, Elijah Newren, John Cai,
	Derrick Stolee, Phillip Wood, Calvin Wan, Toon Claes,
	Dragan Simic, Linus Arver

Hi Christian,

[focusing exclusively on the `range-diff` because I lack the capacity for
anything beyond that]

On Wed, 15 Nov 2023, Christian Couder wrote:

> # Range-diff between v6 and v7
>
> (Sorry it looks like patch 6/14 in v7 is considered to be completely
> different from what it was in v6, so the range-diff is not showing
> differences between them.)
>
>  1:  fac0a9dff4 =  1:  cddcd967b2 t6429: remove switching aspects of fast-rebase
>  2:  bec2eb8928 !  2:  c8476fb093 replay: introduce new builtin
>     @@ Documentation/git-replay.txt (new)
>      +DESCRIPTION
>      +-----------
>      +
>     -+Takes a range of commits and replays them onto a new location.
>     ++Takes a range of commits, specified by <oldbase> and <branch>, and
>     ++replays them onto a new location (see `--onto` option below).
>      +
>      +THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
>      +

Thank you.

>  3:  b0cdfdc0c3 =  3:  43322abd1e replay: start using parse_options API
>  4:  c3403f0b9d =  4:  6524c7f045 replay: die() instead of failing assert()
>  5:  4188eeac30 =  5:  05d0efa3cb replay: introduce pick_regular_commit()
>  6:  b7b4d9001e <  -:  ---------- replay: change rev walking options
>  -:  ---------- >  6:  c7a5aad3d6 replay: change rev walking options

The actual range-diff for the sixth patch looks like this:

-- snip --
6:  b7b4d9001e9 ! 6:  c7a5aad3d62 replay: change rev walking options
    @@ Metadata
      ## Commit message ##
         replay: change rev walking options

    -    Let's set the rev walking options we need after calling
    -    setup_revisions() instead of before. This enforces options we always
    -    want for now.
    +    Let's force the rev walking options we need after calling
    +    setup_revisions() instead of before.
    +
    +    This might override some user supplied rev walking command line options
    +    though. So let's detect that and warn users by:
    +
    +      a) setting the desired values, before setup_revisions(),
    +      b) checking after setup_revisions() whether these values differ from
    +         the desired values,
    +      c) if so throwing a warning and setting the desired values again.

         We want the command to work from older commits to newer ones by default.
         Also we don't want history simplification, as we want to deal with all
         the commits in the affected range.

    -    When we see an option that we are going to override, we emit a warning
    -    to avoid confusion as much as possible though.
    -
         Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
         Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
         Signed-off-by: Elijah Newren <newren@gmail.com>
    @@ Commit message

      ## builtin/replay.c ##
     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix)
    - 	struct merge_result result;
    - 	struct strbuf reflog_msg = STRBUF_INIT;
    - 	struct strbuf branch_name = STRBUF_INIT;
    --	int ret = 0;
    -+	int i, ret = 0;
    -
    - 	const char * const replay_usage[] = {
    - 		N_("git replay --onto <newbase> <oldbase> <branch> # EXPERIMENTAL"),
    -@@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix)

      	repo_init_revisions(the_repository, &revs, prefix);

    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
     -	revs.max_parents = 1;
     -	revs.cherry_mark = 1;
     -	revs.limited = 1;
    --	revs.reverse = 1;
    ++	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
    ++
    ++	/*
    ++	 * Set desired values for rev walking options here. If they
    ++	 * are changed by some user specified option in setup_revisions()
    ++	 * below, we will detect that below and then warn.
    ++	 *
    ++	 * TODO: In the future we might want to either die(), or allow
    ++	 * some options changing these values if we think they could
    ++	 * be useful.
    ++	 */
    + 	revs.reverse = 1;
     -	revs.right_only = 1;
    --	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
    --	revs.topo_order = 1;
    + 	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
    + 	revs.topo_order = 1;
     -
    - 	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
    +-	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
    ++	revs.simplify_history = 0;

    -+	/*
    -+	 * TODO: For now, let's warn when we see an option that we are
    -+	 * going to override after setup_revisions() below. In the
    -+	 * future we might want to either die() or allow them if we
    -+	 * think they could be useful though.
    -+	 */
    -+	for (i = 0; i < argc; i++) {
    -+		if (!strcmp(argv[i], "--reverse") || !strcmp(argv[i], "--date-order") ||
    -+		    !strcmp(argv[i], "--topo-order") || !strcmp(argv[i], "--author-date-order") ||
    -+		    !strcmp(argv[i], "--full-history"))
    -+			warning(_("option '%s' will be overridden"), argv[i]);
    -+	}
    -+
      	if (setup_revisions(rev_walk_args.nr, rev_walk_args.v, &revs, NULL) > 1) {
      		ret = error(_("unhandled options"));
      		goto cleanup;
      	}

    -+	/* requirements/overrides for revs */
    -+	revs.reverse = 1;
    -+	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
    -+	revs.topo_order = 1;
    -+	revs.simplify_history = 0;
    ++	/*
    ++	 * Detect and warn if we override some user specified rev
    ++	 * walking options.
    ++	 */
    ++	if (revs.reverse != 1) {
    ++		warning(_("some rev walking options will be overridden as "
    ++			  "'%s' bit in 'struct rev_info' will be forced"),
    ++			"reverse");
    ++		revs.reverse = 1;
    ++	}
    ++	if (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) {
    ++		warning(_("some rev walking options will be overridden as "
    ++			  "'%s' bit in 'struct rev_info' will be forced"),
    ++			"sort_order");
    ++		revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
    ++	}
    ++	if (revs.topo_order != 1) {
    ++		warning(_("some rev walking options will be overridden as "
    ++			  "'%s' bit in 'struct rev_info' will be forced"),
    ++			"topo_order");
    ++		revs.topo_order = 1;
    ++	}
    ++	if (revs.simplify_history != 0) {
    ++		warning(_("some rev walking options will be overridden as "
    ++			  "'%s' bit in 'struct rev_info' will be forced"),
    ++			"simplify_history");
    ++		revs.simplify_history = 0;
    ++	}
     +
      	strvec_clear(&rev_walk_args);

-- snap --

This looks really good. Thank you for going the extra step to make this
patch so much better.

>  7:  c57577a9b8 =  7:  01f35f924b replay: add an important FIXME comment about gpg signing
>  8:  e78be50f3d =  8:  1498b24bad replay: remove progress and info output
>  9:  e4c79b676f =  9:  6786fc147b replay: remove HEAD related sanity check
> 10:  8d89f1b733 ! 10:  9a24dbb530 replay: make it a minimal server side command
>     @@ Commit message
>          Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
>
>       ## Documentation/git-replay.txt ##
>     -@@ Documentation/git-replay.txt: SYNOPSIS
>     - DESCRIPTION
>     +@@ Documentation/git-replay.txt: DESCRIPTION
>       -----------
>
>     --Takes a range of commits and replays them onto a new location.
>     -+Takes a range of commits and replays them onto a new location. Leaves
>     -+the working tree and the index untouched, and updates no
>     -+references. The output of this command is meant to be used as input to
>     + Takes a range of commits, specified by <oldbase> and <branch>, and
>     +-replays them onto a new location (see `--onto` option below).
>     ++replays them onto a new location (see `--onto` option below). Leaves
>     ++the working tree and the index untouched, and updates no references.
>     ++The output of this command is meant to be used as input to
>      +`git update-ref --stdin`, which would update the relevant branches.
>
>       THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
>     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
>         struct merge_result result;
>      -  struct strbuf reflog_msg = STRBUF_INIT;
>         struct strbuf branch_name = STRBUF_INIT;
>     -   int i, ret = 0;
>     +   int ret = 0;
>
>      @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix)
>         onto = peel_committish(onto_name);

Looks good to me.

> 11:  3d433a1322 ! 11:  ad6ca2fbef replay: use standard revision ranges
>     @@ Documentation/git-replay.txt: git-replay - EXPERIMENTAL: Replay commits on a new
>
>       DESCRIPTION
>       -----------
>     -@@ Documentation/git-replay.txt: DESCRIPTION
>     - Takes a range of commits and replays them onto a new location. Leaves
>     - the working tree and the index untouched, and updates no
>     - references. The output of this command is meant to be used as input to
>     +
>     +-Takes a range of commits, specified by <oldbase> and <branch>, and
>     +-replays them onto a new location (see `--onto` option below). Leaves
>     ++Takes ranges of commits and replays them onto a new location. Leaves
>     + the working tree and the index untouched, and updates no references.
>     + The output of this command is meant to be used as input to
>      -`git update-ref --stdin`, which would update the relevant branches.
>      +`git update-ref --stdin`, which would update the relevant branches
>      +(see the OUTPUT section below).
>     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
>         struct merge_options merge_opt;
>         struct merge_result result;
>      -  struct strbuf branch_name = STRBUF_INIT;
>     -   int i, ret = 0;
>     +   int ret = 0;
>
>         const char * const replay_usage[] = {
>      -          N_("git replay --onto <newbase> <oldbase> <branch> # EXPERIMENTAL"),
>     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
>      -  strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
>      -
>         /*
>     -    * TODO: For now, let's warn when we see an option that we are
>     -    * going to override after setup_revisions() below. In the
>     +    * Set desired values for rev walking options here. If they
>     +    * are changed by some user specified option in setup_revisions()
>      @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix)
>     -                   warning(_("option '%s' will be overridden"), argv[i]);
>     -   }
>     +   revs.topo_order = 1;
>     +   revs.simplify_history = 0;
>
>      -  if (setup_revisions(rev_walk_args.nr, rev_walk_args.v, &revs, NULL) > 1) {
>      -          ret = error(_("unhandled options"));
>     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
>         }
>
>      @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix)
>     -   revs.topo_order = 1;
>     -   revs.simplify_history = 0;
>     +           revs.simplify_history = 0;
>     +   }
>
>      -  strvec_clear(&rev_walk_args);
>      -

This is the correct spot for that documentation change. Good.

> 12:  cca8105382 ! 12:  081864ed5f replay: add --advance or 'cherry-pick' mode
>     @@ builtin/replay.c: static struct commit *pick_regular_commit(struct commit *pickm
>         struct merge_options merge_opt;
>         struct merge_result result;
>      +  struct strset *update_refs = NULL;
>     -   int i, ret = 0;
>     +   int ret = 0;
>
>         const char * const replay_usage[] = {
>      -          N_("git replay --onto <newbase> <revision-range>... # EXPERIMENTAL"),
>     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
>
>         /*
>      @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix)
>     -   revs.topo_order = 1;
>     -   revs.simplify_history = 0;
>     +           revs.simplify_history = 0;
>     +   }
>
>      +  determine_replay_mode(&revs.cmdline, onto_name, &advance_name,
>      +                        &onto, &update_refs);
> 13:  92287a2cc8 ! 13:  19c4016c7c replay: add --contained to rebase contained branches
>     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
>         struct rev_info revs;
>         struct commit *last_commit = NULL;
>      @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix)
>     -   int i, ret = 0;
>     +   int ret = 0;
>
>         const char * const replay_usage[] = {
>      -          N_("git replay (--onto <newbase> | --advance <branch>) <revision-range>... # EXPERIMENTAL"),
> 14:  529a7fda40 ! 14:  29556bcc86 replay: stop assuming replayed branches do not diverge
>     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
>         struct merge_result result;
>         struct strset *update_refs = NULL;
>      +  kh_oid_map_t *replayed_commits;
>     -   int i, ret = 0;
>     +   int ret = 0;
>
>         const char * const replay_usage[] = {
>      @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix)

The last three only have context-only changes. Obviously good.

Apart from the one little outstanding nit where I would love to see
`(EXPERIMENTAL!)` as the first word of the synopsis both in the manual
page and in the output of `git replay -h`, you have addressed all of my
concerns.

Thank you!
Johannes

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v7 00/14] Introduce new `git replay` command
  2023-11-16  8:53             ` [PATCH v7 00/14] Introduce new `git replay` command Johannes Schindelin
@ 2023-11-23 19:32               ` Elijah Newren
  2023-11-24  0:28                 ` Junio C Hamano
  0 siblings, 1 reply; 208+ messages in thread
From: Elijah Newren @ 2023-11-23 19:32 UTC (permalink / raw)
  To: Johannes Schindelin
  Cc: Christian Couder, git, Junio C Hamano, Patrick Steinhardt,
	John Cai, Derrick Stolee, Phillip Wood, Calvin Wan, Toon Claes,
	Dragan Simic, Linus Arver

Hi Christian,

On Thu, Nov 16, 2023 at 12:53 AM Johannes Schindelin
<Johannes.Schindelin@gmx.de> wrote:
> On Wed, 15 Nov 2023, Christian Couder wrote:
>
> > # Range-diff between v6 and v7
> >
[...]
> Apart from the one little outstanding nit where I would love to see
> `(EXPERIMENTAL!)` as the first word of the synopsis both in the manual
> page and in the output of `git replay -h`, you have addressed all of my
> concerns.
>
> Thank you!
> Johannes

Looks good to me too.  Thanks!

^ permalink raw reply	[flat|nested] 208+ messages in thread

* Re: [PATCH v7 00/14] Introduce new `git replay` command
  2023-11-23 19:32               ` Elijah Newren
@ 2023-11-24  0:28                 ` Junio C Hamano
  0 siblings, 0 replies; 208+ messages in thread
From: Junio C Hamano @ 2023-11-24  0:28 UTC (permalink / raw)
  To: Elijah Newren
  Cc: Johannes Schindelin, Christian Couder, git, Patrick Steinhardt,
	John Cai, Derrick Stolee, Phillip Wood, Calvin Wan, Toon Claes,
	Dragan Simic, Linus Arver

Elijah Newren <newren@gmail.com> writes:

> Hi Christian,
>
> On Thu, Nov 16, 2023 at 12:53 AM Johannes Schindelin
> <Johannes.Schindelin@gmx.de> wrote:
>> On Wed, 15 Nov 2023, Christian Couder wrote:
>>
>> > # Range-diff between v6 and v7
>> >
> [...]
>> Apart from the one little outstanding nit where I would love to see
>> `(EXPERIMENTAL!)` as the first word of the synopsis both in the manual
>> page and in the output of `git replay -h`, you have addressed all of my
>> concerns.
>>
>> Thank you!
>> Johannes
>
> Looks good to me too.  Thanks!

Thanks, both, for reviews.  I guess the only remaining issue now is
the "(EXPERIMENTAL)" label and we are ready to declare a victory?

Thanks.

^ permalink raw reply	[flat|nested] 208+ messages in thread

* [PATCH v8 00/14] Introduce new `git replay` command
  2023-11-15 14:33           ` [PATCH v7 " Christian Couder
                               ` (14 preceding siblings ...)
  2023-11-16  8:53             ` [PATCH v7 00/14] Introduce new `git replay` command Johannes Schindelin
@ 2023-11-24 11:10             ` Christian Couder
  2023-11-24 11:10               ` [PATCH v8 01/14] t6429: remove switching aspects of fast-rebase Christian Couder
                                 ` (14 more replies)
  15 siblings, 15 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-24 11:10 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

# Intro

`git replay` has initially been developed entirely by Elijah Newren
mostly last year (2022) at:

https://github.com/newren/git/commits/replay

I took over this year to polish and upstream it as GitLab is
interested in replacing libgit2, and for that purpose needs a command
to do server side (so without using a worktree) rebases, cherry-picks
and reverts.

I reduced the number of commits and features in this patch series,
compared to what Elijah already developed. Especially I stopped short
of replaying merge commits and replaying interactively. These and
other features might be upstreamed in the future after this patch
series has graduated.

The focus in this series is to make it a good plumbing command that
can already be used server side and that replaces the "fast-rebase"
test-tool command. So things to make it easier to use on the command
line, and more advanced features (like replaying merges) are left out.

It looks like GitHub has actually already been using version 3 of this
patch series in production with good results. See:

https://github.blog/2023-07-27-scaling-merge-ort-across-github/
https://lore.kernel.org/git/304f2a49-5e05-7655-9f87-2011606df5db@gmx.de/

# Content of this cover letter

The "Quick Overview" and "Reasons for diverging from cherry-pick &
rebase" sections just below are describing the purpose of the new
command in the big scheme of things. They are taken from Elijah's
design notes
(https://github.com/newren/git/blob/replay/replay-design-notes.txt)
and describe what we want this command to become and the reasons for
that, not what the command is after only this patch series. Also these
design notes were written at least one year ago, so parts of those 2
sections are not true anymore. I have added Phillip Wood's or Felipe
Contreras' notes (thanks to them) where that's the case, but some now
flawed parts may have missed.

After these two sections, starting with the "Important limitations"
section, you will find sections describing what is actually in this
patch series.

More interesting material is available in Elijah's design notes like
an "Intro via examples"
(https://github.com/newren/git/blob/replay/replay-design-notes.txt#L37-L132),
a discussion about "Preserving topology, replaying merges"
(https://github.com/newren/git/blob/replay/replay-design-notes.txt#L264-L341)
and a "Current status" section describing Elijah's work
(https://github.com/newren/git/blob/replay/replay-design-notes.txt#L344-L392)
before I started working on upstreaming it.

I have not included this material here though, as the documentation
added by this patch series for the `git replay` command already
includes an "EXAMPLES" section, and other sections of Elijah's design
notes might not be interesting for now. Also this cover letter is
already pretty long.  But reviewers can refer to the links above if
they think it can help.

# Quick Overview (from Elijah's design notes)

`git replay`, at a basic level, can perhaps be thought of as a
"default-to-dry-run rebase" -- meaning no updates to the working tree,
or to the index, or to any references.  However, it differs from
rebase in that it:

  * Works for branches that aren't checked out

  * Works in a bare repository

  * Can replay multiple branches simultaneously (with or without common
    history in the range being replayed)

  * Preserves relative topology by default (merges are replayed too in
    Elijah's original work, not in this series)

  * Focuses on performance

  * Has several altered defaults as a result of the above

I sometimes think of `git replay` as "fast-replay", a patch-based
analogue to the snapshot-based fast-export & fast-import tools.

# Reasons for diverging from cherry-pick & rebase (from Elijah's
  design notes)

There are multiple reasons to diverge from the defaults in cherry-pick and
rebase.

* Server side needs

  * Both cherry-pick and rebase, via the sequencer, are heavily tied
    to updating the working tree, index, some refs, and a lot of
    control files with every commit replayed, and invoke a mess of
    hooks[1] that might be hard to avoid for backward compatibility
    reasons (at least, that's been brought up a few times on the
    list).

  * cherry-pick and rebase both fork various subprocesses
    unnecessarily, but somewhat intrinsically in part to ensure the
    same hooks are called that old scripted implementations would have
    called.

    Note: since 356ee4659bb (sequencer: try to commit without forking
    'git commit', 2017-11-24) cherry-pick and rebase do not fork
    subprocesses other than hooks for the cases covered by this patch
    series (i.e. they do not fork "git commit" for simple picks).

  * "Dry run" behavior, where there are no updates to worktree, index,
    or even refs might be important.

  * Should not assume users only want to operate on HEAD (see next
    section)

* Decapitate HEAD-centric assumptions

  * cherry-pick forces commits to be played on top of HEAD;
    inflexible.

  * rebase assumes the range of commits to be replayed is
    upstream..HEAD by default, though it allows one to replay
    upstream..otherbranch -- but it still forcibly and needlessly
    checks out 'otherbranch' before starting to replay things.

    Note: since 767a9c417eb (rebase -i: stop checking out the tip of
    the branch to rebase, 2020-01-24) it's not true that rebase
    forcibly and needlessly checks out 'otherbranch'.

  * Assuming HEAD is involved severely limits replaying multiple
    (possibly divergent) branches.

    Note: since 89fc0b53fdb (rebase: update refs from 'update-ref'
    commands, 2022-07-19) the sequencer can update multiple
    branches. The issue with divergent branch is with command line
    arguments and the todo list generation rather than the
    capabilities of the sequencer.

  * Once you stop assuming HEAD has a certain meaning, there's not
    much reason to have two separate commands anymore (except for the
    funny extra not-necessarily-compatible options both have gained
    over time).

  * (Micro issue: Assuming HEAD is involved also makes it harder for
    new users to learn what rebase means and does; it makes command
    lines hard to parse.  Not sure I want to harp on this too much, as
    I have a suspicion I might be creating a tool for experts with
    complicated use cases, but it's a minor quibble.)

* Performance

  * jj is slaughtering us on rebase speed[2].  I would like us to become
    competitive.  (I dropped a few comments in the link at [2] about why
    git is currently so bad.)

  * From [3], there was a simple 4-patch series in linux.git that took
    53 seconds to rebase.  Switching to ort dropped it to 16 seconds.
    While that sounds great, only 11 *milliseconds* were needed to do
    the actual merges.  That means almost *all* the time (>99%) was
    overhead!  Big offenders:

    * --reapply-cherry-picks should be the default

    * can_fast_forward() should be ripped out, and perhaps other extraneous
      revision walks

      Note: d42c9ffa0f (rebase: factor out branch_base calculation,
      2022-10-17) might already deal with that (according to Felipe
      Contreras).

    * avoid updating working tree, index, refs, reflogs, and control
      structures except when needed (e.g. hitting a conflict, or operation
      finished)

  * Other performance ideas (mostly for future work, not in this
    series)

    * single-file control structures instead of directory of files
      (when doing interactive things which is in Elijah's original
      work, but not in this series)

    * avoid forking subprocesses unless explicitly requested (e.g.
      --exec, --strategy, --run-hooks).  For example, definitely do not
      invoke `git commit` or `git merge`.

    * Sanitize hooks:

      * dispense with all per-commit hooks for sure (pre-commit,
        post-commit, post-checkout).

      * pre-rebase also seems to assume exactly 1 ref is written, and
        invoking it repeatedly would be stupid.  Plus, it's specific
        to "rebase".  So...ignore?  (Stolee's --ref-update option for
        rebase probably broke the pre-rebase assumptions already...)

      * post-rewrite hook might make sense, but fast-import got
        exempted, and I think of replay like a patch-based analogue
        to the snapshot-based fast-import.

    * When not running server side, resolve conflicts in a sparse-cone
      sparse-index worktree to reduce number of files written to a
      working tree.  (See below as well.)

    * [High risk of possible premature optimization] Avoid large
      numbers of newly created loose objects, when replaying large
      numbers of commits.  Two possibilities: (1) Consider using
      tmp-objdir and pack objects from the tmp-objdir at end of
      exercise, (2) Lift code from git-fast-import to immediately
      stuff new objects into a pack?

* Multiple branches and non-checked out branches

  * The ability to operate on non-checked out branches also implies
    that we should generally be able to replay when in a dirty working
    tree (exception being when we expect to update HEAD and any of the
    dirty files is one that needs to be updated by the replay).

  * Also, if we are operating locally on a non-checked out branch and
    hit a conflict, we should have a way to resolve the conflict
    without messing with the user's work on their current
    branch. (This is not is this patch series though.)

    * Idea: new worktree with sparse cone + sparse index checkout,
      containing only files in the root directory, and whatever is
      necessary to get the conflicts

    * Companion to above idea: control structures should be written to
      $GIT_COMMON_DIR/replay-${worktree}, so users can have multiple
      replay sessions, and so we know which worktrees are associated
      with which replay operations.

  - [1] https://lore.kernel.org/git/pull.749.v3.git.git.1586044818132.gitgitgadget@gmail.com/
  - [2] https://github.com/martinvonz/jj/discussions/49
  - [3] https://lore.kernel.org/git/CABPp-BE48=97k_3tnNqXPjSEfA163F8hoE+HY0Zvz1SWB2B8EA@mail.gmail.com/

# Important limitations

* The code exits with code 1 if there are any conflict. No
  resumability. No nice output. No interactivity. No special exit code
  depending on the reason.

* When a commit becomes empty as it is replayed, it is still replayed
  as an empty commit, instead of being dropped.

* No replaying merges, nor root commits. Only regular commits.

* Signed commits are not properly handled. It's not clear what to do
  to such commits when replaying on the server side.

* Notes associated with replayed commits are not updated nor
  duplicated. (Thanks to Phillip Wood for noticing.)

# Commit overview

* 1/14 t6429: remove switching aspects of fast-rebase

    Preparatory commit to make it easier to later replace the
    fast-rebase test-tool by `git replay` without breaking existing
    tests.

* 2/14 replay: introduce new builtin

    This creates a minimal `git replay` command by moving the code
    from the `fast-rebase` test helper from `t/helper/` into
    `builtin/` and doing some renames and a few other needed changes.
    Since v7, there is only a synopsys change in the doc, and the
    corresponding usage message change, as suggested by Dscho.

* - 3/14 replay: start using parse_options API
  - 4/14 replay: die() instead of failing assert()
  - 5/14 replay: introduce pick_regular_commit()
  - 6/14 replay: change rev walking options
  - 7/14 replay: add an important FIXME comment about gpg signing
  - 8/14 replay: remove progress and info output
  - 9/14 replay: remove HEAD related sanity check

    These slowly change the command to make it behave more like
    regular commands and to start cleaning up its output.

* 10/14 replay: make it a minimal server side command

    After the cleaning up in previous commits, it's now time to
    radically change the way it works by stopping it to do ref
    updates, to update the index and worktree, to consider HEAD as
    special. Instead just make it output commands that should be
    passed to `git update-ref --stdin`.

* 11/14 replay: use standard revision ranges

    Start adding new interesting features and also documentation and
    tests, as the interface of the command is cristalizing into its
    final form.

* - 12/14 replay: add --advance or 'cherry-pick' mode
  - 13/14 replay: add --contained to rebase contained branches

    Add new options and features to the command.

* 14/14 replay: stop assuming replayed branches do not diverge

    This adds another interesting feature, as well as related
    documentation and tests.

# Notes about `fast-rebase`, tests and documentation

The `fast-rebase` test-tool helper was developed by Elijah to
experiment with a rebasing tool that would be developed from scratch
based on his merge-ort work, could be used to test that merge-ort
work, and would not have the speed and interface limitations of `git
rebase` or `git cherry-pick`.

This `fast-rebase` helper was used before this series in:

t6429-merge-sequence-rename-caching.sh

So when `git replay` is created from `fast-rebase` in patch 2/14, the
t6429 test script is also converted to use `git replay`. This ensures
that `git replay` doesn't break too badly during the first 10 patches
in this patch series.

Tests and documentation are introduced specifically for `git replay`
as soon as patch 2/14, but they are not much improved since around
11/14 as it doesn't make much sense to document and test behavior that
we know is going to change soon.

# Possibly controversial issues 

* bare or not bare: this series works towards a plumbing command with
  the end goal of it being usable and used first on bare repos,
  contrary to existing commands like `git rebase` and `git
  cherry-pick`. The tests check that the command works on both bare
  and non-bare repo though.

* exit status: a successful, non-conflicted replay exits with code
  0. When the replay has conflicts, the exit status is 1. If the
  replay is not able to complete (or start) due to some kind of error,
  the exit status is something other than 0 or 1. There are a few
  tests checking that. It has been suggested in an internal review
  that conflicts might want to get a more specific error code as an
  error code of 1 might be quite easy to return by accident. It
  doesn't seem to me from their docs (which might want to be improved,
  I didn't look at the code) that other commands like `git merge` and
  `git rebase` exit with a special error code in case of conflict.

* make worktree and index changes optional: commit 10/14 stops
  updating the index and worktree, but it might be better especially
  for cli users to make that optional. The issue is that this would
  make the command more complex while we are developing a number of
  important features so that the command can be used on bare repos. It
  seems that this should rather be done in an iterative improvement
  after the important features have landed.

* --advance and --contained: these two advanced options might not
  belong to this first series and could perhaps be added in a followup
  series in separate commits. On the other hand the code for
  --contained seems involved with the code of --advance and it's nice
  to see soon that git replay can indeed do cherry-picking and rebase
  many refs at once, and this way fullfil these parts of its promise.

* replaying diverging branches: 14/14 the last patch in the series,
  which allow replaying diverging branches, can be seen as a
  fundamental fix or alternatively as adding an interesting
  feature. So it's debatable if it should be in its own patch along
  with its own tests as in this series, or if it should be merged into
  a previous patch and which one.

* only 2 patches: this patch series can be seen from a high level
  point of view as 1) introducing the new `git replay` command, and 2)
  using `git replay` to replace, and get rid of, the fast-rebase
  test-tool command. The fact that not much of the original
  fast-rebase code and interface is left would agree with that point
  of view. On the other hand, fast-rebase can also be seen as a first
  iteration towards `git replay`. So it can also make sense to see how
  `git replay` evolved from it.

# Changes between v7 and v8

Thanks to Dscho, Linus Arver, Dragan Simic, Elijah, Junio, Derrick
Stolee, Phillip Wood, Calvin Wan and Toon Claes for their suggestions
on the previous versions! The only few changes compared to v7 are:

* The patch series was rebased onto master at 564d0252ca (Git 2.43,
  2023-11-20). This is to make it stand on a stable base.

* In patch 2/14 (replay: introduce new builtin), there is a synopsys
  change in the doc, and the corresponding usage message change as
  suggested by Dscho. This is just about replacing " # EXPERIMENTAL"
  at the end of both the synopsys and usage message with
  "(EXPERIMENTAL!) " at the beginning of them.

CI tests seem to pass according to:

https://github.com/chriscool/git/actions/runs/6979770154

(Sorry I am not waiting more than 20 minutes for the 3 last ones to
finish.)

# Range-diff between v7 and v8

(A single change was made in patch 2/14, but unfortunately as the
lines changed in that patch are also changed by other patches later,
it looks like there are more changes in subsequent patches.)


 1:  cddcd967b2 =  1:  18fd9b0d5d t6429: remove switching aspects of fast-rebase
 2:  c8476fb093 !  2:  fc6bdf4de4 replay: introduce new builtin
    @@ Documentation/git-replay.txt (new)
     +SYNOPSIS
     +--------
     +[verse]
    -+'git replay' --onto <newbase> <oldbase> <branch> # EXPERIMENTAL
    ++(EXPERIMENTAL!) 'git replay' --onto <newbase> <oldbase> <branch>
     +
     +DESCRIPTION
     +-----------
    @@ builtin/replay.c: int cmd__fast_rebase(int argc, const char **argv)
     -
        if (argc == 2 && !strcmp(argv[1], "-h")) {
     -          printf("Sorry, I am not a psychiatrist; I can not give you the help you need.  Oh, you meant usage...\n");
    -+          printf("git replay --onto <newbase> <oldbase> <branch> # EXPERIMENTAL\n");
    ++          printf("usage: (EXPERIMENTAL!) git replay --onto <newbase> <oldbase> <branch>\n");
                exit(129);
        }
      
 3:  43322abd1e !  3:  e96a66c352 replay: start using parse_options API
    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
        int ret = 0;
      
     -  if (argc == 2 && !strcmp(argv[1], "-h")) {
    --          printf("git replay --onto <newbase> <oldbase> <branch> # EXPERIMENTAL\n");
    +-          printf("usage: (EXPERIMENTAL!) git replay --onto <newbase> <oldbase> <branch>\n");
     -          exit(129);
     +  const char * const replay_usage[] = {
    -+          N_("git replay --onto <newbase> <oldbase> <branch> # EXPERIMENTAL"),
    ++          N_("(EXPERIMENTAL!) git replay --onto <newbase> <oldbase> <branch>"),
     +          NULL
     +  };
     +  struct option replay_options[] = {
 4:  6524c7f045 =  4:  f819d283d9 replay: die() instead of failing assert()
 5:  05d0efa3cb =  5:  68bbcf9492 replay: introduce pick_regular_commit()
 6:  c7a5aad3d6 =  6:  72221c647e replay: change rev walking options
 7:  01f35f924b =  7:  f54d8fce22 replay: add an important FIXME comment about gpg signing
 8:  1498b24bad =  8:  e50cc22522 replay: remove progress and info output
 9:  6786fc147b =  9:  0c5ea3d18e replay: remove HEAD related sanity check
10:  9a24dbb530 = 10:  9fc636fc3d replay: make it a minimal server side command
11:  ad6ca2fbef ! 11:  2096bcad79 replay: use standard revision ranges
    @@ Documentation/git-replay.txt: git-replay - EXPERIMENTAL: Replay commits on a new
      SYNOPSIS
      --------
      [verse]
    --'git replay' --onto <newbase> <oldbase> <branch> # EXPERIMENTAL
    -+'git replay' --onto <newbase> <revision-range>... # EXPERIMENTAL
    +-(EXPERIMENTAL!) 'git replay' --onto <newbase> <oldbase> <branch>
    ++(EXPERIMENTAL!) 'git replay' --onto <newbase> <revision-range>...
      
      DESCRIPTION
      -----------
    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
        int ret = 0;
      
        const char * const replay_usage[] = {
    --          N_("git replay --onto <newbase> <oldbase> <branch> # EXPERIMENTAL"),
    -+          N_("git replay --onto <newbase> <revision-range>... # EXPERIMENTAL"),
    +-          N_("(EXPERIMENTAL!) git replay --onto <newbase> <oldbase> <branch>"),
    ++          N_("(EXPERIMENTAL!) git replay --onto <newbase> <revision-range>..."),
                NULL
        };
        struct option replay_options[] = {
12:  081864ed5f ! 12:  d5414806ef replay: add --advance or 'cherry-pick' mode
    @@ Documentation/git-replay.txt: git-replay - EXPERIMENTAL: Replay commits on a new
      SYNOPSIS
      --------
      [verse]
    --'git replay' --onto <newbase> <revision-range>... # EXPERIMENTAL
    -+'git replay' (--onto <newbase> | --advance <branch>) <revision-range>... # EXPERIMENTAL
    +-(EXPERIMENTAL!) 'git replay' --onto <newbase> <revision-range>...
    ++(EXPERIMENTAL!) 'git replay' (--onto <newbase> | --advance <branch>) <revision-range>...
      
      DESCRIPTION
      -----------
    @@ builtin/replay.c: static struct commit *pick_regular_commit(struct commit *pickm
        int ret = 0;
      
        const char * const replay_usage[] = {
    --          N_("git replay --onto <newbase> <revision-range>... # EXPERIMENTAL"),
    -+          N_("git replay (--onto <newbase> | --advance <branch>) <revision-range>... # EXPERIMENTAL"),
    +-          N_("(EXPERIMENTAL!) git replay --onto <newbase> <revision-range>..."),
    ++          N_("(EXPERIMENTAL!) git replay (--onto <newbase> | --advance <branch>) <revision-range>..."),
                NULL
        };
        struct option replay_options[] = {
13:  19c4016c7c ! 13:  2a3e521c13 replay: add --contained to rebase contained branches
    @@ Documentation/git-replay.txt: git-replay - EXPERIMENTAL: Replay commits on a new
      SYNOPSIS
      --------
      [verse]
    --'git replay' (--onto <newbase> | --advance <branch>) <revision-range>... # EXPERIMENTAL
    -+'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>... # EXPERIMENTAL
    +-(EXPERIMENTAL!) 'git replay' (--onto <newbase> | --advance <branch>) <revision-range>...
    ++(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...
      
      DESCRIPTION
      -----------
    @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
        int ret = 0;
      
        const char * const replay_usage[] = {
    --          N_("git replay (--onto <newbase> | --advance <branch>) <revision-range>... # EXPERIMENTAL"),
    -+          N_("git replay ([--contained] --onto <newbase> | --advance <branch>) "
    -+             "<revision-range>... # EXPERIMENTAL"),
    +-          N_("(EXPERIMENTAL!) git replay (--onto <newbase> | --advance <branch>) <revision-range>..."),
    ++          N_("(EXPERIMENTAL!) git replay "
    ++             "([--contained] --onto <newbase> | --advance <branch>) "
    ++             "<revision-range>..."),
                NULL
        };
        struct option replay_options[] = {
14:  29556bcc86 = 14:  93e034faee replay: stop assuming replayed branches do not diverge


Elijah Newren (14):
  t6429: remove switching aspects of fast-rebase
  replay: introduce new builtin
  replay: start using parse_options API
  replay: die() instead of failing assert()
  replay: introduce pick_regular_commit()
  replay: change rev walking options
  replay: add an important FIXME comment about gpg signing
  replay: remove progress and info output
  replay: remove HEAD related sanity check
  replay: make it a minimal server side command
  replay: use standard revision ranges
  replay: add --advance or 'cherry-pick' mode
  replay: add --contained to rebase contained branches
  replay: stop assuming replayed branches do not diverge

 .gitignore                               |   1 +
 Documentation/git-replay.txt             | 127 +++++++
 Makefile                                 |   2 +-
 builtin.h                                |   1 +
 builtin/replay.c                         | 446 +++++++++++++++++++++++
 command-list.txt                         |   1 +
 git.c                                    |   1 +
 t/helper/test-fast-rebase.c              | 241 ------------
 t/helper/test-tool.c                     |   1 -
 t/helper/test-tool.h                     |   1 -
 t/t3650-replay-basics.sh                 | 198 ++++++++++
 t/t6429-merge-sequence-rename-caching.sh |  45 ++-
 12 files changed, 801 insertions(+), 264 deletions(-)
 create mode 100644 Documentation/git-replay.txt
 create mode 100644 builtin/replay.c
 delete mode 100644 t/helper/test-fast-rebase.c
 create mode 100755 t/t3650-replay-basics.sh


base-commit: 564d0252ca632e0264ed670534a51d18a689ef5d
-- 
2.43.0.14.g93e034faee


^ permalink raw reply	[flat|nested] 208+ messages in thread

* [PATCH v8 01/14] t6429: remove switching aspects of fast-rebase
  2023-11-24 11:10             ` [PATCH v8 " Christian Couder
@ 2023-11-24 11:10               ` Christian Couder
  2023-11-24 11:10               ` [PATCH v8 02/14] replay: introduce new builtin Christian Couder
                                 ` (13 subsequent siblings)
  14 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-24 11:10 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

At the time t6429 was written, merge-ort was still under development,
did not have quite as many tests, and certainly was not widely deployed.
Since t6429 was exercising some codepaths just a little differently, we
thought having them also test the "merge_switch_to_result()" bits of
merge-ort was useful even though they weren't intrinsic to the real
point of these tests.

However, the value provided by doing extra testing of the
"merge_switch_to_result()" bits has decreased a bit over time, and it's
actively making it harder to refactor `test-tool fast-rebase` into `git
replay`, which we are going to do in following commits.  Dispense with
these bits.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 t/helper/test-fast-rebase.c              | 9 +--------
 t/t6429-merge-sequence-rename-caching.sh | 9 +++++++--
 2 files changed, 8 insertions(+), 10 deletions(-)

diff --git a/t/helper/test-fast-rebase.c b/t/helper/test-fast-rebase.c
index cac20a72b3..2bfab66b1b 100644
--- a/t/helper/test-fast-rebase.c
+++ b/t/helper/test-fast-rebase.c
@@ -194,7 +194,7 @@ int cmd__fast_rebase(int argc, const char **argv)
 		last_commit = create_commit(result.tree, commit, last_commit);
 	}
 
-	merge_switch_to_result(&merge_opt, head_tree, &result, 1, !result.clean);
+	merge_finalize(&merge_opt, &result);
 
 	if (result.clean < 0)
 		exit(128);
@@ -213,9 +213,6 @@ int cmd__fast_rebase(int argc, const char **argv)
 		}
 		if (create_symref("HEAD", branch_name.buf, reflog_msg.buf) < 0)
 			die(_("unable to update HEAD"));
-
-		prime_cache_tree(the_repository, the_repository->index,
-				 result.tree);
 	} else {
 		fprintf(stderr, "\nAborting: Hit a conflict.\n");
 		strbuf_addf(&reflog_msg, "rebase progress up to %s",
@@ -228,10 +225,6 @@ int cmd__fast_rebase(int argc, const char **argv)
 			die("Failed to update %s", argv[4]);
 		}
 	}
-	if (write_locked_index(&the_index, &lock,
-			       COMMIT_LOCK | SKIP_IF_UNCHANGED))
-		die(_("unable to write %s"), get_index_file());
-
 	ret = (result.clean == 0);
 cleanup:
 	strbuf_release(&reflog_msg);
diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh
index d02fa16614..75d3fd2dba 100755
--- a/t/t6429-merge-sequence-rename-caching.sh
+++ b/t/t6429-merge-sequence-rename-caching.sh
@@ -72,6 +72,7 @@ test_expect_success 'caching renames does not preclude finding new ones' '
 		git switch upstream &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream~1..topic
 
 		git ls-files >tracked-files &&
@@ -200,6 +201,7 @@ test_expect_success 'rename same file identically, then reintroduce it' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream~1..topic &&
 
 		git ls-files >tracked &&
@@ -277,6 +279,7 @@ test_expect_success 'rename same file identically, then add file to old dir' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream~1..topic &&
 
 		git ls-files >tracked &&
@@ -356,8 +359,6 @@ test_expect_success 'cached dir rename does not prevent noticing later conflict'
 		test_must_fail test-tool fast-rebase --onto HEAD upstream~1 topic >output &&
 		#git cherry-pick upstream..topic &&
 
-		grep CONFLICT..rename/rename output &&
-
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls
 	)
@@ -456,6 +457,7 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
@@ -522,6 +524,7 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
@@ -624,6 +627,7 @@ test_expect_success 'caching renames only on upstream side, part 1' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
@@ -682,6 +686,7 @@ test_expect_success 'caching renames only on upstream side, part 2' '
 		export GIT_TRACE2_PERF &&
 
 		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git reset --hard topic &&
 		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
-- 
2.43.0.14.g93e034faee


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v8 02/14] replay: introduce new builtin
  2023-11-24 11:10             ` [PATCH v8 " Christian Couder
  2023-11-24 11:10               ` [PATCH v8 01/14] t6429: remove switching aspects of fast-rebase Christian Couder
@ 2023-11-24 11:10               ` Christian Couder
  2023-11-24 11:10               ` [PATCH v8 03/14] replay: start using parse_options API Christian Couder
                                 ` (12 subsequent siblings)
  14 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-24 11:10 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

For now, this is just a rename from `t/helper/test-fast-rebase.c` into
`builtin/replay.c` with minimal changes to make it build appropriately.

Let's add a stub documentation and a stub test script though.

Subsequent commits will flesh out the capabilities of the new command
and make it a more standard regular builtin.

Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 .gitignore                                    |  1 +
 Documentation/git-replay.txt                  | 39 ++++++++++++
 Makefile                                      |  2 +-
 builtin.h                                     |  1 +
 .../test-fast-rebase.c => builtin/replay.c    | 29 +++------
 command-list.txt                              |  1 +
 git.c                                         |  1 +
 t/helper/test-tool.c                          |  1 -
 t/helper/test-tool.h                          |  1 -
 t/t3650-replay-basics.sh                      | 60 +++++++++++++++++++
 t/t6429-merge-sequence-rename-caching.sh      | 27 +++------
 11 files changed, 122 insertions(+), 41 deletions(-)
 create mode 100644 Documentation/git-replay.txt
 rename t/helper/test-fast-rebase.c => builtin/replay.c (87%)
 create mode 100755 t/t3650-replay-basics.sh

diff --git a/.gitignore b/.gitignore
index 5e56e471b3..612c0f6a0f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -135,6 +135,7 @@
 /git-remote-ext
 /git-repack
 /git-replace
+/git-replay
 /git-request-pull
 /git-rerere
 /git-reset
diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
new file mode 100644
index 0000000000..2ca7ca5fd8
--- /dev/null
+++ b/Documentation/git-replay.txt
@@ -0,0 +1,39 @@
+git-replay(1)
+=============
+
+NAME
+----
+git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos too
+
+
+SYNOPSIS
+--------
+[verse]
+(EXPERIMENTAL!) 'git replay' --onto <newbase> <oldbase> <branch>
+
+DESCRIPTION
+-----------
+
+Takes a range of commits, specified by <oldbase> and <branch>, and
+replays them onto a new location (see `--onto` option below).
+
+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
+
+OPTIONS
+-------
+
+--onto <newbase>::
+	Starting point at which to create the new commits.  May be any
+	valid commit, and not just an existing branch name.
+
+EXIT STATUS
+-----------
+
+For a successful, non-conflicted replay, the exit status is 0.  When
+the replay has conflicts, the exit status is 1.  If the replay is not
+able to complete (or start) due to some kind of error, the exit status
+is something other than 0 or 1.
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Makefile b/Makefile
index 03adcb5a48..3834bc1544 100644
--- a/Makefile
+++ b/Makefile
@@ -799,7 +799,6 @@ TEST_BUILTINS_OBJS += test-dump-split-index.o
 TEST_BUILTINS_OBJS += test-dump-untracked-cache.o
 TEST_BUILTINS_OBJS += test-env-helper.o
 TEST_BUILTINS_OBJS += test-example-decorate.o
-TEST_BUILTINS_OBJS += test-fast-rebase.o
 TEST_BUILTINS_OBJS += test-find-pack.o
 TEST_BUILTINS_OBJS += test-fsmonitor-client.o
 TEST_BUILTINS_OBJS += test-genrandom.o
@@ -1290,6 +1289,7 @@ BUILTIN_OBJS += builtin/remote-fd.o
 BUILTIN_OBJS += builtin/remote.o
 BUILTIN_OBJS += builtin/repack.o
 BUILTIN_OBJS += builtin/replace.o
+BUILTIN_OBJS += builtin/replay.o
 BUILTIN_OBJS += builtin/rerere.o
 BUILTIN_OBJS += builtin/reset.o
 BUILTIN_OBJS += builtin/rev-list.o
diff --git a/builtin.h b/builtin.h
index d560baa661..28280636da 100644
--- a/builtin.h
+++ b/builtin.h
@@ -211,6 +211,7 @@ int cmd_remote(int argc, const char **argv, const char *prefix);
 int cmd_remote_ext(int argc, const char **argv, const char *prefix);
 int cmd_remote_fd(int argc, const char **argv, const char *prefix);
 int cmd_repack(int argc, const char **argv, const char *prefix);
+int cmd_replay(int argc, const char **argv, const char *prefix);
 int cmd_rerere(int argc, const char **argv, const char *prefix);
 int cmd_reset(int argc, const char **argv, const char *prefix);
 int cmd_restore(int argc, const char **argv, const char *prefix);
diff --git a/t/helper/test-fast-rebase.c b/builtin/replay.c
similarity index 87%
rename from t/helper/test-fast-rebase.c
rename to builtin/replay.c
index 2bfab66b1b..1998134683 100644
--- a/t/helper/test-fast-rebase.c
+++ b/builtin/replay.c
@@ -1,17 +1,11 @@
 /*
- * "git fast-rebase" builtin command
- *
- * FAST: Forking Any Subprocesses (is) Taboo
- *
- * This is meant SOLELY as a demo of what is possible.  sequencer.c and
- * rebase.c should be refactored to use the ideas here, rather than attempting
- * to extend this file to replace those (unless Phillip or Dscho say that
- * refactoring is too hard and we need a clean slate, but I'm guessing that
- * refactoring is the better route).
+ * "git replay" builtin command
  */
 
 #define USE_THE_INDEX_VARIABLE
-#include "test-tool.h"
+#include "git-compat-util.h"
+
+#include "builtin.h"
 #include "cache-tree.h"
 #include "commit.h"
 #include "environment.h"
@@ -27,7 +21,8 @@
 #include "sequencer.h"
 #include "setup.h"
 #include "strvec.h"
-#include "tree.h"
+#include <oidset.h>
+#include <tree.h>
 
 static const char *short_commit_name(struct commit *commit)
 {
@@ -94,7 +89,7 @@ static struct commit *create_commit(struct tree *tree,
 	return (struct commit *)obj;
 }
 
-int cmd__fast_rebase(int argc, const char **argv)
+int cmd_replay(int argc, const char **argv, const char *prefix)
 {
 	struct commit *onto;
 	struct commit *last_commit = NULL, *last_picked_commit = NULL;
@@ -110,14 +105,8 @@ int cmd__fast_rebase(int argc, const char **argv)
 	struct strbuf branch_name = STRBUF_INIT;
 	int ret = 0;
 
-	/*
-	 * test-tool stuff doesn't set up the git directory by default; need to
-	 * do that manually.
-	 */
-	setup_git_directory();
-
 	if (argc == 2 && !strcmp(argv[1], "-h")) {
-		printf("Sorry, I am not a psychiatrist; I can not give you the help you need.  Oh, you meant usage...\n");
+		printf("usage: (EXPERIMENTAL!) git replay --onto <newbase> <oldbase> <branch>\n");
 		exit(129);
 	}
 
@@ -136,7 +125,7 @@ int cmd__fast_rebase(int argc, const char **argv)
 	if (repo_read_index(the_repository) < 0)
 		BUG("Could not read index");
 
-	repo_init_revisions(the_repository, &revs, NULL);
+	repo_init_revisions(the_repository, &revs, prefix);
 	revs.verbose_header = 1;
 	revs.max_parents = 1;
 	revs.cherry_mark = 1;
diff --git a/command-list.txt b/command-list.txt
index 54b2a50f5f..c4cd0f352b 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -160,6 +160,7 @@ git-reflog                              ancillarymanipulators           complete
 git-remote                              ancillarymanipulators           complete
 git-repack                              ancillarymanipulators           complete
 git-replace                             ancillarymanipulators           complete
+git-replay                              plumbingmanipulators
 git-request-pull                        foreignscminterface             complete
 git-rerere                              ancillaryinterrogators
 git-reset                               mainporcelain           history
diff --git a/git.c b/git.c
index c67e44dd82..7068a184b0 100644
--- a/git.c
+++ b/git.c
@@ -594,6 +594,7 @@ static struct cmd_struct commands[] = {
 	{ "remote-fd", cmd_remote_fd, NO_PARSEOPT },
 	{ "repack", cmd_repack, RUN_SETUP },
 	{ "replace", cmd_replace, RUN_SETUP },
+	{ "replay", cmd_replay, RUN_SETUP },
 	{ "rerere", cmd_rerere, RUN_SETUP },
 	{ "reset", cmd_reset, RUN_SETUP },
 	{ "restore", cmd_restore, RUN_SETUP | NEED_WORK_TREE },
diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
index 876cd2dc31..37ba996539 100644
--- a/t/helper/test-tool.c
+++ b/t/helper/test-tool.c
@@ -30,7 +30,6 @@ static struct test_cmd cmds[] = {
 	{ "dump-untracked-cache", cmd__dump_untracked_cache },
 	{ "env-helper", cmd__env_helper },
 	{ "example-decorate", cmd__example_decorate },
-	{ "fast-rebase", cmd__fast_rebase },
 	{ "find-pack", cmd__find_pack },
 	{ "fsmonitor-client", cmd__fsmonitor_client },
 	{ "genrandom", cmd__genrandom },
diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h
index 70dd4eba11..8a1a7c63da 100644
--- a/t/helper/test-tool.h
+++ b/t/helper/test-tool.h
@@ -24,7 +24,6 @@ int cmd__dump_untracked_cache(int argc, const char **argv);
 int cmd__dump_reftable(int argc, const char **argv);
 int cmd__env_helper(int argc, const char **argv);
 int cmd__example_decorate(int argc, const char **argv);
-int cmd__fast_rebase(int argc, const char **argv);
 int cmd__find_pack(int argc, const char **argv);
 int cmd__fsmonitor_client(int argc, const char **argv);
 int cmd__genrandom(int argc, const char **argv);
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
new file mode 100755
index 0000000000..36c1b5082a
--- /dev/null
+++ b/t/t3650-replay-basics.sh
@@ -0,0 +1,60 @@
+#!/bin/sh
+
+test_description='basic git replay tests'
+
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
+. ./test-lib.sh
+
+GIT_AUTHOR_NAME=author@name
+GIT_AUTHOR_EMAIL=bogus@email@address
+export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL
+
+test_expect_success 'setup' '
+	test_commit A &&
+	test_commit B &&
+
+	git switch -c topic1 &&
+	test_commit C &&
+	git switch -c topic2 &&
+	test_commit D &&
+	test_commit E &&
+	git switch topic1 &&
+	test_commit F &&
+	git switch -c topic3 &&
+	test_commit G &&
+	test_commit H &&
+	git switch -c topic4 main &&
+	test_commit I &&
+	test_commit J &&
+
+	git switch -c next main &&
+	test_commit K &&
+	git merge -m "Merge topic1" topic1 &&
+	git merge -m "Merge topic2" topic2 &&
+	git merge -m "Merge topic3" topic3 &&
+	>evil &&
+	git add evil &&
+	git commit --amend &&
+	git merge -m "Merge topic4" topic4 &&
+
+	git switch main &&
+	test_commit L &&
+	test_commit M &&
+
+	git switch -c conflict B &&
+	test_commit C.conflict C.t conflict
+'
+
+test_expect_success 'using replay to rebase two branches, one on top of other' '
+	git switch main &&
+
+	git replay --onto main topic1 topic2 >result &&
+
+	git log --format=%s $(cut -f 3 -d " " result) >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual
+'
+
+test_done
diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh
index 75d3fd2dba..7670b72008 100755
--- a/t/t6429-merge-sequence-rename-caching.sh
+++ b/t/t6429-merge-sequence-rename-caching.sh
@@ -71,9 +71,8 @@ test_expect_success 'caching renames does not preclude finding new ones' '
 
 		git switch upstream &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream~1..topic
 
 		git ls-files >tracked-files &&
 		test_line_count = 2 tracked-files &&
@@ -141,8 +140,7 @@ test_expect_success 'cherry-pick both a commit and its immediate revert' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
-		#git cherry-pick upstream~1..topic &&
+		git replay --onto HEAD upstream~1 topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 1 calls
@@ -200,9 +198,8 @@ test_expect_success 'rename same file identically, then reintroduce it' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream~1..topic &&
 
 		git ls-files >tracked &&
 		test_line_count = 2 tracked &&
@@ -278,9 +275,8 @@ test_expect_success 'rename same file identically, then add file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream~1..topic &&
 
 		git ls-files >tracked &&
 		test_line_count = 4 tracked &&
@@ -356,8 +352,7 @@ test_expect_success 'cached dir rename does not prevent noticing later conflict'
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test_must_fail test-tool fast-rebase --onto HEAD upstream~1 topic >output &&
-		#git cherry-pick upstream..topic &&
+		test_must_fail git replay --onto HEAD upstream~1 topic >output &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls
@@ -456,9 +451,8 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls &&
@@ -523,9 +517,8 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 3 calls &&
@@ -626,9 +619,8 @@ test_expect_success 'caching renames only on upstream side, part 1' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 1 calls &&
@@ -685,9 +677,8 @@ test_expect_success 'caching renames only on upstream side, part 2' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test-tool fast-rebase --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic &&
 		git reset --hard topic &&
-		#git cherry-pick upstream..topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls &&
-- 
2.43.0.14.g93e034faee


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v8 03/14] replay: start using parse_options API
  2023-11-24 11:10             ` [PATCH v8 " Christian Couder
  2023-11-24 11:10               ` [PATCH v8 01/14] t6429: remove switching aspects of fast-rebase Christian Couder
  2023-11-24 11:10               ` [PATCH v8 02/14] replay: introduce new builtin Christian Couder
@ 2023-11-24 11:10               ` Christian Couder
  2023-11-24 11:10               ` [PATCH v8 04/14] replay: die() instead of failing assert() Christian Couder
                                 ` (11 subsequent siblings)
  14 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-24 11:10 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

Instead of manually parsing arguments, let's start using the parse_options
API. This way this new builtin will look more standard, and in some
upcoming commits will more easily be able to handle more command line
options.

Note that we plan to later use standard revision ranges instead of
hardcoded "<oldbase> <branch>" arguments. When we will use standard
revision ranges, it will be easier to check if there are no spurious
arguments if we keep ARGV[0], so let's call parse_options() with
PARSE_OPT_KEEP_ARGV0 even if we don't need ARGV[0] right now to avoid
some useless code churn.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 45 ++++++++++++++++++++++++++++++++-------------
 1 file changed, 32 insertions(+), 13 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 1998134683..7998f6ed04 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -15,7 +15,7 @@
 #include "lockfile.h"
 #include "merge-ort.h"
 #include "object-name.h"
-#include "read-cache-ll.h"
+#include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
 #include "sequencer.h"
@@ -92,6 +92,7 @@ static struct commit *create_commit(struct tree *tree,
 int cmd_replay(int argc, const char **argv, const char *prefix)
 {
 	struct commit *onto;
+	const char *onto_name = NULL;
 	struct commit *last_commit = NULL, *last_picked_commit = NULL;
 	struct object_id head;
 	struct lock_file lock = LOCK_INIT;
@@ -105,16 +106,32 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct strbuf branch_name = STRBUF_INIT;
 	int ret = 0;
 
-	if (argc == 2 && !strcmp(argv[1], "-h")) {
-		printf("usage: (EXPERIMENTAL!) git replay --onto <newbase> <oldbase> <branch>\n");
-		exit(129);
+	const char * const replay_usage[] = {
+		N_("(EXPERIMENTAL!) git replay --onto <newbase> <oldbase> <branch>"),
+		NULL
+	};
+	struct option replay_options[] = {
+		OPT_STRING(0, "onto", &onto_name,
+			   N_("revision"),
+			   N_("replay onto given commit")),
+		OPT_END()
+	};
+
+	argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
+			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
+
+	if (!onto_name) {
+		error(_("option --onto is mandatory"));
+		usage_with_options(replay_usage, replay_options);
 	}
 
-	if (argc != 5 || strcmp(argv[1], "--onto"))
-		die("usage: read the code, figure out how to use it, then do so");
+	if (argc != 3) {
+		error(_("bad number of arguments"));
+		usage_with_options(replay_usage, replay_options);
+	}
 
-	onto = peel_committish(argv[2]);
-	strbuf_addf(&branch_name, "refs/heads/%s", argv[4]);
+	onto = peel_committish(onto_name);
+	strbuf_addf(&branch_name, "refs/heads/%s", argv[2]);
 
 	/* Sanity check */
 	if (repo_get_oid(the_repository, "HEAD", &head))
@@ -126,6 +143,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		BUG("Could not read index");
 
 	repo_init_revisions(the_repository, &revs, prefix);
+
 	revs.verbose_header = 1;
 	revs.max_parents = 1;
 	revs.cherry_mark = 1;
@@ -134,7 +152,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	revs.right_only = 1;
 	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
 	revs.topo_order = 1;
-	strvec_pushl(&rev_walk_args, "", argv[4], "--not", argv[3], NULL);
+
+	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
 
 	if (setup_revisions(rev_walk_args.nr, rev_walk_args.v, &revs, NULL) > 1) {
 		ret = error(_("unhandled options"));
@@ -197,8 +216,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			       &last_commit->object.oid,
 			       &last_picked_commit->object.oid,
 			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
-			error(_("could not update %s"), argv[4]);
-			die("Failed to update %s", argv[4]);
+			error(_("could not update %s"), argv[2]);
+			die("Failed to update %s", argv[2]);
 		}
 		if (create_symref("HEAD", branch_name.buf, reflog_msg.buf) < 0)
 			die(_("unable to update HEAD"));
@@ -210,8 +229,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			       &last_commit->object.oid,
 			       &head,
 			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
-			error(_("could not update %s"), argv[4]);
-			die("Failed to update %s", argv[4]);
+			error(_("could not update %s"), argv[2]);
+			die("Failed to update %s", argv[2]);
 		}
 	}
 	ret = (result.clean == 0);
-- 
2.43.0.14.g93e034faee


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v8 04/14] replay: die() instead of failing assert()
  2023-11-24 11:10             ` [PATCH v8 " Christian Couder
                                 ` (2 preceding siblings ...)
  2023-11-24 11:10               ` [PATCH v8 03/14] replay: start using parse_options API Christian Couder
@ 2023-11-24 11:10               ` Christian Couder
  2023-11-24 11:10               ` [PATCH v8 05/14] replay: introduce pick_regular_commit() Christian Couder
                                 ` (10 subsequent siblings)
  14 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-24 11:10 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

It's not a good idea for regular Git commands to use an assert() to
check for things that could happen but are not supported.

Let's die() with an explanation of the issue instead.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 7998f6ed04..f48c5ed255 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -179,7 +179,12 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 
 		fprintf(stderr, "Rebasing %s...\r",
 			oid_to_hex(&commit->object.oid));
-		assert(commit->parents && !commit->parents->next);
+
+		if (!commit->parents)
+			die(_("replaying down to root commit is not supported yet!"));
+		if (commit->parents->next)
+			die(_("replaying merge commits is not supported yet!"));
+
 		base = commit->parents->item;
 
 		next_tree = repo_get_commit_tree(the_repository, commit);
-- 
2.43.0.14.g93e034faee


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v8 05/14] replay: introduce pick_regular_commit()
  2023-11-24 11:10             ` [PATCH v8 " Christian Couder
                                 ` (3 preceding siblings ...)
  2023-11-24 11:10               ` [PATCH v8 04/14] replay: die() instead of failing assert() Christian Couder
@ 2023-11-24 11:10               ` Christian Couder
  2023-11-24 11:10               ` [PATCH v8 06/14] replay: change rev walking options Christian Couder
                                 ` (9 subsequent siblings)
  14 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-24 11:10 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

Let's refactor the code to handle a regular commit (a commit that is
neither a root commit nor a merge commit) into a single function instead
of keeping it inside cmd_replay().

This is good for separation of concerns, and this will help further work
in the future to replay merge commits.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 54 ++++++++++++++++++++++++++++++------------------
 1 file changed, 34 insertions(+), 20 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index f48c5ed255..d039467cd4 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -89,6 +89,35 @@ static struct commit *create_commit(struct tree *tree,
 	return (struct commit *)obj;
 }
 
+static struct commit *pick_regular_commit(struct commit *pickme,
+					  struct commit *last_commit,
+					  struct merge_options *merge_opt,
+					  struct merge_result *result)
+{
+	struct commit *base;
+	struct tree *pickme_tree, *base_tree;
+
+	base = pickme->parents->item;
+
+	pickme_tree = repo_get_commit_tree(the_repository, pickme);
+	base_tree = repo_get_commit_tree(the_repository, base);
+
+	merge_opt->branch2 = short_commit_name(pickme);
+	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
+
+	merge_incore_nonrecursive(merge_opt,
+				  base_tree,
+				  result->tree,
+				  pickme_tree,
+				  result);
+
+	free((char*)merge_opt->ancestor);
+	merge_opt->ancestor = NULL;
+	if (!result->clean)
+		return NULL;
+	return create_commit(result->tree, pickme, last_commit);
+}
+
 int cmd_replay(int argc, const char **argv, const char *prefix)
 {
 	struct commit *onto;
@@ -100,7 +129,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct rev_info revs;
 	struct commit *commit;
 	struct merge_options merge_opt;
-	struct tree *next_tree, *base_tree, *head_tree;
+	struct tree *head_tree;
 	struct merge_result result;
 	struct strbuf reflog_msg = STRBUF_INIT;
 	struct strbuf branch_name = STRBUF_INIT;
@@ -175,7 +204,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	result.tree = head_tree;
 	last_commit = onto;
 	while ((commit = get_revision(&revs))) {
-		struct commit *base;
+		struct commit *pick;
 
 		fprintf(stderr, "Rebasing %s...\r",
 			oid_to_hex(&commit->object.oid));
@@ -185,26 +214,11 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		if (commit->parents->next)
 			die(_("replaying merge commits is not supported yet!"));
 
-		base = commit->parents->item;
-
-		next_tree = repo_get_commit_tree(the_repository, commit);
-		base_tree = repo_get_commit_tree(the_repository, base);
-
-		merge_opt.branch2 = short_commit_name(commit);
-		merge_opt.ancestor = xstrfmt("parent of %s", merge_opt.branch2);
-
-		merge_incore_nonrecursive(&merge_opt,
-					  base_tree,
-					  result.tree,
-					  next_tree,
-					  &result);
-
-		free((char*)merge_opt.ancestor);
-		merge_opt.ancestor = NULL;
-		if (!result.clean)
+		pick = pick_regular_commit(commit, last_commit, &merge_opt, &result);
+		if (!pick)
 			break;
+		last_commit = pick;
 		last_picked_commit = commit;
-		last_commit = create_commit(result.tree, commit, last_commit);
 	}
 
 	merge_finalize(&merge_opt, &result);
-- 
2.43.0.14.g93e034faee


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v8 06/14] replay: change rev walking options
  2023-11-24 11:10             ` [PATCH v8 " Christian Couder
                                 ` (4 preceding siblings ...)
  2023-11-24 11:10               ` [PATCH v8 05/14] replay: introduce pick_regular_commit() Christian Couder
@ 2023-11-24 11:10               ` Christian Couder
  2023-11-24 11:10               ` [PATCH v8 07/14] replay: add an important FIXME comment about gpg signing Christian Couder
                                 ` (8 subsequent siblings)
  14 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-24 11:10 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

Let's force the rev walking options we need after calling
setup_revisions() instead of before.

This might override some user supplied rev walking command line options
though. So let's detect that and warn users by:

  a) setting the desired values, before setup_revisions(),
  b) checking after setup_revisions() whether these values differ from
     the desired values,
  c) if so throwing a warning and setting the desired values again.

We want the command to work from older commits to newer ones by default.
Also we don't want history simplification, as we want to deal with all
the commits in the affected range.

Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 48 +++++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 41 insertions(+), 7 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index d039467cd4..2f664218be 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -173,22 +173,56 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 
 	repo_init_revisions(the_repository, &revs, prefix);
 
-	revs.verbose_header = 1;
-	revs.max_parents = 1;
-	revs.cherry_mark = 1;
-	revs.limited = 1;
+	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
+
+	/*
+	 * Set desired values for rev walking options here. If they
+	 * are changed by some user specified option in setup_revisions()
+	 * below, we will detect that below and then warn.
+	 *
+	 * TODO: In the future we might want to either die(), or allow
+	 * some options changing these values if we think they could
+	 * be useful.
+	 */
 	revs.reverse = 1;
-	revs.right_only = 1;
 	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
 	revs.topo_order = 1;
-
-	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
+	revs.simplify_history = 0;
 
 	if (setup_revisions(rev_walk_args.nr, rev_walk_args.v, &revs, NULL) > 1) {
 		ret = error(_("unhandled options"));
 		goto cleanup;
 	}
 
+	/*
+	 * Detect and warn if we override some user specified rev
+	 * walking options.
+	 */
+	if (revs.reverse != 1) {
+		warning(_("some rev walking options will be overridden as "
+			  "'%s' bit in 'struct rev_info' will be forced"),
+			"reverse");
+		revs.reverse = 1;
+	}
+	if (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) {
+		warning(_("some rev walking options will be overridden as "
+			  "'%s' bit in 'struct rev_info' will be forced"),
+			"sort_order");
+		revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
+	}
+	if (revs.topo_order != 1) {
+		warning(_("some rev walking options will be overridden as "
+			  "'%s' bit in 'struct rev_info' will be forced"),
+			"topo_order");
+		revs.topo_order = 1;
+	}
+	if (revs.simplify_history != 0) {
+		warning(_("some rev walking options will be overridden as "
+			  "'%s' bit in 'struct rev_info' will be forced"),
+			"simplify_history");
+		revs.simplify_history = 0;
+	}
+
 	strvec_clear(&rev_walk_args);
 
 	if (prepare_revision_walk(&revs) < 0) {
-- 
2.43.0.14.g93e034faee


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v8 07/14] replay: add an important FIXME comment about gpg signing
  2023-11-24 11:10             ` [PATCH v8 " Christian Couder
                                 ` (5 preceding siblings ...)
  2023-11-24 11:10               ` [PATCH v8 06/14] replay: change rev walking options Christian Couder
@ 2023-11-24 11:10               ` Christian Couder
  2023-11-24 11:10               ` [PATCH v8 08/14] replay: remove progress and info output Christian Couder
                                 ` (7 subsequent siblings)
  14 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-24 11:10 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

We want to be able to handle signed commits in some way in the future,
but we are not ready to do it now. So for the time being let's just add
a FIXME comment to remind us about it.

These are different ways we could handle them:

  - in case of a cli user and if there was an interactive mode, we could
    perhaps ask if the user wants to sign again
  - we could add an option to just fail if there are signed commits

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 2f664218be..384bb4ddd3 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -62,7 +62,7 @@ static struct commit *create_commit(struct tree *tree,
 	struct object *obj;
 	struct commit_list *parents = NULL;
 	char *author;
-	char *sign_commit = NULL;
+	char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
 	struct commit_extra_header *extra;
 	struct strbuf msg = STRBUF_INIT;
 	const char *out_enc = get_commit_output_encoding();
-- 
2.43.0.14.g93e034faee


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v8 08/14] replay: remove progress and info output
  2023-11-24 11:10             ` [PATCH v8 " Christian Couder
                                 ` (6 preceding siblings ...)
  2023-11-24 11:10               ` [PATCH v8 07/14] replay: add an important FIXME comment about gpg signing Christian Couder
@ 2023-11-24 11:10               ` Christian Couder
  2023-11-24 11:10               ` [PATCH v8 09/14] replay: remove HEAD related sanity check Christian Couder
                                 ` (6 subsequent siblings)
  14 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-24 11:10 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

The replay command will be changed in a follow up commit, so that it
will not update refs directly, but instead it will print on stdout a
list of commands that can be consumed by `git update-ref --stdin`.

We don't want this output to be polluted by its current low value
output, so let's just remove the latter.

In the future, when the command gets an option to update refs by
itself, it will make a lot of sense to display a progress meter, but
we are not there yet.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c | 7 +------
 1 file changed, 1 insertion(+), 6 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 384bb4ddd3..ca3867dc57 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -232,7 +232,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
-	merge_opt.show_rename_progress = 1;
+	merge_opt.show_rename_progress = 0;
 	merge_opt.branch1 = "HEAD";
 	head_tree = repo_get_commit_tree(the_repository, onto);
 	result.tree = head_tree;
@@ -240,9 +240,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	while ((commit = get_revision(&revs))) {
 		struct commit *pick;
 
-		fprintf(stderr, "Rebasing %s...\r",
-			oid_to_hex(&commit->object.oid));
-
 		if (!commit->parents)
 			die(_("replaying down to root commit is not supported yet!"));
 		if (commit->parents->next)
@@ -261,7 +258,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		exit(128);
 
 	if (result.clean) {
-		fprintf(stderr, "\nDone.\n");
 		strbuf_addf(&reflog_msg, "finish rebase %s onto %s",
 			    oid_to_hex(&last_picked_commit->object.oid),
 			    oid_to_hex(&last_commit->object.oid));
@@ -275,7 +271,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		if (create_symref("HEAD", branch_name.buf, reflog_msg.buf) < 0)
 			die(_("unable to update HEAD"));
 	} else {
-		fprintf(stderr, "\nAborting: Hit a conflict.\n");
 		strbuf_addf(&reflog_msg, "rebase progress up to %s",
 			    oid_to_hex(&last_picked_commit->object.oid));
 		if (update_ref(reflog_msg.buf, "HEAD",
-- 
2.43.0.14.g93e034faee


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v8 09/14] replay: remove HEAD related sanity check
  2023-11-24 11:10             ` [PATCH v8 " Christian Couder
                                 ` (7 preceding siblings ...)
  2023-11-24 11:10               ` [PATCH v8 08/14] replay: remove progress and info output Christian Couder
@ 2023-11-24 11:10               ` Christian Couder
  2023-11-24 11:10               ` [PATCH v8 10/14] replay: make it a minimal server side command Christian Couder
                                 ` (5 subsequent siblings)
  14 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-24 11:10 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

We want replay to be a command that can be used on the server side on
any branch, not just the current one, so we are going to stop updating
HEAD in a future commit.

A "sanity check" that makes sure we are replaying the current branch
doesn't make sense anymore. Let's remove it.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c         | 8 +-------
 t/t3650-replay-basics.sh | 2 --
 2 files changed, 1 insertion(+), 9 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index ca3867dc57..bdec2f2b97 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -123,7 +123,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct commit *onto;
 	const char *onto_name = NULL;
 	struct commit *last_commit = NULL, *last_picked_commit = NULL;
-	struct object_id head;
 	struct lock_file lock = LOCK_INIT;
 	struct strvec rev_walk_args = STRVEC_INIT;
 	struct rev_info revs;
@@ -162,11 +161,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	onto = peel_committish(onto_name);
 	strbuf_addf(&branch_name, "refs/heads/%s", argv[2]);
 
-	/* Sanity check */
-	if (repo_get_oid(the_repository, "HEAD", &head))
-		die(_("Cannot read HEAD"));
-	assert(oideq(&onto->object.oid, &head));
-
 	repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
 	if (repo_read_index(the_repository) < 0)
 		BUG("Could not read index");
@@ -275,7 +269,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			    oid_to_hex(&last_picked_commit->object.oid));
 		if (update_ref(reflog_msg.buf, "HEAD",
 			       &last_commit->object.oid,
-			       &head,
+			       &onto->object.oid,
 			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
 			error(_("could not update %s"), argv[2]);
 			die("Failed to update %s", argv[2]);
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 36c1b5082a..b5b9f9ade2 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -48,8 +48,6 @@ test_expect_success 'setup' '
 '
 
 test_expect_success 'using replay to rebase two branches, one on top of other' '
-	git switch main &&
-
 	git replay --onto main topic1 topic2 >result &&
 
 	git log --format=%s $(cut -f 3 -d " " result) >actual &&
-- 
2.43.0.14.g93e034faee


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v8 10/14] replay: make it a minimal server side command
  2023-11-24 11:10             ` [PATCH v8 " Christian Couder
                                 ` (8 preceding siblings ...)
  2023-11-24 11:10               ` [PATCH v8 09/14] replay: remove HEAD related sanity check Christian Couder
@ 2023-11-24 11:10               ` Christian Couder
  2023-11-24 11:10               ` [PATCH v8 11/14] replay: use standard revision ranges Christian Couder
                                 ` (4 subsequent siblings)
  14 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-24 11:10 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

We want this command to be a minimal command that just does server side
picking of commits, displaying the results on stdout for higher level
scripts to consume.

So let's simplify it:
  * remove the worktree and index reading/writing,
  * remove the ref (and reflog) updating,
  * remove the assumptions tying us to HEAD, since (a) this is not a
    rebase and (b) we want to be able to pick commits in a bare repo,
    i.e. to/from branches that are not checked out and not the main
    branch,
  * remove unneeded includes,
  * handle rebasing multiple branches by printing on stdout the update
    ref commands that should be performed.

The output can be piped into `git update-ref --stdin` for the ref
updates to happen.

In the future to make it easier for users to use this command
directly maybe an option can be added to automatically pipe its output
into `git update-ref`.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 Documentation/git-replay.txt             |  5 +-
 builtin/replay.c                         | 78 ++++++++----------------
 t/t3650-replay-basics.sh                 | 19 +++++-
 t/t6429-merge-sequence-rename-caching.sh | 39 +++++++-----
 4 files changed, 72 insertions(+), 69 deletions(-)

diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
index 2ca7ca5fd8..267282d92a 100644
--- a/Documentation/git-replay.txt
+++ b/Documentation/git-replay.txt
@@ -15,7 +15,10 @@ DESCRIPTION
 -----------
 
 Takes a range of commits, specified by <oldbase> and <branch>, and
-replays them onto a new location (see `--onto` option below).
+replays them onto a new location (see `--onto` option below). Leaves
+the working tree and the index untouched, and updates no references.
+The output of this command is meant to be used as input to
+`git update-ref --stdin`, which would update the relevant branches.
 
 THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
 
diff --git a/builtin/replay.c b/builtin/replay.c
index bdec2f2b97..bfccbbbfea 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -6,11 +6,7 @@
 #include "git-compat-util.h"
 
 #include "builtin.h"
-#include "cache-tree.h"
-#include "commit.h"
 #include "environment.h"
-#include "gettext.h"
-#include "hash.h"
 #include "hex.h"
 #include "lockfile.h"
 #include "merge-ort.h"
@@ -18,8 +14,6 @@
 #include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
-#include "sequencer.h"
-#include "setup.h"
 #include "strvec.h"
 #include <oidset.h>
 #include <tree.h>
@@ -102,6 +96,7 @@ static struct commit *pick_regular_commit(struct commit *pickme,
 	pickme_tree = repo_get_commit_tree(the_repository, pickme);
 	base_tree = repo_get_commit_tree(the_repository, base);
 
+	merge_opt->branch1 = short_commit_name(last_commit);
 	merge_opt->branch2 = short_commit_name(pickme);
 	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
 
@@ -122,15 +117,12 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 {
 	struct commit *onto;
 	const char *onto_name = NULL;
-	struct commit *last_commit = NULL, *last_picked_commit = NULL;
-	struct lock_file lock = LOCK_INIT;
+	struct commit *last_commit = NULL;
 	struct strvec rev_walk_args = STRVEC_INIT;
 	struct rev_info revs;
 	struct commit *commit;
 	struct merge_options merge_opt;
-	struct tree *head_tree;
 	struct merge_result result;
-	struct strbuf reflog_msg = STRBUF_INIT;
 	struct strbuf branch_name = STRBUF_INIT;
 	int ret = 0;
 
@@ -161,10 +153,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	onto = peel_committish(onto_name);
 	strbuf_addf(&branch_name, "refs/heads/%s", argv[2]);
 
-	repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
-	if (repo_read_index(the_repository) < 0)
-		BUG("Could not read index");
-
 	repo_init_revisions(the_repository, &revs, prefix);
 
 	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
@@ -227,58 +215,44 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
 	merge_opt.show_rename_progress = 0;
-	merge_opt.branch1 = "HEAD";
-	head_tree = repo_get_commit_tree(the_repository, onto);
-	result.tree = head_tree;
+	result.tree = repo_get_commit_tree(the_repository, onto);
 	last_commit = onto;
 	while ((commit = get_revision(&revs))) {
-		struct commit *pick;
+		const struct name_decoration *decoration;
 
 		if (!commit->parents)
 			die(_("replaying down to root commit is not supported yet!"));
 		if (commit->parents->next)
 			die(_("replaying merge commits is not supported yet!"));
 
-		pick = pick_regular_commit(commit, last_commit, &merge_opt, &result);
-		if (!pick)
+		last_commit = pick_regular_commit(commit, last_commit, &merge_opt, &result);
+		if (!last_commit)
 			break;
-		last_commit = pick;
-		last_picked_commit = commit;
+
+		decoration = get_name_decoration(&commit->object);
+		if (!decoration)
+			continue;
+
+		while (decoration) {
+			if (decoration->type == DECORATION_REF_LOCAL) {
+				printf("update %s %s %s\n",
+				       decoration->name,
+				       oid_to_hex(&last_commit->object.oid),
+				       oid_to_hex(&commit->object.oid));
+			}
+			decoration = decoration->next;
+		}
 	}
 
 	merge_finalize(&merge_opt, &result);
+	ret = result.clean;
 
-	if (result.clean < 0)
-		exit(128);
-
-	if (result.clean) {
-		strbuf_addf(&reflog_msg, "finish rebase %s onto %s",
-			    oid_to_hex(&last_picked_commit->object.oid),
-			    oid_to_hex(&last_commit->object.oid));
-		if (update_ref(reflog_msg.buf, branch_name.buf,
-			       &last_commit->object.oid,
-			       &last_picked_commit->object.oid,
-			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
-			error(_("could not update %s"), argv[2]);
-			die("Failed to update %s", argv[2]);
-		}
-		if (create_symref("HEAD", branch_name.buf, reflog_msg.buf) < 0)
-			die(_("unable to update HEAD"));
-	} else {
-		strbuf_addf(&reflog_msg, "rebase progress up to %s",
-			    oid_to_hex(&last_picked_commit->object.oid));
-		if (update_ref(reflog_msg.buf, "HEAD",
-			       &last_commit->object.oid,
-			       &onto->object.oid,
-			       REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
-			error(_("could not update %s"), argv[2]);
-			die("Failed to update %s", argv[2]);
-		}
-	}
-	ret = (result.clean == 0);
 cleanup:
-	strbuf_release(&reflog_msg);
 	strbuf_release(&branch_name);
 	release_revisions(&revs);
-	return ret;
+
+	/* Return */
+	if (ret < 0)
+		exit(128);
+	return ret ? 0 : 1;
 }
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index b5b9f9ade2..3567c98362 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -47,12 +47,29 @@ test_expect_success 'setup' '
 	test_commit C.conflict C.t conflict
 '
 
+test_expect_success 'setup bare' '
+	git clone --bare . bare
+'
+
 test_expect_success 'using replay to rebase two branches, one on top of other' '
 	git replay --onto main topic1 topic2 >result &&
 
+	test_line_count = 1 result &&
+
 	git log --format=%s $(cut -f 3 -d " " result) >actual &&
 	test_write_lines E D M L B A >expect &&
-	test_cmp expect actual
+	test_cmp expect actual &&
+
+	printf "update refs/heads/topic2 " >expect &&
+	printf "%s " $(cut -f 3 -d " " result) >>expect &&
+	git rev-parse topic2 >>expect &&
+
+	test_cmp expect result
+'
+
+test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '
+	git -C bare replay --onto main topic1 topic2 >result-bare &&
+	test_cmp expect result-bare
 '
 
 test_done
diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh
index 7670b72008..099aefeffc 100755
--- a/t/t6429-merge-sequence-rename-caching.sh
+++ b/t/t6429-merge-sequence-rename-caching.sh
@@ -71,8 +71,9 @@ test_expect_success 'caching renames does not preclude finding new ones' '
 
 		git switch upstream &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		git ls-files >tracked-files &&
 		test_line_count = 2 tracked-files &&
@@ -140,7 +141,9 @@ test_expect_success 'cherry-pick both a commit and its immediate revert' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 1 calls
@@ -198,8 +201,9 @@ test_expect_success 'rename same file identically, then reintroduce it' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		git ls-files >tracked &&
 		test_line_count = 2 tracked &&
@@ -275,8 +279,9 @@ test_expect_success 'rename same file identically, then add file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		git ls-files >tracked &&
 		test_line_count = 4 tracked &&
@@ -451,8 +456,9 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls &&
@@ -517,8 +523,9 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 3 calls &&
@@ -619,8 +626,9 @@ test_expect_success 'caching renames only on upstream side, part 1' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 1 calls &&
@@ -677,8 +685,9 @@ test_expect_success 'caching renames only on upstream side, part 2' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic &&
-		git reset --hard topic &&
+		git replay --onto HEAD upstream~1 topic >out &&
+		git update-ref --stdin <out &&
+		git checkout topic &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls &&
-- 
2.43.0.14.g93e034faee


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v8 11/14] replay: use standard revision ranges
  2023-11-24 11:10             ` [PATCH v8 " Christian Couder
                                 ` (9 preceding siblings ...)
  2023-11-24 11:10               ` [PATCH v8 10/14] replay: make it a minimal server side command Christian Couder
@ 2023-11-24 11:10               ` Christian Couder
  2023-11-24 11:10               ` [PATCH v8 12/14] replay: add --advance or 'cherry-pick' mode Christian Couder
                                 ` (3 subsequent siblings)
  14 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-24 11:10 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

Instead of the fixed "<oldbase> <branch>" arguments, the replay
command now accepts "<revision-range>..." arguments in a similar
way as many other Git commands. This makes its interface more
standard and more flexible.

This also enables many revision related options accepted and
eaten by setup_revisions(). If the replay command was a high level
one or had a high level mode, it would make sense to restrict some
of the possible options, like those generating non-contiguous
history, as they could be confusing for most users.

Also as the interface of the command is now mostly finalized,
we can add more documentation and more testcases to make sure
the command will continue to work as designed in the future.

We only document the rev-list related options among all the
revision related options that are now accepted, as the rev-list
related ones are probably the most useful for now.

Helped-by: Dragan Simic <dsimic@manjaro.org>
Helped-by: Linus Arver <linusa@google.com>
Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 Documentation/git-replay.txt             | 58 ++++++++++++++++++++++--
 builtin/replay.c                         | 21 ++-------
 t/t3650-replay-basics.sh                 | 12 ++++-
 t/t6429-merge-sequence-rename-caching.sh | 18 ++++----
 4 files changed, 77 insertions(+), 32 deletions(-)

diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
index 267282d92a..f7b232caa2 100644
--- a/Documentation/git-replay.txt
+++ b/Documentation/git-replay.txt
@@ -9,16 +9,16 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
 SYNOPSIS
 --------
 [verse]
-(EXPERIMENTAL!) 'git replay' --onto <newbase> <oldbase> <branch>
+(EXPERIMENTAL!) 'git replay' --onto <newbase> <revision-range>...
 
 DESCRIPTION
 -----------
 
-Takes a range of commits, specified by <oldbase> and <branch>, and
-replays them onto a new location (see `--onto` option below). Leaves
+Takes ranges of commits and replays them onto a new location. Leaves
 the working tree and the index untouched, and updates no references.
 The output of this command is meant to be used as input to
-`git update-ref --stdin`, which would update the relevant branches.
+`git update-ref --stdin`, which would update the relevant branches
+(see the OUTPUT section below).
 
 THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
 
@@ -28,6 +28,30 @@ OPTIONS
 --onto <newbase>::
 	Starting point at which to create the new commits.  May be any
 	valid commit, and not just an existing branch name.
++
+The update-ref command(s) in the output will update the branch(es) in
+the revision range to point at the new commits, similar to the way how
+`git rebase --update-refs` updates multiple branches in the affected
+range.
+
+<revision-range>::
+	Range of commits to replay; see "Specifying Ranges" in
+	linkgit:git-rev-parse and the "Commit Limiting" options below.
+
+include::rev-list-options.txt[]
+
+OUTPUT
+------
+
+When there are no conflicts, the output of this command is usable as
+input to `git update-ref --stdin`.  It is of the form:
+
+	update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
+	update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
+	update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
+
+where the number of refs updated depends on the arguments passed and
+the shape of the history being replayed.
 
 EXIT STATUS
 -----------
@@ -37,6 +61,32 @@ the replay has conflicts, the exit status is 1.  If the replay is not
 able to complete (or start) due to some kind of error, the exit status
 is something other than 0 or 1.
 
+EXAMPLES
+--------
+
+To simply rebase `mybranch` onto `target`:
+
+------------
+$ git replay --onto target origin/main..mybranch
+update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
+------------
+
+When calling `git replay`, one does not need to specify a range of
+commits to replay using the syntax `A..B`; any range expression will
+do:
+
+------------
+$ git replay --onto origin/main ^base branch1 branch2 branch3
+update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
+update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
+update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
+------------
+
+This will simultaneously rebase `branch1`, `branch2`, and `branch3`,
+all commits they have since `base`, playing them on top of
+`origin/main`. These three branches may have commits on top of `base`
+that they have in common, but that does not need to be the case.
+
 GIT
 ---
 Part of the linkgit:git[1] suite
diff --git a/builtin/replay.c b/builtin/replay.c
index bfccbbbfea..3d5e00147b 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -14,7 +14,6 @@
 #include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
-#include "strvec.h"
 #include <oidset.h>
 #include <tree.h>
 
@@ -118,16 +117,14 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct commit *onto;
 	const char *onto_name = NULL;
 	struct commit *last_commit = NULL;
-	struct strvec rev_walk_args = STRVEC_INIT;
 	struct rev_info revs;
 	struct commit *commit;
 	struct merge_options merge_opt;
 	struct merge_result result;
-	struct strbuf branch_name = STRBUF_INIT;
 	int ret = 0;
 
 	const char * const replay_usage[] = {
-		N_("(EXPERIMENTAL!) git replay --onto <newbase> <oldbase> <branch>"),
+		N_("(EXPERIMENTAL!) git replay --onto <newbase> <revision-range>..."),
 		NULL
 	};
 	struct option replay_options[] = {
@@ -145,18 +142,10 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		usage_with_options(replay_usage, replay_options);
 	}
 
-	if (argc != 3) {
-		error(_("bad number of arguments"));
-		usage_with_options(replay_usage, replay_options);
-	}
-
 	onto = peel_committish(onto_name);
-	strbuf_addf(&branch_name, "refs/heads/%s", argv[2]);
 
 	repo_init_revisions(the_repository, &revs, prefix);
 
-	strvec_pushl(&rev_walk_args, "", argv[2], "--not", argv[1], NULL);
-
 	/*
 	 * Set desired values for rev walking options here. If they
 	 * are changed by some user specified option in setup_revisions()
@@ -171,8 +160,9 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	revs.topo_order = 1;
 	revs.simplify_history = 0;
 
-	if (setup_revisions(rev_walk_args.nr, rev_walk_args.v, &revs, NULL) > 1) {
-		ret = error(_("unhandled options"));
+	argc = setup_revisions(argc, argv, &revs, NULL);
+	if (argc > 1) {
+		ret = error(_("unrecognized argument: %s"), argv[1]);
 		goto cleanup;
 	}
 
@@ -205,8 +195,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		revs.simplify_history = 0;
 	}
 
-	strvec_clear(&rev_walk_args);
-
 	if (prepare_revision_walk(&revs) < 0) {
 		ret = error(_("error preparing revisions"));
 		goto cleanup;
@@ -248,7 +236,6 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	ret = result.clean;
 
 cleanup:
-	strbuf_release(&branch_name);
 	release_revisions(&revs);
 
 	/* Return */
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 3567c98362..a1da4f9ef9 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -52,7 +52,7 @@ test_expect_success 'setup bare' '
 '
 
 test_expect_success 'using replay to rebase two branches, one on top of other' '
-	git replay --onto main topic1 topic2 >result &&
+	git replay --onto main topic1..topic2 >result &&
 
 	test_line_count = 1 result &&
 
@@ -68,8 +68,16 @@ test_expect_success 'using replay to rebase two branches, one on top of other' '
 '
 
 test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '
-	git -C bare replay --onto main topic1 topic2 >result-bare &&
+	git -C bare replay --onto main topic1..topic2 >result-bare &&
 	test_cmp expect result-bare
 '
 
+test_expect_success 'using replay to rebase with a conflict' '
+	test_expect_code 1 git replay --onto topic1 B..conflict
+'
+
+test_expect_success 'using replay on bare repo to rebase with a conflict' '
+	test_expect_code 1 git -C bare replay --onto topic1 B..conflict
+'
+
 test_done
diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh
index 099aefeffc..0f39ed0d08 100755
--- a/t/t6429-merge-sequence-rename-caching.sh
+++ b/t/t6429-merge-sequence-rename-caching.sh
@@ -71,7 +71,7 @@ test_expect_success 'caching renames does not preclude finding new ones' '
 
 		git switch upstream &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -141,7 +141,7 @@ test_expect_success 'cherry-pick both a commit and its immediate revert' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -201,7 +201,7 @@ test_expect_success 'rename same file identically, then reintroduce it' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -279,7 +279,7 @@ test_expect_success 'rename same file identically, then add file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -357,7 +357,7 @@ test_expect_success 'cached dir rename does not prevent noticing later conflict'
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		test_must_fail git replay --onto HEAD upstream~1 topic >output &&
+		test_must_fail git replay --onto HEAD upstream~1..topic >output &&
 
 		grep region_enter.*diffcore_rename trace.output >calls &&
 		test_line_count = 2 calls
@@ -456,7 +456,7 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -523,7 +523,7 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -626,7 +626,7 @@ test_expect_success 'caching renames only on upstream side, part 1' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
@@ -685,7 +685,7 @@ test_expect_success 'caching renames only on upstream side, part 2' '
 		GIT_TRACE2_PERF="$(pwd)/trace.output" &&
 		export GIT_TRACE2_PERF &&
 
-		git replay --onto HEAD upstream~1 topic >out &&
+		git replay --onto HEAD upstream~1..topic >out &&
 		git update-ref --stdin <out &&
 		git checkout topic &&
 
-- 
2.43.0.14.g93e034faee


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v8 12/14] replay: add --advance or 'cherry-pick' mode
  2023-11-24 11:10             ` [PATCH v8 " Christian Couder
                                 ` (10 preceding siblings ...)
  2023-11-24 11:10               ` [PATCH v8 11/14] replay: use standard revision ranges Christian Couder
@ 2023-11-24 11:10               ` Christian Couder
  2023-11-24 11:10               ` [PATCH v8 13/14] replay: add --contained to rebase contained branches Christian Couder
                                 ` (2 subsequent siblings)
  14 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-24 11:10 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

There is already a 'rebase' mode with `--onto`. Let's add an 'advance' or
'cherry-pick' mode with `--advance`. This new mode will make the target
branch advance as we replay commits onto it.

The replayed commits should have a single tip, so that it's clear where
the target branch should be advanced. If they have more than one tip,
this new mode will error out.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 Documentation/git-replay.txt |  41 ++++++--
 builtin/replay.c             | 185 +++++++++++++++++++++++++++++++++--
 t/t3650-replay-basics.sh     |  34 +++++++
 3 files changed, 243 insertions(+), 17 deletions(-)

diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
index f7b232caa2..c4c64f955a 100644
--- a/Documentation/git-replay.txt
+++ b/Documentation/git-replay.txt
@@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
 SYNOPSIS
 --------
 [verse]
-(EXPERIMENTAL!) 'git replay' --onto <newbase> <revision-range>...
+(EXPERIMENTAL!) 'git replay' (--onto <newbase> | --advance <branch>) <revision-range>...
 
 DESCRIPTION
 -----------
@@ -29,14 +29,25 @@ OPTIONS
 	Starting point at which to create the new commits.  May be any
 	valid commit, and not just an existing branch name.
 +
-The update-ref command(s) in the output will update the branch(es) in
-the revision range to point at the new commits, similar to the way how
-`git rebase --update-refs` updates multiple branches in the affected
-range.
+When `--onto` is specified, the update-ref command(s) in the output will
+update the branch(es) in the revision range to point at the new
+commits, similar to the way how `git rebase --update-refs` updates
+multiple branches in the affected range.
+
+--advance <branch>::
+	Starting point at which to create the new commits; must be a
+	branch name.
++
+When `--advance` is specified, the update-ref command(s) in the output
+will update the branch passed as an argument to `--advance` to point at
+the new commits (in other words, this mimics a cherry-pick operation).
 
 <revision-range>::
-	Range of commits to replay; see "Specifying Ranges" in
-	linkgit:git-rev-parse and the "Commit Limiting" options below.
+	Range of commits to replay. More than one <revision-range> can
+	be passed, but in `--advance <branch>` mode, they should have
+	a single tip, so that it's clear where <branch> should point
+	to. See "Specifying Ranges" in linkgit:git-rev-parse and the
+	"Commit Limiting" options below.
 
 include::rev-list-options.txt[]
 
@@ -51,7 +62,9 @@ input to `git update-ref --stdin`.  It is of the form:
 	update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
 
 where the number of refs updated depends on the arguments passed and
-the shape of the history being replayed.
+the shape of the history being replayed.  When using `--advance`, the
+number of refs updated is always one, but for `--onto`, it can be one
+or more (rebasing multiple branches simultaneously is supported).
 
 EXIT STATUS
 -----------
@@ -71,6 +84,18 @@ $ git replay --onto target origin/main..mybranch
 update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
 ------------
 
+To cherry-pick the commits from mybranch onto target:
+
+------------
+$ git replay --advance target origin/main..mybranch
+update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}
+------------
+
+Note that the first two examples replay the exact same commits and on
+top of the exact same new base, they only differ in that the first
+provides instructions to make mybranch point at the new commits and
+the second provides instructions to make target point at them.
+
 When calling `git replay`, one does not need to specify a range of
 commits to replay using the syntax `A..B`; any range expression will
 do:
diff --git a/builtin/replay.c b/builtin/replay.c
index 3d5e00147b..f26806d7e2 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -14,6 +14,7 @@
 #include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
+#include "strmap.h"
 #include <oidset.h>
 #include <tree.h>
 
@@ -82,6 +83,146 @@ static struct commit *create_commit(struct tree *tree,
 	return (struct commit *)obj;
 }
 
+struct ref_info {
+	struct commit *onto;
+	struct strset positive_refs;
+	struct strset negative_refs;
+	int positive_refexprs;
+	int negative_refexprs;
+};
+
+static void get_ref_information(struct rev_cmdline_info *cmd_info,
+				struct ref_info *ref_info)
+{
+	int i;
+
+	ref_info->onto = NULL;
+	strset_init(&ref_info->positive_refs);
+	strset_init(&ref_info->negative_refs);
+	ref_info->positive_refexprs = 0;
+	ref_info->negative_refexprs = 0;
+
+	/*
+	 * When the user specifies e.g.
+	 *   git replay origin/main..mybranch
+	 *   git replay ^origin/next mybranch1 mybranch2
+	 * we want to be able to determine where to replay the commits.  In
+	 * these examples, the branches are probably based on an old version
+	 * of either origin/main or origin/next, so we want to replay on the
+	 * newest version of that branch.  In contrast we would want to error
+	 * out if they ran
+	 *   git replay ^origin/master ^origin/next mybranch
+	 *   git replay mybranch~2..mybranch
+	 * the first of those because there's no unique base to choose, and
+	 * the second because they'd likely just be replaying commits on top
+	 * of the same commit and not making any difference.
+	 */
+	for (i = 0; i < cmd_info->nr; i++) {
+		struct rev_cmdline_entry *e = cmd_info->rev + i;
+		struct object_id oid;
+		const char *refexpr = e->name;
+		char *fullname = NULL;
+		int can_uniquely_dwim = 1;
+
+		if (*refexpr == '^')
+			refexpr++;
+		if (repo_dwim_ref(the_repository, refexpr, strlen(refexpr), &oid, &fullname, 0) != 1)
+			can_uniquely_dwim = 0;
+
+		if (e->flags & BOTTOM) {
+			if (can_uniquely_dwim)
+				strset_add(&ref_info->negative_refs, fullname);
+			if (!ref_info->negative_refexprs)
+				ref_info->onto = lookup_commit_reference_gently(the_repository,
+										&e->item->oid, 1);
+			ref_info->negative_refexprs++;
+		} else {
+			if (can_uniquely_dwim)
+				strset_add(&ref_info->positive_refs, fullname);
+			ref_info->positive_refexprs++;
+		}
+
+		free(fullname);
+	}
+}
+
+static void determine_replay_mode(struct rev_cmdline_info *cmd_info,
+				  const char *onto_name,
+				  const char **advance_name,
+				  struct commit **onto,
+				  struct strset **update_refs)
+{
+	struct ref_info rinfo;
+
+	get_ref_information(cmd_info, &rinfo);
+	if (!rinfo.positive_refexprs)
+		die(_("need some commits to replay"));
+	if (onto_name && *advance_name)
+		die(_("--onto and --advance are incompatible"));
+	else if (onto_name) {
+		*onto = peel_committish(onto_name);
+		if (rinfo.positive_refexprs <
+		    strset_get_size(&rinfo.positive_refs))
+			die(_("all positive revisions given must be references"));
+	} else if (*advance_name) {
+		struct object_id oid;
+		char *fullname = NULL;
+
+		*onto = peel_committish(*advance_name);
+		if (repo_dwim_ref(the_repository, *advance_name, strlen(*advance_name),
+			     &oid, &fullname, 0) == 1) {
+			*advance_name = fullname;
+		} else {
+			die(_("argument to --advance must be a reference"));
+		}
+		if (rinfo.positive_refexprs > 1)
+			die(_("cannot advance target with multiple sources because ordering would be ill-defined"));
+	} else {
+		int positive_refs_complete = (
+			rinfo.positive_refexprs ==
+			strset_get_size(&rinfo.positive_refs));
+		int negative_refs_complete = (
+			rinfo.negative_refexprs ==
+			strset_get_size(&rinfo.negative_refs));
+		/*
+		 * We need either positive_refs_complete or
+		 * negative_refs_complete, but not both.
+		 */
+		if (rinfo.negative_refexprs > 0 &&
+		    positive_refs_complete == negative_refs_complete)
+			die(_("cannot implicitly determine whether this is an --advance or --onto operation"));
+		if (negative_refs_complete) {
+			struct hashmap_iter iter;
+			struct strmap_entry *entry;
+
+			if (rinfo.negative_refexprs == 0)
+				die(_("all positive revisions given must be references"));
+			else if (rinfo.negative_refexprs > 1)
+				die(_("cannot implicitly determine whether this is an --advance or --onto operation"));
+			else if (rinfo.positive_refexprs > 1)
+				die(_("cannot advance target with multiple source branches because ordering would be ill-defined"));
+
+			/* Only one entry, but we have to loop to get it */
+			strset_for_each_entry(&rinfo.negative_refs,
+					      &iter, entry) {
+				*advance_name = entry->key;
+			}
+		} else { /* positive_refs_complete */
+			if (rinfo.negative_refexprs > 1)
+				die(_("cannot implicitly determine correct base for --onto"));
+			if (rinfo.negative_refexprs == 1)
+				*onto = rinfo.onto;
+		}
+	}
+	if (!*advance_name) {
+		*update_refs = xcalloc(1, sizeof(**update_refs));
+		**update_refs = rinfo.positive_refs;
+		memset(&rinfo.positive_refs, 0, sizeof(**update_refs));
+	}
+	strset_clear(&rinfo.negative_refs);
+	strset_clear(&rinfo.positive_refs);
+}
+
 static struct commit *pick_regular_commit(struct commit *pickme,
 					  struct commit *last_commit,
 					  struct merge_options *merge_opt,
@@ -114,20 +255,26 @@ static struct commit *pick_regular_commit(struct commit *pickme,
 
 int cmd_replay(int argc, const char **argv, const char *prefix)
 {
-	struct commit *onto;
+	const char *advance_name = NULL;
+	struct commit *onto = NULL;
 	const char *onto_name = NULL;
-	struct commit *last_commit = NULL;
+
 	struct rev_info revs;
+	struct commit *last_commit = NULL;
 	struct commit *commit;
 	struct merge_options merge_opt;
 	struct merge_result result;
+	struct strset *update_refs = NULL;
 	int ret = 0;
 
 	const char * const replay_usage[] = {
-		N_("(EXPERIMENTAL!) git replay --onto <newbase> <revision-range>..."),
+		N_("(EXPERIMENTAL!) git replay (--onto <newbase> | --advance <branch>) <revision-range>..."),
 		NULL
 	};
 	struct option replay_options[] = {
+		OPT_STRING(0, "advance", &advance_name,
+			   N_("branch"),
+			   N_("make replay advance given branch")),
 		OPT_STRING(0, "onto", &onto_name,
 			   N_("revision"),
 			   N_("replay onto given commit")),
@@ -137,13 +284,11 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
 			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
 
-	if (!onto_name) {
-		error(_("option --onto is mandatory"));
+	if (!onto_name && !advance_name) {
+		error(_("option --onto or --advance is mandatory"));
 		usage_with_options(replay_usage, replay_options);
 	}
 
-	onto = peel_committish(onto_name);
-
 	repo_init_revisions(the_repository, &revs, prefix);
 
 	/*
@@ -195,6 +340,12 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		revs.simplify_history = 0;
 	}
 
+	determine_replay_mode(&revs.cmdline, onto_name, &advance_name,
+			      &onto, &update_refs);
+
+	if (!onto) /* FIXME: Should handle replaying down to root commit */
+		die("Replaying down to root commit is not supported yet!");
+
 	if (prepare_revision_walk(&revs) < 0) {
 		ret = error(_("error preparing revisions"));
 		goto cleanup;
@@ -203,6 +354,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
 	merge_opt.show_rename_progress = 0;
+
 	result.tree = repo_get_commit_tree(the_repository, onto);
 	last_commit = onto;
 	while ((commit = get_revision(&revs))) {
@@ -217,12 +369,15 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		if (!last_commit)
 			break;
 
+		/* Update any necessary branches */
+		if (advance_name)
+			continue;
 		decoration = get_name_decoration(&commit->object);
 		if (!decoration)
 			continue;
-
 		while (decoration) {
-			if (decoration->type == DECORATION_REF_LOCAL) {
+			if (decoration->type == DECORATION_REF_LOCAL &&
+			    strset_contains(update_refs, decoration->name)) {
 				printf("update %s %s %s\n",
 				       decoration->name,
 				       oid_to_hex(&last_commit->object.oid),
@@ -232,10 +387,22 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		}
 	}
 
+	/* In --advance mode, advance the target ref */
+	if (result.clean == 1 && advance_name) {
+		printf("update %s %s %s\n",
+		       advance_name,
+		       oid_to_hex(&last_commit->object.oid),
+		       oid_to_hex(&onto->object.oid));
+	}
+
 	merge_finalize(&merge_opt, &result);
 	ret = result.clean;
 
 cleanup:
+	if (update_refs) {
+		strset_clear(update_refs);
+		free(update_refs);
+	}
 	release_revisions(&revs);
 
 	/* Return */
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index a1da4f9ef9..68a87e7803 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -80,4 +80,38 @@ test_expect_success 'using replay on bare repo to rebase with a conflict' '
 	test_expect_code 1 git -C bare replay --onto topic1 B..conflict
 '
 
+test_expect_success 'using replay to perform basic cherry-pick' '
+	# The differences between this test and previous ones are:
+	#   --advance vs --onto
+	# 2nd field of result is refs/heads/main vs. refs/heads/topic2
+	# 4th field of result is hash for main instead of hash for topic2
+
+	git replay --advance main topic1..topic2 >result &&
+
+	test_line_count = 1 result &&
+
+	git log --format=%s $(cut -f 3 -d " " result) >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/main " >expect &&
+	printf "%s " $(cut -f 3 -d " " result) >>expect &&
+	git rev-parse main >>expect &&
+
+	test_cmp expect result
+'
+
+test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
+	git -C bare replay --advance main topic1..topic2 >result-bare &&
+	test_cmp expect result-bare
+'
+
+test_expect_success 'replay on bare repo fails with both --advance and --onto' '
+	test_must_fail git -C bare replay --advance main --onto main topic1..topic2 >result-bare
+'
+
+test_expect_success 'replay fails when both --advance and --onto are omitted' '
+	test_must_fail git replay topic1..topic2 >result
+'
+
 test_done
-- 
2.43.0.14.g93e034faee


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v8 13/14] replay: add --contained to rebase contained branches
  2023-11-24 11:10             ` [PATCH v8 " Christian Couder
                                 ` (11 preceding siblings ...)
  2023-11-24 11:10               ` [PATCH v8 12/14] replay: add --advance or 'cherry-pick' mode Christian Couder
@ 2023-11-24 11:10               ` Christian Couder
  2023-11-24 11:10               ` [PATCH v8 14/14] replay: stop assuming replayed branches do not diverge Christian Couder
  2023-11-25  0:02               ` [PATCH v8 00/14] Introduce new `git replay` command Johannes Schindelin
  14 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-24 11:10 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

Let's add a `--contained` option that can be used along with
`--onto` to rebase all the branches contained in the <revision-range>
argument.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 Documentation/git-replay.txt | 12 +++++++++++-
 builtin/replay.c             | 14 ++++++++++++--
 t/t3650-replay-basics.sh     | 29 +++++++++++++++++++++++++++++
 3 files changed, 52 insertions(+), 3 deletions(-)

diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
index c4c64f955a..f6c269c62d 100644
--- a/Documentation/git-replay.txt
+++ b/Documentation/git-replay.txt
@@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
 SYNOPSIS
 --------
 [verse]
-(EXPERIMENTAL!) 'git replay' (--onto <newbase> | --advance <branch>) <revision-range>...
+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...
 
 DESCRIPTION
 -----------
@@ -96,6 +96,16 @@ top of the exact same new base, they only differ in that the first
 provides instructions to make mybranch point at the new commits and
 the second provides instructions to make target point at them.
 
+What if you have a stack of branches, one depending upon another, and
+you'd really like to rebase the whole set?
+
+------------
+$ git replay --contained --onto origin/main origin/main..tipbranch
+update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
+update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
+update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}
+------------
+
 When calling `git replay`, one does not need to specify a range of
 commits to replay using the syntax `A..B`; any range expression will
 do:
diff --git a/builtin/replay.c b/builtin/replay.c
index f26806d7e2..df14657e2f 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -258,6 +258,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	const char *advance_name = NULL;
 	struct commit *onto = NULL;
 	const char *onto_name = NULL;
+	int contained = 0;
 
 	struct rev_info revs;
 	struct commit *last_commit = NULL;
@@ -268,7 +269,9 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	int ret = 0;
 
 	const char * const replay_usage[] = {
-		N_("(EXPERIMENTAL!) git replay (--onto <newbase> | --advance <branch>) <revision-range>..."),
+		N_("(EXPERIMENTAL!) git replay "
+		   "([--contained] --onto <newbase> | --advance <branch>) "
+		   "<revision-range>..."),
 		NULL
 	};
 	struct option replay_options[] = {
@@ -278,6 +281,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		OPT_STRING(0, "onto", &onto_name,
 			   N_("revision"),
 			   N_("replay onto given commit")),
+		OPT_BOOL(0, "contained", &contained,
+			 N_("advance all branches contained in revision-range")),
 		OPT_END()
 	};
 
@@ -289,6 +294,10 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 		usage_with_options(replay_usage, replay_options);
 	}
 
+	if (advance_name && contained)
+		die(_("options '%s' and '%s' cannot be used together"),
+		    "--advance", "--contained");
+
 	repo_init_revisions(the_repository, &revs, prefix);
 
 	/*
@@ -377,7 +386,8 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 			continue;
 		while (decoration) {
 			if (decoration->type == DECORATION_REF_LOCAL &&
-			    strset_contains(update_refs, decoration->name)) {
+			    (contained || strset_contains(update_refs,
+							  decoration->name))) {
 				printf("update %s %s %s\n",
 				       decoration->name,
 				       oid_to_hex(&last_commit->object.oid),
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 68a87e7803..d6286f9580 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -114,4 +114,33 @@ test_expect_success 'replay fails when both --advance and --onto are omitted' '
 	test_must_fail git replay topic1..topic2 >result
 '
 
+test_expect_success 'using replay to also rebase a contained branch' '
+	git replay --contained --onto main main..topic3 >result &&
+
+	test_line_count = 2 result &&
+	cut -f 3 -d " " result >new-branch-tips &&
+
+	git log --format=%s $(head -n 1 new-branch-tips) >actual &&
+	test_write_lines F C M L B A >expect &&
+	test_cmp expect actual &&
+
+	git log --format=%s $(tail -n 1 new-branch-tips) >actual &&
+	test_write_lines H G F C M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/topic1 " >expect &&
+	printf "%s " $(head -n 1 new-branch-tips) >>expect &&
+	git rev-parse topic1 >>expect &&
+	printf "update refs/heads/topic3 " >>expect &&
+	printf "%s " $(tail -n 1 new-branch-tips) >>expect &&
+	git rev-parse topic3 >>expect &&
+
+	test_cmp expect result
+'
+
+test_expect_success 'using replay on bare repo to also rebase a contained branch' '
+	git -C bare replay --contained --onto main main..topic3 >result-bare &&
+	test_cmp expect result-bare
+'
+
 test_done
-- 
2.43.0.14.g93e034faee


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* [PATCH v8 14/14] replay: stop assuming replayed branches do not diverge
  2023-11-24 11:10             ` [PATCH v8 " Christian Couder
                                 ` (12 preceding siblings ...)
  2023-11-24 11:10               ` [PATCH v8 13/14] replay: add --contained to rebase contained branches Christian Couder
@ 2023-11-24 11:10               ` Christian Couder
  2023-11-25  0:02               ` [PATCH v8 00/14] Introduce new `git replay` command Johannes Schindelin
  14 siblings, 0 replies; 208+ messages in thread
From: Christian Couder @ 2023-11-24 11:10 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Johannes Schindelin,
	Elijah Newren, John Cai, Derrick Stolee, Phillip Wood, Calvin Wan,
	Toon Claes, Dragan Simic, Linus Arver, Christian Couder

From: Elijah Newren <newren@gmail.com>

The replay command is able to replay multiple branches but when some of
them are based on other replayed branches, their commit should be
replayed onto already replayed commits.

For this purpose, let's store the replayed commit and its original
commit in a key value store, so that we can easily find and reuse a
replayed commit instead of the original one.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 builtin/replay.c         | 44 ++++++++++++++++++++++++++--------
 t/t3650-replay-basics.sh | 52 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 86 insertions(+), 10 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index df14657e2f..6bc4b47f09 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -223,20 +223,33 @@ static void determine_replay_mode(struct rev_cmdline_info *cmd_info,
 	strset_clear(&rinfo.positive_refs);
 }
 
+static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,
+				    struct commit *commit,
+				    struct commit *fallback)
+{
+	khint_t pos = kh_get_oid_map(replayed_commits, commit->object.oid);
+	if (pos == kh_end(replayed_commits))
+		return fallback;
+	return kh_value(replayed_commits, pos);
+}
+
 static struct commit *pick_regular_commit(struct commit *pickme,
-					  struct commit *last_commit,
+					  kh_oid_map_t *replayed_commits,
+					  struct commit *onto,
 					  struct merge_options *merge_opt,
 					  struct merge_result *result)
 {
-	struct commit *base;
+	struct commit *base, *replayed_base;
 	struct tree *pickme_tree, *base_tree;
 
 	base = pickme->parents->item;
+	replayed_base = mapped_commit(replayed_commits, base, onto);
 
+	result->tree = repo_get_commit_tree(the_repository, replayed_base);
 	pickme_tree = repo_get_commit_tree(the_repository, pickme);
 	base_tree = repo_get_commit_tree(the_repository, base);
 
-	merge_opt->branch1 = short_commit_name(last_commit);
+	merge_opt->branch1 = short_commit_name(replayed_base);
 	merge_opt->branch2 = short_commit_name(pickme);
 	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
 
@@ -250,7 +263,7 @@ static struct commit *pick_regular_commit(struct commit *pickme,
 	merge_opt->ancestor = NULL;
 	if (!result->clean)
 		return NULL;
-	return create_commit(result->tree, pickme, last_commit);
+	return create_commit(result->tree, pickme, replayed_base);
 }
 
 int cmd_replay(int argc, const char **argv, const char *prefix)
@@ -266,6 +279,7 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	struct merge_options merge_opt;
 	struct merge_result result;
 	struct strset *update_refs = NULL;
+	kh_oid_map_t *replayed_commits;
 	int ret = 0;
 
 	const char * const replay_usage[] = {
@@ -363,21 +377,30 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
 	merge_opt.show_rename_progress = 0;
-
-	result.tree = repo_get_commit_tree(the_repository, onto);
 	last_commit = onto;
+	replayed_commits = kh_init_oid_map();
 	while ((commit = get_revision(&revs))) {
 		const struct name_decoration *decoration;
+		khint_t pos;
+		int hr;
 
 		if (!commit->parents)
 			die(_("replaying down to root commit is not supported yet!"));
 		if (commit->parents->next)
 			die(_("replaying merge commits is not supported yet!"));
 
-		last_commit = pick_regular_commit(commit, last_commit, &merge_opt, &result);
+		last_commit = pick_regular_commit(commit, replayed_commits, onto,
+						  &merge_opt, &result);
 		if (!last_commit)
 			break;
 
+		/* Record commit -> last_commit mapping */
+		pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr);
+		if (hr == 0)
+			BUG("Duplicate rewritten commit: %s\n",
+			    oid_to_hex(&commit->object.oid));
+		kh_value(replayed_commits, pos) = last_commit;
+
 		/* Update any necessary branches */
 		if (advance_name)
 			continue;
@@ -406,13 +429,14 @@ int cmd_replay(int argc, const char **argv, const char *prefix)
 	}
 
 	merge_finalize(&merge_opt, &result);
-	ret = result.clean;
-
-cleanup:
+	kh_destroy_oid_map(replayed_commits);
 	if (update_refs) {
 		strset_clear(update_refs);
 		free(update_refs);
 	}
+	ret = result.clean;
+
+cleanup:
 	release_revisions(&revs);
 
 	/* Return */
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index d6286f9580..389670262e 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -143,4 +143,56 @@ test_expect_success 'using replay on bare repo to also rebase a contained branch
 	test_cmp expect result-bare
 '
 
+test_expect_success 'using replay to rebase multiple divergent branches' '
+	git replay --onto main ^topic1 topic2 topic4 >result &&
+
+	test_line_count = 2 result &&
+	cut -f 3 -d " " result >new-branch-tips &&
+
+	git log --format=%s $(head -n 1 new-branch-tips) >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual &&
+
+	git log --format=%s $(tail -n 1 new-branch-tips) >actual &&
+	test_write_lines J I M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/topic2 " >expect &&
+	printf "%s " $(head -n 1 new-branch-tips) >>expect &&
+	git rev-parse topic2 >>expect &&
+	printf "update refs/heads/topic4 " >>expect &&
+	printf "%s " $(tail -n 1 new-branch-tips) >>expect &&
+	git rev-parse topic4 >>expect &&
+
+	test_cmp expect result
+'
+
+test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '
+	git -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&
+
+	test_line_count = 4 result &&
+	cut -f 3 -d " " result >new-branch-tips &&
+
+	>expect &&
+	for i in 2 1 3 4
+	do
+		printf "update refs/heads/topic$i " >>expect &&
+		printf "%s " $(grep topic$i result | cut -f 3 -d " ") >>expect &&
+		git -C bare rev-parse topic$i >>expect || return 1
+	done &&
+
+	test_cmp expect result &&
+
+	test_write_lines F C M L B A >expect1 &&
+	test_write_lines E D C M L B A >expect2 &&
+	test_write_lines H G F C M L B A >expect3 &&
+	test_write_lines J I M L B A >expect4 &&
+
+	for i in 1 2 3 4
+	do
+		git -C bare log --format=%s $(grep topic$i result | cut -f 3 -d " ") >actual &&
+		test_cmp expect$i actual || return 1
+	done
+'
+
 test_done
-- 
2.43.0.14.g93e034faee


^ permalink raw reply related	[flat|nested] 208+ messages in thread

* Re: [PATCH v8 00/14] Introduce new `git replay` command
  2023-11-24 11:10             ` [PATCH v8 " Christian Couder
                                 ` (13 preceding siblings ...)
  2023-11-24 11:10               ` [PATCH v8 14/14] replay: stop assuming replayed branches do not diverge Christian Couder
@ 2023-11-25  0:02               ` Johannes Schindelin
  14 siblings, 0 replies; 208+ messages in thread
From: Johannes Schindelin @ 2023-11-25  0:02 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Patrick Steinhardt, Elijah Newren, John Cai,
	Derrick Stolee, Phillip Wood, Calvin Wan, Toon Claes,
	Dragan Simic, Linus Arver

Hi Christian,

On Fri, 24 Nov 2023, Christian Couder wrote:

> # Changes between v7 and v8
>
> Thanks to Dscho, Linus Arver, Dragan Simic, Elijah, Junio, Derrick
> Stolee, Phillip Wood, Calvin Wan and Toon Claes for their suggestions
> on the previous versions! The only few changes compared to v7 are:
>
> * The patch series was rebased onto master at 564d0252ca (Git 2.43,
>   2023-11-20). This is to make it stand on a stable base.
>
> * In patch 2/14 (replay: introduce new builtin), there is a synopsys
>   change in the doc, and the corresponding usage message change as
>   suggested by Dscho. This is just about replacing " # EXPERIMENTAL"
>   at the end of both the synopsys and usage message with
>   "(EXPERIMENTAL!) " at the beginning of them.

Thank you so much for going all the way to where we are now.

> CI tests seem to pass according to:
>
> https://github.com/chriscool/git/actions/runs/6979770154
>
> (Sorry I am not waiting more than 20 minutes for the 3 last ones to
> finish.)

Yes, our test suite takes too long, and I fear that there are quite a few
developers ignoring it as a consequence.

> # Range-diff between v7 and v8
>
> (A single change was made in patch 2/14, but unfortunately as the
> lines changed in that patch are also changed by other patches later,
> it looks like there are more changes in subsequent patches.)

Right, the lines added in 2/14 are changed multiple times over the course
of the patch series.

>  1:  cddcd967b2 =  1:  18fd9b0d5d t6429: remove switching aspects of fast-rebase
>  2:  c8476fb093 !  2:  fc6bdf4de4 replay: introduce new builtin
>     @@ Documentation/git-replay.txt (new)
>      +SYNOPSIS
>      +--------
>      +[verse]
>     -+'git replay' --onto <newbase> <oldbase> <branch> # EXPERIMENTAL
>     ++(EXPERIMENTAL!) 'git replay' --onto <newbase> <oldbase> <branch>
>      +
>      +DESCRIPTION
>      +-----------
>     @@ builtin/replay.c: int cmd__fast_rebase(int argc, const char **argv)
>      -
>         if (argc == 2 && !strcmp(argv[1], "-h")) {
>      -          printf("Sorry, I am not a psychiatrist; I can not give you the help you need.  Oh, you meant usage...\n");
>     -+          printf("git replay --onto <newbase> <oldbase> <branch> # EXPERIMENTAL\n");
>     ++          printf("usage: (EXPERIMENTAL!) git replay --onto <newbase> <oldbase> <branch>\n");
>                 exit(129);
>         }
>
>  3:  43322abd1e !  3:  e96a66c352 replay: start using parse_options API
>     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
>         int ret = 0;
>
>      -  if (argc == 2 && !strcmp(argv[1], "-h")) {
>     --          printf("git replay --onto <newbase> <oldbase> <branch> # EXPERIMENTAL\n");
>     +-          printf("usage: (EXPERIMENTAL!) git replay --onto <newbase> <oldbase> <branch>\n");
>      -          exit(129);
>      +  const char * const replay_usage[] = {
>     -+          N_("git replay --onto <newbase> <oldbase> <branch> # EXPERIMENTAL"),
>     ++          N_("(EXPERIMENTAL!) git replay --onto <newbase> <oldbase> <branch>"),
>      +          NULL
>      +  };
>      +  struct option replay_options[] = {
>  4:  6524c7f045 =  4:  f819d283d9 replay: die() instead of failing assert()
>  5:  05d0efa3cb =  5:  68bbcf9492 replay: introduce pick_regular_commit()
>  6:  c7a5aad3d6 =  6:  72221c647e replay: change rev walking options
>  7:  01f35f924b =  7:  f54d8fce22 replay: add an important FIXME comment about gpg signing
>  8:  1498b24bad =  8:  e50cc22522 replay: remove progress and info output
>  9:  6786fc147b =  9:  0c5ea3d18e replay: remove HEAD related sanity check
> 10:  9a24dbb530 = 10:  9fc636fc3d replay: make it a minimal server side command
> 11:  ad6ca2fbef ! 11:  2096bcad79 replay: use standard revision ranges
>     @@ Documentation/git-replay.txt: git-replay - EXPERIMENTAL: Replay commits on a new
>       SYNOPSIS
>       --------
>       [verse]
>     --'git replay' --onto <newbase> <oldbase> <branch> # EXPERIMENTAL
>     -+'git replay' --onto <newbase> <revision-range>... # EXPERIMENTAL
>     +-(EXPERIMENTAL!) 'git replay' --onto <newbase> <oldbase> <branch>
>     ++(EXPERIMENTAL!) 'git replay' --onto <newbase> <revision-range>...
>
>       DESCRIPTION
>       -----------
>     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
>         int ret = 0;
>
>         const char * const replay_usage[] = {
>     --          N_("git replay --onto <newbase> <oldbase> <branch> # EXPERIMENTAL"),
>     -+          N_("git replay --onto <newbase> <revision-range>... # EXPERIMENTAL"),
>     +-          N_("(EXPERIMENTAL!) git replay --onto <newbase> <oldbase> <branch>"),
>     ++          N_("(EXPERIMENTAL!) git replay --onto <newbase> <revision-range>..."),
>                 NULL
>         };
>         struct option replay_options[] = {
> 12:  081864ed5f ! 12:  d5414806ef replay: add --advance or 'cherry-pick' mode
>     @@ Documentation/git-replay.txt: git-replay - EXPERIMENTAL: Replay commits on a new
>       SYNOPSIS
>       --------
>       [verse]
>     --'git replay' --onto <newbase> <revision-range>... # EXPERIMENTAL
>     -+'git replay' (--onto <newbase> | --advance <branch>) <revision-range>... # EXPERIMENTAL
>     +-(EXPERIMENTAL!) 'git replay' --onto <newbase> <revision-range>...
>     ++(EXPERIMENTAL!) 'git replay' (--onto <newbase> | --advance <branch>) <revision-range>...
>
>       DESCRIPTION
>       -----------
>     @@ builtin/replay.c: static struct commit *pick_regular_commit(struct commit *pickm
>         int ret = 0;
>
>         const char * const replay_usage[] = {
>     --          N_("git replay --onto <newbase> <revision-range>... # EXPERIMENTAL"),
>     -+          N_("git replay (--onto <newbase> | --advance <branch>) <revision-range>... # EXPERIMENTAL"),
>     +-          N_("(EXPERIMENTAL!) git replay --onto <newbase> <revision-range>..."),
>     ++          N_("(EXPERIMENTAL!) git replay (--onto <newbase> | --advance <branch>) <revision-range>..."),
>                 NULL
>         };
>         struct option replay_options[] = {
> 13:  19c4016c7c ! 13:  2a3e521c13 replay: add --contained to rebase contained branches
>     @@ Documentation/git-replay.txt: git-replay - EXPERIMENTAL: Replay commits on a new
>       SYNOPSIS
>       --------
>       [verse]
>     --'git replay' (--onto <newbase> | --advance <branch>) <revision-range>... # EXPERIMENTAL
>     -+'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>... # EXPERIMENTAL
>     +-(EXPERIMENTAL!) 'git replay' (--onto <newbase> | --advance <branch>) <revision-range>...
>     ++(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...
>
>       DESCRIPTION
>       -----------
>     @@ builtin/replay.c: int cmd_replay(int argc, const char **argv, const char *prefix
>         int ret = 0;
>
>         const char * const replay_usage[] = {
>     --          N_("git replay (--onto <newbase> | --advance <branch>) <revision-range>... # EXPERIMENTAL"),
>     -+          N_("git replay ([--contained] --onto <newbase> | --advance <branch>) "
>     -+             "<revision-range>... # EXPERIMENTAL"),
>     +-          N_("(EXPERIMENTAL!) git replay (--onto <newbase> | --advance <branch>) <revision-range>..."),
>     ++          N_("(EXPERIMENTAL!) git replay "
>     ++             "([--contained] --onto <newbase> | --advance <branch>) "
>     ++             "<revision-range>..."),
>                 NULL
>         };
>         struct option replay_options[] = {
> 14:  29556bcc86 = 14:  93e034faee replay: stop assuming replayed branches do not diverge

The range-diff looks excellent!

Thank you for addressing all of my concerns, I am very much in favor of
getting this version into git/git's main branch.

Thank you,
Johannes

^ permalink raw reply	[flat|nested] 208+ messages in thread

end of thread, other threads:[~2023-11-25  0:02 UTC | newest]

Thread overview: 208+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2023-04-07  7:24 [PATCH 00/14] Introduce new `git replay` command Christian Couder
2023-04-07  7:24 ` [PATCH 01/14] replay: introduce new builtin Christian Couder
2023-04-07  7:24 ` [PATCH 02/14] replay: start using parse_options API Christian Couder
2023-04-07  7:24 ` [PATCH 03/14] replay: die() instead of failing assert() Christian Couder
2023-04-07  7:24 ` [PATCH 04/14] replay: introduce pick_regular_commit() Christian Couder
2023-04-07  7:24 ` [PATCH 05/14] replay: don't simplify history Christian Couder
2023-04-07  7:24 ` [PATCH 06/14] replay: add an important FIXME comment about gpg signing Christian Couder
2023-04-07  7:24 ` [PATCH 07/14] replay: remove progress and info output Christian Couder
2023-04-07  7:24 ` [PATCH 08/14] replay: remove HEAD related sanity check Christian Couder
2023-04-07  7:24 ` [PATCH 09/14] replay: very coarse worktree updating Christian Couder
2023-04-07  7:24 ` [PATCH 10/14] replay: make it a minimal server side command Christian Couder
2023-04-07  7:24 ` [PATCH 11/14] replay: use standard revision ranges Christian Couder
2023-04-14 14:09   ` Derrick Stolee
2023-04-14 14:23     ` Derrick Stolee
2023-04-15 19:07       ` Elijah Newren
2023-04-16  5:28         ` Elijah Newren
2023-04-17 14:05         ` Derrick Stolee
2023-04-18  5:54           ` Elijah Newren
2023-04-18 13:10             ` Derrick Stolee
2023-04-20  4:53               ` Elijah Newren
2023-04-20 13:44                 ` Derrick Stolee
2023-04-23  1:18                   ` Elijah Newren
2023-04-24 15:23                     ` Derrick Stolee
2023-04-30  6:45                       ` Elijah Newren
2023-09-03 15:47                         ` Johannes Schindelin
2023-09-07  8:39                           ` Christian Couder
2023-09-07 10:22                             ` Johannes Schindelin
2023-04-17 15:45         ` Junio C Hamano
2023-04-18  5:58           ` Elijah Newren
2023-04-18  4:58       ` Elijah Newren
2023-04-15 18:30     ` Elijah Newren
2023-04-07  7:24 ` [PATCH 12/14] replay: introduce guess_new_base() Christian Couder
2023-04-07  7:24 ` [PATCH 13/14] replay: add different modes Christian Couder
2023-04-07  7:24 ` [PATCH 14/14] replay: stop assuming replayed branches do not diverge Christian Couder
2023-04-14 10:12 ` [PATCH 00/14] Introduce new `git replay` command Phillip Wood
2023-04-15 17:18   ` Elijah Newren
2023-04-14 17:39 ` Felipe Contreras
2023-04-15  6:44 ` Elijah Newren
2023-05-09 17:53 ` [PATCH v2 00/15] " Christian Couder
2023-05-09 17:53   ` [PATCH v2 01/15] t6429: remove switching aspects of fast-rebase Christian Couder
2023-05-09 17:53   ` [PATCH v2 02/15] replay: introduce new builtin Christian Couder
2023-05-09 17:53   ` [PATCH v2 03/15] replay: start using parse_options API Christian Couder
2023-05-09 17:53   ` [PATCH v2 04/15] replay: die() instead of failing assert() Christian Couder
2023-05-09 17:53   ` [PATCH v2 05/15] replay: introduce pick_regular_commit() Christian Couder
2023-05-09 17:53   ` [PATCH v2 06/15] replay: don't simplify history Christian Couder
2023-05-09 17:53   ` [PATCH v2 07/15] replay: add an important FIXME comment about gpg signing Christian Couder
2023-05-09 17:53   ` [PATCH v2 08/15] replay: remove progress and info output Christian Couder
2023-05-09 17:53   ` [PATCH v2 09/15] replay: remove HEAD related sanity check Christian Couder
2023-05-09 17:53   ` [PATCH v2 10/15] replay: make it a minimal server side command Christian Couder
2023-05-09 17:53   ` [PATCH v2 11/15] replay: use standard revision ranges Christian Couder
2023-05-09 17:53   ` [PATCH v2 12/15] replay: disallow revision specific options and pathspecs Christian Couder
2023-05-16  4:25     ` Elijah Newren
2023-05-09 17:53   ` [PATCH v2 13/15] replay: add --advance or 'cherry-pick' mode Christian Couder
2023-05-09 17:53   ` [PATCH v2 14/15] replay: add --contained to rebase contained branches Christian Couder
2023-05-16  4:26     ` Elijah Newren
2023-05-09 17:53   ` [PATCH v2 15/15] replay: stop assuming replayed branches do not diverge Christian Couder
2023-05-16  4:26     ` Elijah Newren
2023-05-09 22:28   ` [PATCH v2 00/15] Introduce new `git replay` command Junio C Hamano
2023-05-10  7:33     ` Christian Couder
2023-05-16  4:42   ` Elijah Newren
2023-06-02 10:25   ` [PATCH v3 " Christian Couder
2023-06-02 10:25     ` [PATCH v3 01/15] t6429: remove switching aspects of fast-rebase Christian Couder
2023-06-02 10:25     ` [PATCH v3 02/15] replay: introduce new builtin Christian Couder
2023-06-02 10:25     ` [PATCH v3 03/15] replay: start using parse_options API Christian Couder
2023-06-02 10:25     ` [PATCH v3 04/15] replay: die() instead of failing assert() Christian Couder
2023-06-02 10:25     ` [PATCH v3 05/15] replay: introduce pick_regular_commit() Christian Couder
2023-06-02 10:25     ` [PATCH v3 06/15] replay: don't simplify history Christian Couder
2023-06-02 10:25     ` [PATCH v3 07/15] replay: add an important FIXME comment about gpg signing Christian Couder
2023-06-02 10:25     ` [PATCH v3 08/15] replay: remove progress and info output Christian Couder
2023-06-02 10:25     ` [PATCH v3 09/15] replay: remove HEAD related sanity check Christian Couder
2023-06-02 10:25     ` [PATCH v3 10/15] replay: make it a minimal server side command Christian Couder
2023-06-22 10:01       ` Toon Claes
2023-09-07  8:32         ` Christian Couder
2023-06-02 10:25     ` [PATCH v3 11/15] replay: use standard revision ranges Christian Couder
2023-06-22 10:03       ` Toon Claes
2023-09-07  8:32         ` Christian Couder
2023-09-07 21:02           ` Dragan Simic
2023-10-10 12:44             ` Christian Couder
2023-10-10 14:02               ` Dragan Simic
2023-06-02 10:25     ` [PATCH v3 12/15] replay: disallow revision specific options and pathspecs Christian Couder
2023-07-25 21:16       ` Junio C Hamano
2023-09-07  8:33         ` Christian Couder
2023-06-02 10:25     ` [PATCH v3 13/15] replay: add --advance or 'cherry-pick' mode Christian Couder
2023-06-22 10:05       ` Toon Claes
2023-09-07  8:35         ` Christian Couder
2023-07-25 21:41       ` Junio C Hamano
2023-09-07  8:35         ` Christian Couder
2023-06-02 10:25     ` [PATCH v3 14/15] replay: add --contained to rebase contained branches Christian Couder
2023-06-22 10:10       ` Toon Claes
2023-09-07  8:37         ` Christian Couder
2023-06-02 10:25     ` [PATCH v3 15/15] replay: stop assuming replayed branches do not diverge Christian Couder
2023-06-03  1:42     ` [PATCH v3 00/15] Introduce new `git replay` command Junio C Hamano
2023-06-05  7:11       ` Christian Couder
2023-09-07  9:25     ` [PATCH v4 " Christian Couder
2023-09-07  9:25       ` [PATCH v4 01/15] t6429: remove switching aspects of fast-rebase Christian Couder
2023-09-07  9:25       ` [PATCH v4 02/15] replay: introduce new builtin Christian Couder
2023-09-07 10:23         ` Johannes Schindelin
2023-10-10 12:42           ` Christian Couder
2023-09-07  9:25       ` [PATCH v4 03/15] replay: start using parse_options API Christian Couder
2023-09-07  9:25       ` [PATCH v4 04/15] replay: die() instead of failing assert() Christian Couder
2023-09-07  9:25       ` [PATCH v4 05/15] replay: introduce pick_regular_commit() Christian Couder
2023-09-07  9:25       ` [PATCH v4 06/15] replay: don't simplify history Christian Couder
2023-09-07 10:23         ` Johannes Schindelin
2023-10-10 12:43           ` Christian Couder
2023-09-07  9:25       ` [PATCH v4 07/15] replay: add an important FIXME comment about gpg signing Christian Couder
2023-09-07  9:25       ` [PATCH v4 08/15] replay: remove progress and info output Christian Couder
2023-09-07  9:25       ` [PATCH v4 09/15] replay: remove HEAD related sanity check Christian Couder
2023-09-07  9:25       ` [PATCH v4 10/15] replay: make it a minimal server side command Christian Couder
2023-09-07  9:25       ` [PATCH v4 11/15] replay: use standard revision ranges Christian Couder
2023-09-07 10:24         ` Johannes Schindelin
2023-10-10 12:49           ` Christian Couder
2023-09-08 22:55         ` Linus Arver
2023-09-10  3:20           ` Linus Arver
2023-10-10 12:48             ` Christian Couder
2023-10-10 12:48           ` Christian Couder
2023-10-19 19:26             ` Linus Arver
2023-09-07  9:25       ` [PATCH v4 12/15] replay: disallow revision specific options and pathspecs Christian Couder
2023-09-07 10:24         ` Johannes Schindelin
2023-10-10 12:49           ` Christian Couder
2023-09-07  9:25       ` [PATCH v4 13/15] replay: add --advance or 'cherry-pick' mode Christian Couder
2023-09-07  9:25       ` [PATCH v4 14/15] replay: add --contained to rebase contained branches Christian Couder
2023-09-07  9:25       ` [PATCH v4 15/15] replay: stop assuming replayed branches do not diverge Christian Couder
2023-09-07 10:25       ` [PATCH v4 00/15] Introduce new `git replay` command Johannes Schindelin
2023-10-10 12:50         ` Christian Couder
2023-10-10 12:38       ` [PATCH v5 00/14] " Christian Couder
2023-10-10 12:38         ` [PATCH v5 01/14] t6429: remove switching aspects of fast-rebase Christian Couder
2023-10-10 12:38         ` [PATCH v5 02/14] replay: introduce new builtin Christian Couder
2023-10-10 12:38         ` [PATCH v5 03/14] replay: start using parse_options API Christian Couder
2023-10-10 12:38         ` [PATCH v5 04/14] replay: die() instead of failing assert() Christian Couder
2023-10-10 12:38         ` [PATCH v5 05/14] replay: introduce pick_regular_commit() Christian Couder
2023-10-10 12:38         ` [PATCH v5 06/14] replay: change rev walking options Christian Couder
2023-10-10 12:38         ` [PATCH v5 07/14] replay: add an important FIXME comment about gpg signing Christian Couder
2023-10-10 12:38         ` [PATCH v5 08/14] replay: remove progress and info output Christian Couder
2023-10-10 12:38         ` [PATCH v5 09/14] replay: remove HEAD related sanity check Christian Couder
2023-10-10 12:38         ` [PATCH v5 10/14] replay: make it a minimal server side command Christian Couder
2023-10-10 12:38         ` [PATCH v5 11/14] replay: use standard revision ranges Christian Couder
2023-10-19 19:49           ` Linus Arver
2023-10-10 12:38         ` [PATCH v5 12/14] replay: add --advance or 'cherry-pick' mode Christian Couder
2023-10-10 12:38         ` [PATCH v5 13/14] replay: add --contained to rebase contained branches Christian Couder
2023-10-10 12:38         ` [PATCH v5 14/14] replay: stop assuming replayed branches do not diverge Christian Couder
2023-10-26 13:44         ` [PATCH v5 00/14] Introduce new `git replay` command Johannes Schindelin
2023-10-29  6:01           ` Elijah Newren
2023-11-02 14:59             ` Christian Couder
2023-11-08 12:25               ` Johannes Schindelin
2023-11-02 15:06           ` Christian Couder
2023-11-08 12:25             ` Johannes Schindelin
2023-10-29  6:00         ` Elijah Newren
2023-10-29 14:14           ` Johannes Schindelin
2023-10-30 17:18             ` Elijah Newren
2023-11-02 14:44               ` Christian Couder
2023-11-02 14:48           ` Christian Couder
2023-11-02 13:51         ` [PATCH v6 " Christian Couder
2023-11-02 13:51           ` [PATCH v6 01/14] t6429: remove switching aspects of fast-rebase Christian Couder
2023-11-02 13:51           ` [PATCH v6 02/14] replay: introduce new builtin Christian Couder
2023-11-02 13:51           ` [PATCH v6 03/14] replay: start using parse_options API Christian Couder
2023-11-02 13:51           ` [PATCH v6 04/14] replay: die() instead of failing assert() Christian Couder
2023-11-02 13:51           ` [PATCH v6 05/14] replay: introduce pick_regular_commit() Christian Couder
2023-11-02 13:51           ` [PATCH v6 06/14] replay: change rev walking options Christian Couder
2023-11-02 13:51           ` [PATCH v6 07/14] replay: add an important FIXME comment about gpg signing Christian Couder
2023-11-02 13:51           ` [PATCH v6 08/14] replay: remove progress and info output Christian Couder
2023-11-02 13:51           ` [PATCH v6 09/14] replay: remove HEAD related sanity check Christian Couder
2023-11-02 13:51           ` [PATCH v6 10/14] replay: make it a minimal server side command Christian Couder
2023-11-02 13:51           ` [PATCH v6 11/14] replay: use standard revision ranges Christian Couder
2023-11-02 13:51           ` [PATCH v6 12/14] replay: add --advance or 'cherry-pick' mode Christian Couder
2023-11-02 13:51           ` [PATCH v6 13/14] replay: add --contained to rebase contained branches Christian Couder
2023-11-02 13:51           ` [PATCH v6 14/14] replay: stop assuming replayed branches do not diverge Christian Couder
2023-11-07  2:43           ` [PATCH v6 00/14] Introduce new `git replay` command Elijah Newren
2023-11-07  9:43             ` Christian Couder
2023-11-15 14:51               ` Christian Couder
2023-11-08 12:19             ` Johannes Schindelin
2023-11-08 12:47           ` Johannes Schindelin
2023-11-15 14:46             ` Christian Couder
2023-11-16  8:45               ` Johannes Schindelin
2023-11-16  8:52                 ` Christian Couder
2023-11-15 14:33           ` [PATCH v7 " Christian Couder
2023-11-15 14:33             ` [PATCH v7 01/14] t6429: remove switching aspects of fast-rebase Christian Couder
2023-11-15 14:33             ` [PATCH v7 02/14] replay: introduce new builtin Christian Couder
2023-11-15 14:33             ` [PATCH v7 03/14] replay: start using parse_options API Christian Couder
2023-11-15 14:33             ` [PATCH v7 04/14] replay: die() instead of failing assert() Christian Couder
2023-11-15 14:33             ` [PATCH v7 05/14] replay: introduce pick_regular_commit() Christian Couder
2023-11-15 14:33             ` [PATCH v7 06/14] replay: change rev walking options Christian Couder
2023-11-15 14:33             ` [PATCH v7 07/14] replay: add an important FIXME comment about gpg signing Christian Couder
2023-11-15 14:33             ` [PATCH v7 08/14] replay: remove progress and info output Christian Couder
2023-11-15 14:33             ` [PATCH v7 09/14] replay: remove HEAD related sanity check Christian Couder
2023-11-15 14:33             ` [PATCH v7 10/14] replay: make it a minimal server side command Christian Couder
2023-11-15 14:33             ` [PATCH v7 11/14] replay: use standard revision ranges Christian Couder
2023-11-15 14:33             ` [PATCH v7 12/14] replay: add --advance or 'cherry-pick' mode Christian Couder
2023-11-15 14:33             ` [PATCH v7 13/14] replay: add --contained to rebase contained branches Christian Couder
2023-11-15 14:33             ` [PATCH v7 14/14] replay: stop assuming replayed branches do not diverge Christian Couder
2023-11-16  8:53             ` [PATCH v7 00/14] Introduce new `git replay` command Johannes Schindelin
2023-11-23 19:32               ` Elijah Newren
2023-11-24  0:28                 ` Junio C Hamano
2023-11-24 11:10             ` [PATCH v8 " Christian Couder
2023-11-24 11:10               ` [PATCH v8 01/14] t6429: remove switching aspects of fast-rebase Christian Couder
2023-11-24 11:10               ` [PATCH v8 02/14] replay: introduce new builtin Christian Couder
2023-11-24 11:10               ` [PATCH v8 03/14] replay: start using parse_options API Christian Couder
2023-11-24 11:10               ` [PATCH v8 04/14] replay: die() instead of failing assert() Christian Couder
2023-11-24 11:10               ` [PATCH v8 05/14] replay: introduce pick_regular_commit() Christian Couder
2023-11-24 11:10               ` [PATCH v8 06/14] replay: change rev walking options Christian Couder
2023-11-24 11:10               ` [PATCH v8 07/14] replay: add an important FIXME comment about gpg signing Christian Couder
2023-11-24 11:10               ` [PATCH v8 08/14] replay: remove progress and info output Christian Couder
2023-11-24 11:10               ` [PATCH v8 09/14] replay: remove HEAD related sanity check Christian Couder
2023-11-24 11:10               ` [PATCH v8 10/14] replay: make it a minimal server side command Christian Couder
2023-11-24 11:10               ` [PATCH v8 11/14] replay: use standard revision ranges Christian Couder
2023-11-24 11:10               ` [PATCH v8 12/14] replay: add --advance or 'cherry-pick' mode Christian Couder
2023-11-24 11:10               ` [PATCH v8 13/14] replay: add --contained to rebase contained branches Christian Couder
2023-11-24 11:10               ` [PATCH v8 14/14] replay: stop assuming replayed branches do not diverge Christian Couder
2023-11-25  0:02               ` [PATCH v8 00/14] Introduce new `git replay` command Johannes Schindelin

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).