* [PATCH RFC 01/11] sequencer: optionally skip printing commit summary
2025-08-19 10:55 [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing Patrick Steinhardt
@ 2025-08-19 10:55 ` Patrick Steinhardt
2025-08-19 10:55 ` [PATCH RFC 02/11] sequencer: add option to rewind HEAD after picking commits Patrick Steinhardt
` (19 subsequent siblings)
20 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-19 10:55 UTC (permalink / raw)
To: git
When picking commits by using for example git-cherry-pick(1) we end up
printing a commit summary that gives the reader information around what
exactly we have been picking:
```
$ git cherry-pick main
[other 76c8456] bar
Date: Tue Aug 19 08:07:26 2025 +0200
1 file changed, 1 insertion(+)
create mode 100644 bar
```
While useful for some commands, we're about to introduce a new command
where this output will be less so. But right now there is no way to
disable printing this commit summary.
Introduce a new `skip_commit_summary` replay option that does so.
Persist the option into the sequencer configuration so that it persists
across different processes, e.g. when we need to stop due to a merge
conflict.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
sequencer.c | 12 +++++++++---
sequencer.h | 1 +
2 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/sequencer.c b/sequencer.c
index aaf2e4df64..7066cdc939 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -1742,7 +1742,7 @@ static int do_commit(struct repository *r,
refs_delete_ref(get_main_ref_store(r), "",
"CHERRY_PICK_HEAD", NULL, REF_NO_DEREF);
unlink(git_path_merge_msg(r));
- if (!is_rebase_i(opts))
+ if (!is_rebase_i(opts) && !opts->skip_commit_summary)
print_commit_summary(r, NULL, &oid,
SUMMARY_SHOW_AUTHOR_DATE);
return res;
@@ -3139,8 +3139,12 @@ static int populate_opts_cb(const char *key, const char *value,
else if (!strcmp(key, "options.default-msg-cleanup")) {
opts->explicit_cleanup = 1;
opts->default_msg_cleanup = get_cleanup_mode(value, 1);
- } else
+ } else if (!strcmp(key, "options.skip-commit-summary")) {
+ opts->skip_commit_summary =
+ git_config_bool_or_int(key, value, ctx->kvi, &error_flag);
+ } else {
return error(_("invalid key: %s"), key);
+ }
if (!error_flag)
return error(_("invalid value for '%s': '%s'"), key, value);
@@ -3698,11 +3702,13 @@ static int save_opts(struct replay_opts *opts)
"options.allow-rerere-auto", NULL,
opts->allow_rerere_auto == RERERE_AUTOUPDATE ?
"true" : "false");
-
if (opts->explicit_cleanup)
res |= repo_config_set_in_file_gently(the_repository, opts_file,
"options.default-msg-cleanup", NULL,
describe_cleanup_mode(opts->default_msg_cleanup));
+ if (opts->skip_commit_summary)
+ res |= repo_config_set_in_file_gently(the_repository, opts_file,
+ "options.skip-commit-summary", NULL, "true");
return res;
}
diff --git a/sequencer.h b/sequencer.h
index 304ba4b4d3..1767fd737e 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -52,6 +52,7 @@ struct replay_opts {
int keep_redundant_commits;
int verbose;
int quiet;
+ int skip_commit_summary;
int reschedule_failed_exec;
int committer_date_is_author_date;
int ignore_date;
--
2.51.0.261.g7ce5a0a67e.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH RFC 02/11] sequencer: add option to rewind HEAD after picking commits
2025-08-19 10:55 [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing Patrick Steinhardt
2025-08-19 10:55 ` [PATCH RFC 01/11] sequencer: optionally skip printing commit summary Patrick Steinhardt
@ 2025-08-19 10:55 ` Patrick Steinhardt
2025-08-19 10:55 ` [PATCH RFC 03/11] cache-tree: allow writing in-memory index as tree Patrick Steinhardt
` (18 subsequent siblings)
20 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-19 10:55 UTC (permalink / raw)
To: git
While the sequencer infrastructure knows to rewind "HEAD" to whatever it
was pointing to before a rebase, it doesn't do the same for non-rebase
operations like cherry-picks. This is because the expectation is that
the user directly picks commits on top of whatever "HEAD" points to, and
we advance the reference pointed to by "HEAD" instead of updating it
directly.
We're about to introduce a new command though that needs to detach
"HEAD" while being more similar to git-cherry-pick(1) rathen than to
git-rebase(1). As such, we'll want to restore "HEAD" to point to the
branch that we started on while not using the more heavy-weight rebase
machinery.
Introduce a new option `restore_head_target` to do so. Persist the
option into the sequencer configuration so that it persists across
different processes, e.g. when we need to stop due to a merge conflict.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
sequencer.c | 24 ++++++++++++++++++++++++
sequencer.h | 3 +++
2 files changed, 27 insertions(+)
diff --git a/sequencer.c b/sequencer.c
index 7066cdc939..b13348ba34 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -413,6 +413,7 @@ void replay_opts_release(struct replay_opts *opts)
struct replay_ctx *ctx = opts->ctx;
free(opts->gpg_sign);
+ free(opts->restore_head_target);
free(opts->reflog_action);
free(opts->default_strategy);
free(opts->strategy);
@@ -3142,6 +3143,8 @@ static int populate_opts_cb(const char *key, const char *value,
} else if (!strcmp(key, "options.skip-commit-summary")) {
opts->skip_commit_summary =
git_config_bool_or_int(key, value, ctx->kvi, &error_flag);
+ } else if (!strcmp(key, "options.restore-head-target")) {
+ git_config_string_dup(&opts->restore_head_target, key, value);
} else {
return error(_("invalid key: %s"), key);
}
@@ -3709,6 +3712,10 @@ static int save_opts(struct replay_opts *opts)
if (opts->skip_commit_summary)
res |= repo_config_set_in_file_gently(the_repository, opts_file,
"options.skip-commit-summary", NULL, "true");
+ if (opts->restore_head_target)
+ res |= repo_config_set_in_file_gently(the_repository, opts_file,
+ "options.restore-head-target", NULL, opts->restore_head_target);
+
return res;
}
@@ -5177,6 +5184,23 @@ static int pick_commits(struct repository *r,
return -1;
}
+ if (opts->restore_head_target) {
+ struct reset_head_opts reset_opts = { 0 };
+ const char *msg;
+
+ msg = reflog_message(opts, "finish", "returning to %s", opts->restore_head_target);
+
+ reset_opts.branch = opts->restore_head_target;
+ reset_opts.flags = RESET_HEAD_REFS_ONLY;
+ reset_opts.branch_msg = msg;
+ reset_opts.head_msg = msg;
+
+ if (reset_head(r, &reset_opts)) {
+ error(_("could not switch HEAD back to %s"), opts->restore_head_target);
+ return -1;
+ }
+ }
+
/*
* Sequence of picks finished successfully; cleanup by
* removing the .git/sequencer directory
diff --git a/sequencer.h b/sequencer.h
index 1767fd737e..a905f6afc7 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -72,6 +72,9 @@ struct replay_opts {
/* Reflog */
char *reflog_action;
+ /* Reference to which HEAD shall be reset to after the operation. */
+ char *restore_head_target;
+
/* placeholder commit for -i --root */
struct object_id squash_onto;
int have_squash_onto;
--
2.51.0.261.g7ce5a0a67e.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH RFC 03/11] cache-tree: allow writing in-memory index as tree
2025-08-19 10:55 [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing Patrick Steinhardt
2025-08-19 10:55 ` [PATCH RFC 01/11] sequencer: optionally skip printing commit summary Patrick Steinhardt
2025-08-19 10:55 ` [PATCH RFC 02/11] sequencer: add option to rewind HEAD after picking commits Patrick Steinhardt
@ 2025-08-19 10:55 ` Patrick Steinhardt
2025-08-19 10:56 ` [PATCH RFC 04/11] builtin: add new "history" command Patrick Steinhardt
` (17 subsequent siblings)
20 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-19 10:55 UTC (permalink / raw)
To: git
The function `write_in_core_index_as_tree()` takes a repository and
writes its index into a tree object. What this function cannot do though
is to take an _arbitrary_ in-memory index.
Introduce a new `struct index_state` parameter so that the caller can
pass a different index than the one belonging to the repository. This
will be used in a subsequent commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
builtin/checkout.c | 3 ++-
cache-tree.c | 5 ++---
cache-tree.h | 3 ++-
3 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/builtin/checkout.c b/builtin/checkout.c
index f9453473fe2..43583c8d1be 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -902,7 +902,8 @@ static int merge_working_tree(const struct checkout_opts *opts,
0);
init_ui_merge_options(&o, the_repository);
o.verbosity = 0;
- work = write_in_core_index_as_tree(the_repository);
+ work = write_in_core_index_as_tree(the_repository,
+ the_repository->index);
ret = reset_tree(new_tree,
opts, 1,
diff --git a/cache-tree.c b/cache-tree.c
index 66ef2becbe0..029ec933abe 100644
--- a/cache-tree.c
+++ b/cache-tree.c
@@ -699,11 +699,11 @@ static int write_index_as_tree_internal(struct object_id *oid,
return 0;
}
-struct tree* write_in_core_index_as_tree(struct repository *repo) {
+struct tree *write_in_core_index_as_tree(struct repository *repo,
+ struct index_state *index_state) {
struct object_id o;
int was_valid, ret;
- struct index_state *index_state = repo->index;
was_valid = index_state->cache_tree &&
cache_tree_fully_valid(index_state->cache_tree);
@@ -723,7 +723,6 @@ struct tree* write_in_core_index_as_tree(struct repository *repo) {
return lookup_tree(repo, &index_state->cache_tree->oid);
}
-
int write_index_as_tree(struct object_id *oid, struct index_state *index_state, const char *index_path, int flags, const char *prefix)
{
int entries, was_valid;
diff --git a/cache-tree.h b/cache-tree.h
index b82c4963e7c..f8bddae5235 100644
--- a/cache-tree.h
+++ b/cache-tree.h
@@ -47,7 +47,8 @@ int cache_tree_verify(struct repository *, struct index_state *);
#define WRITE_TREE_UNMERGED_INDEX (-2)
#define WRITE_TREE_PREFIX_ERROR (-3)
-struct tree* write_in_core_index_as_tree(struct repository *repo);
+struct tree *write_in_core_index_as_tree(struct repository *repo,
+ struct index_state *index_state);
int write_index_as_tree(struct object_id *oid, struct index_state *index_state, const char *index_path, int flags, const char *prefix);
void prime_cache_tree(struct repository *, struct index_state *, struct tree *);
--
2.51.0.261.g7ce5a0a67e.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH RFC 04/11] builtin: add new "history" command
2025-08-19 10:55 [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing Patrick Steinhardt
` (2 preceding siblings ...)
2025-08-19 10:55 ` [PATCH RFC 03/11] cache-tree: allow writing in-memory index as tree Patrick Steinhardt
@ 2025-08-19 10:56 ` Patrick Steinhardt
2025-08-19 10:56 ` [PATCH RFC 05/11] builtin/history: implement "drop" subcommand Patrick Steinhardt
` (16 subsequent siblings)
20 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-19 10:56 UTC (permalink / raw)
To: git
When rewriting history via git-rebase(1) there are a couple of very
common use cases:
- The ordering of two commits should be reversed.
- A commit should be split up into two commits.
- A commit should be dropped from the history completely.
- Multiple commits should be squashed into one.
While these operations are all doable, it often feels needlessly cludgy
to do so by doing an interactive rebase, using the editor to say what
one wants, and then perform the actions. Furthermore, some operations
like splitting up a commit into two are way more involved than that and
require a whole series of commands.
Add a new "history" command to plug this gap. This command will have
several different subcommands to imperatively rewrite history for common
use cases like the above. These commands will be implemented in
subsequent commits.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
.gitignore | 1 +
Documentation/git-history.adoc | 43 ++++++++++++++++++++++++++++++++++++++++++
Documentation/meson.build | 1 +
Makefile | 1 +
builtin.h | 1 +
builtin/history.c | 20 ++++++++++++++++++++
git.c | 1 +
meson.build | 1 +
8 files changed, 69 insertions(+)
diff --git a/.gitignore b/.gitignore
index 04c444404e..3932d4d618 100644
--- a/.gitignore
+++ b/.gitignore
@@ -77,6 +77,7 @@
/git-grep
/git-hash-object
/git-help
+/git-history
/git-hook
/git-http-backend
/git-http-fetch
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
new file mode 100644
index 0000000000..9dafb8fc16
--- /dev/null
+++ b/Documentation/git-history.adoc
@@ -0,0 +1,43 @@
+git-history(1)
+==============
+
+NAME
+----
+git-history - Rewrite history of the current branch
+
+SYNOPSIS
+--------
+[synopsis]
+git history [<options>]
+
+DESCRIPTION
+-----------
+
+Rewrite history by rearranging or modifying specific commits in the
+history.
+
+This command is similar to linkgit:git-rebase[1] and uses the same
+underlying machinery. You should use rebases if you either want to
+reapply a range of commits onto a different base, or interactive rebases
+if you want to edit a range of commits.
+
+Note that this command does not (yet) work with histories that contain
+merges. You should use linkgit:git-rebase[1] with the `--rebase-merges`
+flag instead.
+
+COMMANDS
+--------
+
+This command requires a subcommand. Several subcommands are available to
+rewrite history in different ways.
+
+CONFIGURATION
+-------------
+
+include::includes/cmd-config-section-all.adoc[]
+
+include::config/sequencer.adoc[]
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Documentation/meson.build b/Documentation/meson.build
index 4404c623f0..a30b5307fd 100644
--- a/Documentation/meson.build
+++ b/Documentation/meson.build
@@ -64,6 +64,7 @@ manpages = {
'git-gui.adoc' : 1,
'git-hash-object.adoc' : 1,
'git-help.adoc' : 1,
+ 'git-history.adoc' : 1,
'git-hook.adoc' : 1,
'git-http-backend.adoc' : 1,
'git-http-fetch.adoc' : 1,
diff --git a/Makefile b/Makefile
index e11340c1ae..bed6eda5e6 100644
--- a/Makefile
+++ b/Makefile
@@ -1261,6 +1261,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o
BUILTIN_OBJS += builtin/grep.o
BUILTIN_OBJS += builtin/hash-object.o
BUILTIN_OBJS += builtin/help.o
+BUILTIN_OBJS += builtin/history.o
BUILTIN_OBJS += builtin/hook.o
BUILTIN_OBJS += builtin/index-pack.o
BUILTIN_OBJS += builtin/init-db.o
diff --git a/builtin.h b/builtin.h
index bff13e3069..2934f4479a 100644
--- a/builtin.h
+++ b/builtin.h
@@ -172,6 +172,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix, struc
int cmd_grep(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_hash_object(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_help(int argc, const char **argv, const char *prefix, struct repository *repo);
+int cmd_history(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_hook(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_index_pack(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_init_db(int argc, const char **argv, const char *prefix, struct repository *repo);
diff --git a/builtin/history.c b/builtin/history.c
new file mode 100644
index 0000000000..d1a40368e0
--- /dev/null
+++ b/builtin/history.c
@@ -0,0 +1,20 @@
+#include "builtin.h"
+#include "gettext.h"
+#include "parse-options.h"
+
+int cmd_history(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo UNUSED)
+{
+ const char * const usage[] = {
+ N_("git history [<options>]"),
+ NULL,
+ };
+ struct option options[] = {
+ OPT_END(),
+ };
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ return 0;
+}
diff --git a/git.c b/git.c
index 83eac0aeab..9d2cba2906 100644
--- a/git.c
+++ b/git.c
@@ -560,6 +560,7 @@ static struct cmd_struct commands[] = {
{ "grep", cmd_grep, RUN_SETUP_GENTLY },
{ "hash-object", cmd_hash_object },
{ "help", cmd_help },
+ { "history", cmd_history, RUN_SETUP },
{ "hook", cmd_hook, RUN_SETUP },
{ "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },
{ "init", cmd_init_db },
diff --git a/meson.build b/meson.build
index 5dd299b496..0e40778a23 100644
--- a/meson.build
+++ b/meson.build
@@ -603,6 +603,7 @@ builtin_sources = [
'builtin/grep.c',
'builtin/hash-object.c',
'builtin/help.c',
+ 'builtin/history.c',
'builtin/hook.c',
'builtin/index-pack.c',
'builtin/init-db.c',
--
2.51.0.261.g7ce5a0a67e.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH RFC 05/11] builtin/history: implement "drop" subcommand
2025-08-19 10:55 [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing Patrick Steinhardt
` (3 preceding siblings ...)
2025-08-19 10:56 ` [PATCH RFC 04/11] builtin: add new "history" command Patrick Steinhardt
@ 2025-08-19 10:56 ` Patrick Steinhardt
2025-08-20 20:39 ` Ben Knoble
2025-08-23 16:15 ` Jean-Noël AVILA
2025-08-19 10:56 ` [PATCH RFC 06/11] builtin/history: implement "reorder" subcommand Patrick Steinhardt
` (15 subsequent siblings)
20 siblings, 2 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-19 10:56 UTC (permalink / raw)
To: git
It is a fairly common operation to perform an interactive rebase so that
one of the commits can be dropped from history. Doing this is not very
hard in general, but still requires the user to perform multiple steps:
1. Identify the commit in question that is to be dropped.
2. Perform an interactive rebase on top of that commit's parent.
3. Edit the instruction sheet to drop that commit.
This is needlessly complex for such a supposedly-trivial operation.
Furthermore, the second step doesn't account for certain edge cases like
for example dropping the root commit.
Introduce a new "drop" subcommand to make this use case significantly
simpler: all the user needs to do is to say `git history drop $COMMIT`
and they're done.
Note that for now, this command only allows users to drop a single
commit at once. It should be easy enough though to expand the command at
a later point in time to support dropping whole commit ranges.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-history.adoc | 27 +++-
builtin/history.c | 297 ++++++++++++++++++++++++++++++++++++++++-
t/meson.build | 3 +-
t/t3450-history-drop.sh | 127 ++++++++++++++++++
4 files changed, 449 insertions(+), 5 deletions(-)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index 9dafb8fc16..3012445ddc 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -8,7 +8,7 @@ git-history - Rewrite history of the current branch
SYNOPSIS
--------
[synopsis]
-git history [<options>]
+git history drop [<options>] <revision>
DESCRIPTION
-----------
@@ -31,6 +31,31 @@ COMMANDS
This command requires a subcommand. Several subcommands are available to
rewrite history in different ways.
+drop <revision>::
+ Drop a commit from the history and reapply all children of that
+ commit on top of the commit's parent. The commit that is to be
+ dropped must be reachable from the current `HEAD` commit.
++
+Dropping the root commit converts the child of that commit into the new
+root commit. It is invalid to drop a root commit that does not have any
+child commits, as that would lead to an empty branch.
+
+EXAMPLES
+--------
+
+* Drop a commit from history.
++
+----------
+$ git log --oneline
+2d4cd6d third
+125a0f3 second
+e098c27 first
+$ git history drop HEAD~
+$ git log
+b1bc1bd third
+e098c27 first
+----------
+
CONFIGURATION
-------------
diff --git a/builtin/history.c b/builtin/history.c
index d1a40368e0..183ab9d5f7 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -1,20 +1,311 @@
#include "builtin.h"
+#include "commit.h"
+#include "commit-reach.h"
+#include "config.h"
+#include "environment.h"
#include "gettext.h"
+#include "hex.h"
+#include "object-name.h"
#include "parse-options.h"
+#include "refs.h"
+#include "reset.h"
+#include "revision.h"
+#include "sequencer.h"
+
+static int collect_commits(struct repository *repo,
+ struct commit *old_commit,
+ struct commit *new_commit,
+ struct strvec *out)
+{
+ struct setup_revision_opt revision_opts = {
+ .assume_dashdash = 1,
+ };
+ struct strvec revisions = STRVEC_INIT;
+ struct commit_list *from_list = NULL;
+ struct commit *child;
+ struct rev_info rev = { 0 };
+ int ret;
+
+ /*
+ * Check that the old actually is an ancestor of HEAD. If not
+ * the whole request becomes nonsensical.
+ */
+ if (old_commit) {
+ commit_list_insert(old_commit, &from_list);
+ if (!repo_is_descendant_of(repo, new_commit, from_list)) {
+ ret = error(_("commit must be reachable from current HEAD commit"));
+ goto out;
+ }
+ }
+
+ repo_init_revisions(repo, &rev, NULL);
+ strvec_push(&revisions, "");
+ strvec_push(&revisions, oid_to_hex(&new_commit->object.oid));
+ if (old_commit)
+ strvec_pushf(&revisions, "^%s", oid_to_hex(&old_commit->object.oid));
+ if (setup_revisions(revisions.nr, revisions.v, &rev, &revision_opts) != 1 ||
+ prepare_revision_walk(&rev)) {
+ ret = error(_("revision walk setup failed"));
+ goto out;
+ }
+
+ while ((child = get_revision(&rev))) {
+ if (old_commit && !child->parents)
+ BUG("revision walk did not find child commit");
+ if (child->parents && child->parents->next) {
+ ret = error(_("cannot rearrange commit history with merges"));
+ goto out;
+ }
+
+ strvec_push(out, oid_to_hex(&child->object.oid));
+
+ if (child->parents && old_commit &&
+ commit_list_contains(old_commit, child->parents))
+ break;
+ }
+
+ /*
+ * Revisions are in newest-order-first. We have to reverse the
+ * array though so that we pick the oldest commits first. Note
+ * that we keep the first string untouched, as it is the
+ * equivalent of `argv[0]` to `setup_revisions()`.
+ */
+ for (size_t i = 0, j = out->nr - 1; i < j; i++, j--)
+ SWAP(out->v[i], out->v[j]);
+
+ ret = 0;
+
+out:
+ free_commit_list(from_list);
+ strvec_clear(&revisions);
+ release_revisions(&rev);
+ reset_revision_walk();
+ return ret;
+}
+
+static int apply_commits(struct repository *repo,
+ const struct strvec *commits,
+ struct commit *head,
+ struct commit *base,
+ const char *action)
+{
+ struct setup_revision_opt revision_opts = {
+ .assume_dashdash = 1,
+ };
+ struct replay_opts replay_opts = REPLAY_OPTS_INIT;
+ struct reset_head_opts reset_opts = { 0 };
+ struct object_id root_commit;
+ struct strvec args = STRVEC_INIT;
+ struct strbuf buf = STRBUF_INIT;
+ char hex[GIT_MAX_HEXSZ + 1];
+ int ref_flags, ret;
+
+ /*
+ * We have performed all safety checks, so we now prepare
+ * replaying the commits.
+ */
+ replay_opts.action = REPLAY_PICK;
+ sequencer_init_config(&replay_opts);
+ replay_opts.quiet = 1;
+ replay_opts.skip_commit_summary = 1;
+ if (!replay_opts.strategy && replay_opts.default_strategy) {
+ replay_opts.strategy = replay_opts.default_strategy;
+ replay_opts.default_strategy = NULL;
+ }
+
+ strvec_push(&args, "");
+ strvec_pushv(&args, commits->v);
+
+ replay_opts.revs = xmalloc(sizeof(*replay_opts.revs));
+ repo_init_revisions(repo, replay_opts.revs, NULL);
+ replay_opts.revs->no_walk = 1;
+ replay_opts.revs->unsorted_input = 1;
+ if (setup_revisions(args.nr, args.v, replay_opts.revs,
+ &revision_opts) != 1) {
+ ret = error(_("setting up revisions failed"));
+ goto out;
+ }
+
+ /*
+ * If we're dropping the root commit we first need to create
+ * a new empty root. We then instruct the seqencer machinery to
+ * squash that root commit with the first commit we're picking
+ * onto it.
+ */
+ if (!base) {
+ if (commit_tree("", 0, repo->hash_algo->empty_tree, NULL,
+ &root_commit, NULL, NULL) < 0) {
+ ret = error(_("Could not create new root commit"));
+ goto out;
+ }
+
+ replay_opts.squash_onto = root_commit;
+ replay_opts.have_squash_onto = 1;
+ reset_opts.oid = &root_commit;
+ } else {
+ reset_opts.oid = &base->object.oid;
+ }
+
+ replay_opts.restore_head_target =
+ xstrdup_or_null(refs_resolve_ref_unsafe(get_main_ref_store(repo),
+ "HEAD", 0, NULL, &ref_flags));
+ if (!(ref_flags & REF_ISSYMREF))
+ FREE_AND_NULL(replay_opts.restore_head_target);
+
+ /*
+ * Perform a hard-reset to the parent of our commit that is to
+ * be dropped. This is the new base onto which we'll pick all
+ * the descendants.
+ */
+ strbuf_addf(&buf, "%s (start): checkout %s", action,
+ oid_to_hex_r(hex, reset_opts.oid));
+ reset_opts.orig_head = &head->object.oid;
+ reset_opts.flags = RESET_HEAD_DETACH | RESET_ORIG_HEAD;
+ reset_opts.head_msg = buf.buf;
+ reset_opts.default_reflog_action = action;
+ if (reset_head(repo, &reset_opts) < 0) {
+ ret = error(_("could not switch to %s"), oid_to_hex(reset_opts.oid));
+ goto out;
+ }
+
+ ret = sequencer_pick_revisions(repo, &replay_opts);
+ if (ret < 0) {
+ ret = error(_("could not pick commits"));
+ goto out;
+ } else if (ret > 0) {
+ /*
+ * A positive return value indicates we've got a merge
+ * conflict. Bail out, but don't print a message as
+ * `sequencer_pick_revisions()` already printed enough
+ * information.
+ */
+ ret = -1;
+ goto out;
+ }
+
+ ret = 0;
+
+out:
+ replay_opts_release(&replay_opts);
+ strbuf_release(&buf);
+ strvec_clear(&args);
+ return ret;
+}
+
+static int cmd_history_drop(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo)
+{
+ const char * const usage[] = {
+ N_("git history drop [<options>] <revision>"),
+ NULL,
+ };
+ struct option options[] = {
+ OPT_END(),
+ };
+ struct commit *commit_to_drop, *head;
+ struct strvec commits = STRVEC_INIT;
+ struct strbuf buf = STRBUF_INIT;
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc != 1) {
+ ret = error(_("command expects a single revision"));
+ goto out;
+ }
+ repo_config(repo, git_default_config, NULL);
+
+ commit_to_drop = lookup_commit_reference_by_name(argv[0]);
+ if (!commit_to_drop) {
+ ret = error(_("commit to be dropped cannot be found: %s"), argv[0]);
+ goto out;
+ }
+ if (commit_to_drop->parents && commit_to_drop->parents->next) {
+ ret = error(_("commit to be dropped must not be a merge commit"));
+ goto out;
+ }
+
+ head = lookup_commit_reference_by_name("HEAD");
+ if (!head) {
+ ret = error(_("could not resolve HEAD to a commit"));
+ goto out;
+ }
+
+ if (oideq(&commit_to_drop->object.oid, &head->object.oid)) {
+ /*
+ * If we want to drop the tip of the current branch we don't
+ * have to perform any rebase at all. Instead, we simply
+ * perform a hard reset to the parent commit.
+ */
+ struct reset_head_opts reset_opts = {
+ .orig_head = &head->object.oid,
+ .flags = RESET_ORIG_HEAD,
+ .default_reflog_action = "drop",
+ };
+ char hex[GIT_MAX_HEXSZ + 1];
+
+ if (!commit_to_drop->parents) {
+ ret = error(_("cannot drop the only commit on this branch"));
+ goto out;
+ }
+
+ oid_to_hex_r(hex, &commit_to_drop->parents->item->object.oid);
+ strbuf_addf(&buf, "drop (start): checkout %s", hex);
+ reset_opts.oid = &commit_to_drop->parents->item->object.oid;
+ reset_opts.head_msg = buf.buf;
+
+ if (reset_head(repo, &reset_opts) < 0) {
+ ret = error(_("could not switch to %s"), hex);
+ goto out;
+ }
+ } else {
+ /*
+ * Prepare a revision walk from old commit to the commit that is
+ * about to be dropped. This serves three purposes:
+ *
+ * - We verify that the history doesn't contain any merges.
+ * For now, merges aren't yet handled by us.
+ *
+ * - We need to find the child of the commit-to-be-dropped.
+ * This child is what will be adopted by the parent of the
+ * commit that we are about to drop.
+ *
+ * - We compute the list of commits-to-be-picked.
+ */
+ ret = collect_commits(repo, commit_to_drop, head, &commits);
+ if (ret < 0)
+ goto out;
+
+ ret = apply_commits(repo, &commits, head, commit_to_drop->parents ?
+ commit_to_drop->parents->item : NULL, "drop");
+ if (ret < 0)
+ goto out;
+ }
+
+ ret = 0;
+
+out:
+ strvec_clear(&commits);
+ strbuf_release(&buf);
+ return ret;
+}
int cmd_history(int argc,
const char **argv,
const char *prefix,
- struct repository *repo UNUSED)
+ struct repository *repo)
{
const char * const usage[] = {
- N_("git history [<options>]"),
+ N_("git history drop [<options>] <revision>"),
NULL,
};
+ parse_opt_subcommand_fn *fn = NULL;
struct option options[] = {
+ OPT_SUBCOMMAND("drop", &fn, cmd_history_drop),
OPT_END(),
};
argc = parse_options(argc, argv, prefix, options, usage, 0);
- return 0;
+ return fn(argc, argv, prefix, repo);
}
diff --git a/t/meson.build b/t/meson.build
index bbeba1a8d5..859c388987 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -376,6 +376,7 @@ integration_tests = [
't3436-rebase-more-options.sh',
't3437-rebase-fixup-options.sh',
't3438-rebase-broken-files.sh',
+ 't3450-history-drop.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
@@ -1214,4 +1215,4 @@ if perl.found() and time.found()
timeout: 0,
)
endforeach
-endif
\ No newline at end of file
+endif
diff --git a/t/t3450-history-drop.sh b/t/t3450-history-drop.sh
new file mode 100755
index 0000000000..4782144da0
--- /dev/null
+++ b/t/t3450-history-drop.sh
@@ -0,0 +1,127 @@
+#!/bin/sh
+
+test_description='tests for git-history drop subcommand'
+
+. ./test-lib.sh
+
+test_expect_success 'refuses to work with merge commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ git branch branch &&
+ test_commit ours &&
+ git switch branch &&
+ test_commit theirs &&
+ git switch - &&
+ git merge theirs &&
+ test_must_fail git history drop HEAD 2>err &&
+ test_grep "commit to be dropped must not be a merge commit" err &&
+ test_must_fail git history drop HEAD~ 2>err &&
+ test_grep "cannot rearrange commit history with merges" err
+ )
+'
+
+test_expect_success 'refuses to work when history becomes empty' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ test_must_fail git history drop HEAD 2>err &&
+ test_grep "cannot drop the only commit on this branch" err
+ )
+'
+
+test_expect_success 'can drop tip of a branch' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ git symbolic-ref HEAD >expect &&
+ git history drop HEAD &&
+ git symbolic-ref HEAD >actual &&
+ test_cmp expect actual &&
+
+ cat >expect <<-EOF &&
+ second
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'can drop commit in the middle' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ test_commit fourth &&
+ test_commit fifth &&
+
+ git symbolic-ref HEAD >expect &&
+ git history drop HEAD~2 &&
+ git symbolic-ref HEAD >actual &&
+ test_cmp expect actual &&
+
+ cat >expect <<-EOF &&
+ fifth
+ fourth
+ second
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'correct order is retained' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ test_commit fourth &&
+ test_commit fifth &&
+ git history drop HEAD~3 &&
+ cat >expect <<-EOF &&
+ fifth
+ fourth
+ third
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'can drop root commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ git history drop HEAD~2 &&
+ cat >expect <<-EOF &&
+ third
+ second
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_done
--
2.51.0.261.g7ce5a0a67e.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH RFC 05/11] builtin/history: implement "drop" subcommand
2025-08-19 10:56 ` [PATCH RFC 05/11] builtin/history: implement "drop" subcommand Patrick Steinhardt
@ 2025-08-20 20:39 ` Ben Knoble
2025-08-22 12:21 ` Patrick Steinhardt
2025-08-23 16:15 ` Jean-Noël AVILA
1 sibling, 1 reply; 278+ messages in thread
From: Ben Knoble @ 2025-08-20 20:39 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git
> Le 19 août 2025 à 06:57, Patrick Steinhardt <ps@pks.im> a écrit :
>
> It is a fairly common operation to perform an interactive rebase so that
> one of the commits can be dropped from history.
> diff --git a/builtin/history.c b/builtin/history.c
> index d1a40368e0..183ab9d5f7 100644
> --- a/builtin/history.c
> +++ b/builtin/history.c
> @@ -1,20 +1,311 @@
> #include "builtin.h"
> +#include "commit.h"
> +#include "commit-reach.h"
> +#include "config.h"
> +#include "environment.h"
> #include "gettext.h"
> +#include "hex.h"
> +#include "object-name.h"
> #include "parse-options.h"
> +#include "refs.h"
> +#include "reset.h"
> +#include "revision.h"
> +#include "sequencer.h"
> +
> +static int collect_commits(struct repository *repo,
> + struct commit *old_commit,
> + struct commit *new_commit,
> + struct strvec *out)
> +{
> + struct setup_revision_opt revision_opts = {
> + .assume_dashdash = 1,
> + };
> + struct strvec revisions = STRVEC_INIT;
> + struct commit_list *from_list = NULL;
> + struct commit *child;
> + struct rev_info rev = { 0 };
> + int ret;
> +
> + /*
> + * Check that the old actually is an ancestor of HEAD. If not
The “old commit” perhaps?
> + * the whole request becomes nonsensical.
> + */
> + if (old_commit) {
> + commit_list_insert(old_commit, &from_list);
> + if (!repo_is_descendant_of(repo, new_commit, from_list)) {
> + ret = error(_("commit must be reachable from current HEAD commit"));
> + goto out;
> + }
> + }
> +
> + repo_init_revisions(repo, &rev, NULL);
> + strvec_push(&revisions, "");
> + strvec_push(&revisions, oid_to_hex(&new_commit->object.oid));
> + if (old_commit)
> + strvec_pushf(&revisions, "^%s", oid_to_hex(&old_commit->object.oid));
> + if (setup_revisions(revisions.nr, revisions.v, &rev, &revision_opts) != 1 ||
> + prepare_revision_walk(&rev)) {
> + ret = error(_("revision walk setup failed"));
> + goto out;
> + }
> +
> + while ((child = get_revision(&rev))) {
> + if (old_commit && !child->parents)
> + BUG("revision walk did not find child commit");
> + if (child->parents && child->parents->next) {
> + ret = error(_("cannot rearrange commit history with merges"));
> + goto out;
> + }
> +
> + strvec_push(out, oid_to_hex(&child->object.oid));
> +
> + if (child->parents && old_commit &&
> + commit_list_contains(old_commit, child->parents))
> + break;
> + }
> +
> + /*
> + * Revisions are in newest-order-first. We have to reverse the
> + * array though so that we pick the oldest commits first. Note
> + * that we keep the first string untouched, as it is the
> + * equivalent of `argv[0]` to `setup_revisions()`.
> + */
> + for (size_t i = 0, j = out->nr - 1; i < j; i++, j--)
> + SWAP(out->v[i], out->v[j]);
> +
But doesn’t this swap out->v[0] on first iteration? I only skimmed the code that built it up, but it doesn’t look the comment is right 🤔
Rest looked reasonable, but I don’t know the sequencer APIs very well.
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC 05/11] builtin/history: implement "drop" subcommand
2025-08-20 20:39 ` Ben Knoble
@ 2025-08-22 12:21 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-22 12:21 UTC (permalink / raw)
To: Ben Knoble; +Cc: git
On Wed, Aug 20, 2025 at 04:39:34PM -0400, Ben Knoble wrote:
> > diff --git a/builtin/history.c b/builtin/history.c
> > index d1a40368e0..183ab9d5f7 100644
> > --- a/builtin/history.c
> > +++ b/builtin/history.c
> > @@ -1,20 +1,311 @@
> > #include "builtin.h"
> > +#include "commit.h"
> > +#include "commit-reach.h"
> > +#include "config.h"
> > +#include "environment.h"
> > #include "gettext.h"
> > +#include "hex.h"
> > +#include "object-name.h"
> > #include "parse-options.h"
> > +#include "refs.h"
> > +#include "reset.h"
> > +#include "revision.h"
> > +#include "sequencer.h"
> > +
> > +static int collect_commits(struct repository *repo,
> > + struct commit *old_commit,
> > + struct commit *new_commit,
> > + struct strvec *out)
> > +{
> > + struct setup_revision_opt revision_opts = {
> > + .assume_dashdash = 1,
> > + };
> > + struct strvec revisions = STRVEC_INIT;
> > + struct commit_list *from_list = NULL;
> > + struct commit *child;
> > + struct rev_info rev = { 0 };
> > + int ret;
> > +
> > + /*
> > + * Check that the old actually is an ancestor of HEAD. If not
>
> The “old commit” perhaps?
Yup, indeed.
> > + * the whole request becomes nonsensical.
> > + */
> > + if (old_commit) {
> > + commit_list_insert(old_commit, &from_list);
> > + if (!repo_is_descendant_of(repo, new_commit, from_list)) {
> > + ret = error(_("commit must be reachable from current HEAD commit"));
> > + goto out;
> > + }
> > + }
> > +
> > + repo_init_revisions(repo, &rev, NULL);
> > + strvec_push(&revisions, "");
> > + strvec_push(&revisions, oid_to_hex(&new_commit->object.oid));
> > + if (old_commit)
> > + strvec_pushf(&revisions, "^%s", oid_to_hex(&old_commit->object.oid));
> > + if (setup_revisions(revisions.nr, revisions.v, &rev, &revision_opts) != 1 ||
> > + prepare_revision_walk(&rev)) {
> > + ret = error(_("revision walk setup failed"));
> > + goto out;
> > + }
> > +
> > + while ((child = get_revision(&rev))) {
> > + if (old_commit && !child->parents)
> > + BUG("revision walk did not find child commit");
> > + if (child->parents && child->parents->next) {
> > + ret = error(_("cannot rearrange commit history with merges"));
> > + goto out;
> > + }
> > +
> > + strvec_push(out, oid_to_hex(&child->object.oid));
> > +
> > + if (child->parents && old_commit &&
> > + commit_list_contains(old_commit, child->parents))
> > + break;
> > + }
> > +
> > + /*
> > + * Revisions are in newest-order-first. We have to reverse the
> > + * array though so that we pick the oldest commits first. Note
> > + * that we keep the first string untouched, as it is the
> > + * equivalent of `argv[0]` to `setup_revisions()`.
> > + */
> > + for (size_t i = 0, j = out->nr - 1; i < j; i++, j--)
> > + SWAP(out->v[i], out->v[j]);
> > +
>
> But doesn’t this swap out->v[0] on first iteration? I only skimmed the
> code that built it up, but it doesn’t look the comment is right 🤔
Very true. This comment is indeed stale from a previous iteration, where
`out->v[0]` was indeed `argv[0]`. Will fix.
> Rest looked reasonable, but I don’t know the sequencer APIs very well.
The sequencer API is quite complex overall, I cannot blame you :) Took
me quite a while to get the hang of it.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC 05/11] builtin/history: implement "drop" subcommand
2025-08-19 10:56 ` [PATCH RFC 05/11] builtin/history: implement "drop" subcommand Patrick Steinhardt
2025-08-20 20:39 ` Ben Knoble
@ 2025-08-23 16:15 ` Jean-Noël AVILA
2025-08-24 16:02 ` Patrick Steinhardt
1 sibling, 1 reply; 278+ messages in thread
From: Jean-Noël AVILA @ 2025-08-23 16:15 UTC (permalink / raw)
To: git, Patrick Steinhardt
On Tuesday, 19 August 2025 12:56:01 CEST Patrick Steinhardt wrote:
> It is a fairly common operation to perform an interactive rebase so that
> one of the commits can be dropped from history. Doing this is not very
> hard in general, but still requires the user to perform multiple steps:
>
> 1. Identify the commit in question that is to be dropped.
>
> 2. Perform an interactive rebase on top of that commit's parent.
>
> 3. Edit the instruction sheet to drop that commit.
>
> This is needlessly complex for such a supposedly-trivial operation.
> Furthermore, the second step doesn't account for certain edge cases like
> for example dropping the root commit.
>
> Introduce a new "drop" subcommand to make this use case significantly
> simpler: all the user needs to do is to say `git history drop $COMMIT`
> and they're done.
>
> Note that for now, this command only allows users to drop a single
> commit at once. It should be easy enough though to expand the command at
> a later point in time to support dropping whole commit ranges.
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> Documentation/git-history.adoc | 27 +++-
> builtin/history.c | 297 ++++++++++++++++++++++++++++++++++++++
++-
> t/meson.build | 3 +-
> t/t3450-history-drop.sh | 127 ++++++++++++++++++
> 4 files changed, 449 insertions(+), 5 deletions(-)
>
> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
> index 9dafb8fc16..3012445ddc 100644
> --- a/Documentation/git-history.adoc
> +++ b/Documentation/git-history.adoc
> @@ -8,7 +8,7 @@ git-history - Rewrite history of the current branch
> SYNOPSIS
> --------
> [synopsis]
> -git history [<options>]
> +git history drop [<options>] <revision>
>
Grepping through the documentation for the <revision> placeholder does not
yield a lot of matches. Can <revision> be replaced by <commit> or <commit-ish>
in this context; these ones seem widely used.
> DESCRIPTION
> -----------
> @@ -31,6 +31,31 @@ COMMANDS
> This command requires a subcommand. Several subcommands are available to
> rewrite history in different ways.
>
> +drop <revision>::
My linting patch series[1] does not catch this kind of synopsis miss, but
here, backticks are missing because this is a part of synopsis:
`drop <revision>`::
> + Drop a commit from the history and reapply all children of that
> + commit on top of the commit's parent. The commit that is to be
> + dropped must be reachable from the current `HEAD` commit.
> ++
> +Dropping the root commit converts the child of that commit into the new
> +root commit. It is invalid to drop a root commit that does not have any
> +child commits, as that would lead to an empty branch.
> +
> +EXAMPLES
> +--------
> +
> +* Drop a commit from history.
> ++
As the examples are quite long, it would make sense to declare each example as
a sub-section:
Drop a commit from history
~~~~~~~~~~~~~~~~~~~~~~~~~~
> +----------
> +$ git log --oneline
> +2d4cd6d third
> +125a0f3 second
> +e098c27 first
> +$ git history drop HEAD~
> +$ git log
> +b1bc1bd third
> +e098c27 first
> +----------
> +
> CONFIGURATION
> -------------
>
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC 05/11] builtin/history: implement "drop" subcommand
2025-08-23 16:15 ` Jean-Noël AVILA
@ 2025-08-24 16:02 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-24 16:02 UTC (permalink / raw)
To: Jean-Noël AVILA; +Cc: git
On Sat, Aug 23, 2025 at 06:15:08PM +0200, Jean-Noël AVILA wrote:
> On Tuesday, 19 August 2025 12:56:01 CEST Patrick Steinhardt wrote:
> > diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
> > index 9dafb8fc16..3012445ddc 100644
> > --- a/Documentation/git-history.adoc
> > +++ b/Documentation/git-history.adoc
> > @@ -8,7 +8,7 @@ git-history - Rewrite history of the current branch
> > SYNOPSIS
> > --------
> > [synopsis]
> > -git history [<options>]
> > +git history drop [<options>] <revision>
> >
>
> Grepping through the documentation for the <revision> placeholder does not
> yield a lot of matches. Can <revision> be replaced by <commit> or <commit-ish>
> in this context; these ones seem widely used.
Yup, makes sense.
> > DESCRIPTION
> > -----------
> > @@ -31,6 +31,31 @@ COMMANDS
> > This command requires a subcommand. Several subcommands are available to
> > rewrite history in different ways.
> >
> > +drop <revision>::
>
> My linting patch series[1] does not catch this kind of synopsis miss, but
> here, backticks are missing because this is a part of synopsis:
>
> `drop <revision>`::
Okay.
> > + Drop a commit from the history and reapply all children of that
> > + commit on top of the commit's parent. The commit that is to be
> > + dropped must be reachable from the current `HEAD` commit.
> > ++
> > +Dropping the root commit converts the child of that commit into the new
> > +root commit. It is invalid to drop a root commit that does not have any
> > +child commits, as that would lead to an empty branch.
> > +
> > +EXAMPLES
> > +--------
> > +
> > +* Drop a commit from history.
> > ++
>
> As the examples are quite long, it would make sense to declare each example as
> a sub-section:
>
> Drop a commit from history
> ~~~~~~~~~~~~~~~~~~~~~~~~~~
Makes sense.
Thanks by the way for caring about our documentation and trying to make
sure that we're being more consistent. I really appreciate this kind of
work!
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH RFC 06/11] builtin/history: implement "reorder" subcommand
2025-08-19 10:55 [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing Patrick Steinhardt
` (4 preceding siblings ...)
2025-08-19 10:56 ` [PATCH RFC 05/11] builtin/history: implement "drop" subcommand Patrick Steinhardt
@ 2025-08-19 10:56 ` Patrick Steinhardt
2025-08-23 16:24 ` Jean-Noël AVILA
2025-08-24 17:25 ` Kristoffer Haugsbakk
2025-08-19 10:56 ` [PATCH RFC 07/11] add-patch: split out header from "add-interactive.h" Patrick Steinhardt
` (14 subsequent siblings)
20 siblings, 2 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-19 10:56 UTC (permalink / raw)
To: git
When working in projects where having nice commits matters it's quite
common that developers end up reordering commits a lot. Tihs is
typically done via interactive rebases, where they can then rearrange
commits in the instruction sheet.
Still, this operation is a frequent-enough operation to provide a more
direct of doing this imperatively. As such, introduce a new "reorder"
subcommand where users can reorder a commit A to come after or before
another commit B:
$ git log --oneline
a978f73 fifth
57594ee fourth
04eb1c4 third
d535e30 second
bf7438d first
$ git history reorder :/fourth --before=:/second
$ git log --oneline
1610fe0 fifth
444f97d third
2f90797 second
b0ae659 fourth
bf7438d first
$ git history reorder :/fourth --after=:/second
$ git log --oneline
c48729d fifth
f44a46e third
26693b8 fourth
8cb4171 second
bf7438d first
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-history.adoc | 32 ++++++
builtin/history.c | 135 +++++++++++++++++++++++++
t/meson.build | 1 +
t/t3451-history-reorder.sh | 218 +++++++++++++++++++++++++++++++++++++++++
4 files changed, 386 insertions(+)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index 3012445ddc..6e8b4e1326 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -9,6 +9,7 @@ SYNOPSIS
--------
[synopsis]
git history drop [<options>] <revision>
+git history reorder [<options>] <revision> --(before|after)=<revision>
DESCRIPTION
-----------
@@ -40,6 +41,12 @@ Dropping the root commit converts the child of that commit into the new
root commit. It is invalid to drop a root commit that does not have any
child commits, as that would lead to an empty branch.
+reorder <revision> (--before=<revision>|--after=<revision>)::
+ Reorder the commit so that it becomes either the parent
+ (`--before=`) or child (`--after=`) of the other specified
+ commit. The commits must be related to one another and must be
+ reachable from the current `HEAD` commit.
+
EXAMPLES
--------
@@ -56,6 +63,31 @@ b1bc1bd third
e098c27 first
----------
+* Reorder a commit.
++
+----------
+$ git log --oneline
+a978f73 fifth
+57594ee fourth
+04eb1c4 third
+d535e30 second
+bf7438d first
+$ git history reorder :/fourth --before=:/second
+$ git log --oneline
+1610fe0 fifth
+444f97d third
+2f90797 second
+b0ae659 fourth
+bf7438d first
+$ git history reorder :/fourth --after=:/second
+$ git log --oneline
+c48729d fifth
+f44a46e third
+26693b8 fourth
+8cb4171 second
+bf7438d first
+----------
+
CONFIGURATION
-------------
diff --git a/builtin/history.c b/builtin/history.c
index 183ab9d5f7..de6073f557 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -83,6 +83,33 @@ static int collect_commits(struct repository *repo,
return ret;
}
+static void replace_commits(struct strvec *commits,
+ const struct object_id *commit_to_replace,
+ const struct object_id *replacements,
+ size_t replacements_nr)
+{
+ char commit_to_replace_oid[GIT_MAX_HEXSZ + 1];
+ struct strvec replacement_oids = STRVEC_INIT;
+ bool found = false;
+ size_t i;
+
+ oid_to_hex_r(commit_to_replace_oid, commit_to_replace);
+ for (i = 0; i < replacements_nr; i++)
+ strvec_push(&replacement_oids, oid_to_hex(&replacements[i]));
+
+ for (i = 0; i < commits->nr; i++) {
+ if (strcmp(commits->v[i], commit_to_replace_oid))
+ continue;
+ strvec_splice(commits, i, 1, replacement_oids.v, replacement_oids.nr);
+ found = true;
+ break;
+ }
+ if (!found)
+ BUG("could not find commit to replace");
+
+ strvec_clear(&replacement_oids);
+}
+
static int apply_commits(struct repository *repo,
const struct strvec *commits,
struct commit *head,
@@ -291,6 +318,112 @@ static int cmd_history_drop(int argc,
return ret;
}
+static int cmd_history_reorder(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo)
+{
+ const char * const usage[] = {
+ N_("git history reorder [<options>] <revision> (--before=<commit>|--after=<commit>)"),
+ NULL,
+ };
+ const char *before = NULL, *after = NULL;
+ struct option options[] = {
+ OPT_STRING(0, "before", &before, N_("commit"), N_("reorder before this commit")),
+ OPT_STRING(0, "after", &after, N_("commit"), N_("reorder after this commit")),
+ OPT_END(),
+ };
+ struct commit *commit_to_reorder, *head, *anchor, *old;
+ struct strvec commits = STRVEC_INIT;
+ struct object_id replacement[2];
+ struct commit_list *list = NULL;
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc != 1)
+ die(_("command expects a single revision"));
+ if (!before && !after)
+ die(_("exactly one option of 'before' or 'after' must be given"));
+ die_for_incompatible_opt2(!!before, "before", !!after, "after");
+
+ repo_config(repo, git_default_config, NULL);
+
+ commit_to_reorder = lookup_commit_reference_by_name(argv[0]);
+ if (!commit_to_reorder)
+ die(_("commit to be reordered cannot be found: %s"), argv[0]);
+ if (commit_to_reorder->parents && commit_to_reorder->parents->next)
+ die(_("commit to be reordered must not be a merge commit"));
+
+ anchor = lookup_commit_reference_by_name(before ? before : after);
+ if (!commit_to_reorder)
+ die(_("anchor commit cannot be found: %s"), before ? before : after);
+
+ if (oideq(&commit_to_reorder->object.oid, &anchor->object.oid))
+ die(_("commit to reorder and anchor must not be the same"));
+
+ head = lookup_commit_reference_by_name("HEAD");
+ if (!head)
+ die(_("could not resolve HEAD to a commit"));
+
+ commit_list_append(commit_to_reorder, &list);
+ if (!repo_is_descendant_of(repo, commit_to_reorder, list))
+ die(_("reordered commit must be reachable from current HEAD commit"));
+
+ /*
+ * There is no requirement for the user to have either one of the
+ * provided commits be the parent or child. We thus have to figure out
+ * ourselves which one is which.
+ */
+ if (repo_is_descendant_of(repo, anchor, list))
+ old = commit_to_reorder;
+ else
+ old = anchor;
+
+ /*
+ * Select the whole range of commits, including the boundary commit
+ * itself. In case the old commit is the root commit we simply pass no
+ * boundary.
+ */
+ ret = collect_commits(repo, old->parents ? old->parents->item : NULL,
+ head, &commits);
+ if (ret < 0)
+ goto out;
+
+ /*
+ * Perform the reordering of commits in the strvec. This is done by:
+ *
+ * - Deleting the to-be-reordered commit from the range of commits.
+ *
+ * - Replacing the anchor commit with the anchor commit plus the
+ * to-be-reordered commit.
+ */
+ if (before) {
+ replacement[0] = commit_to_reorder->object.oid;
+ replacement[1] = anchor->object.oid;
+ } else {
+ replacement[0] = anchor->object.oid;
+ replacement[1] = commit_to_reorder->object.oid;
+ }
+ replace_commits(&commits, &commit_to_reorder->object.oid, NULL, 0);
+ replace_commits(&commits, &anchor->object.oid, replacement, ARRAY_SIZE(replacement));
+
+ /*
+ * And now we pick commits in the new order on top of either the root
+ * commit or on top the old commit's parent.
+ */
+ ret = apply_commits(repo, &commits, head,
+ old->parents ? old->parents->item : NULL, "reorder");
+ if (ret < 0)
+ goto out;
+
+ ret = 0;
+
+out:
+ free_commit_list(list);
+ strvec_clear(&commits);
+ return ret;
+}
+
int cmd_history(int argc,
const char **argv,
const char *prefix,
@@ -298,11 +431,13 @@ int cmd_history(int argc,
{
const char * const usage[] = {
N_("git history drop [<options>] <revision>"),
+ N_("git history reorder [<options>] <revision> --(before|after)=<revision>"),
NULL,
};
parse_opt_subcommand_fn *fn = NULL;
struct option options[] = {
OPT_SUBCOMMAND("drop", &fn, cmd_history_drop),
+ OPT_SUBCOMMAND("reorder", &fn, cmd_history_reorder),
OPT_END(),
};
diff --git a/t/meson.build b/t/meson.build
index 859c388987..8eded9ec1b 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -377,6 +377,7 @@ integration_tests = [
't3437-rebase-fixup-options.sh',
't3438-rebase-broken-files.sh',
't3450-history-drop.sh',
+ 't3451-history-reorder.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
diff --git a/t/t3451-history-reorder.sh b/t/t3451-history-reorder.sh
new file mode 100755
index 0000000000..cc311ba190
--- /dev/null
+++ b/t/t3451-history-reorder.sh
@@ -0,0 +1,218 @@
+#!/bin/sh
+
+test_description='tests for git-history reorder subcommand'
+
+. ./test-lib.sh
+
+test_expect_success 'reorder refuses to work with merge commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ git branch branch &&
+ test_commit ours &&
+ git switch branch &&
+ test_commit theirs &&
+ git switch - &&
+ git merge theirs &&
+ test_must_fail git history reorder HEAD --before=HEAD~ 2>err &&
+ test_grep "commit to be reordered must not be a merge commit" err &&
+ test_must_fail git history reorder HEAD~ --after=HEAD 2>err &&
+ test_grep "cannot rearrange commit history with merges" err
+ )
+'
+
+test_expect_success 'reorder requires exactly one of --before or --after' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_must_fail git history reorder HEAD 2>err &&
+ test_grep "exactly one option of ${SQ}before${SQ} or ${SQ}after${SQ} must be given" err &&
+ test_must_fail git history reorder HEAD --before=a --after=b 2>err &&
+ test_grep "options ${SQ}before${SQ} and ${SQ}after${SQ} cannot be used together" err
+ )
+'
+
+test_expect_success 'reorder refuses to reorder commit with itself' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_must_fail git history reorder HEAD --after=HEAD 2>err &&
+ test_grep "commit to reorder and anchor must not be the same" err
+ )
+'
+
+test_expect_success '--before can move commit back in history' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ test_commit fourth &&
+ test_commit fifth &&
+ git history reorder :/fourth --before=:/second &&
+ cat >expect <<-EOF &&
+ fifth
+ third
+ second
+ fourth
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success '--before can move commit forward in history' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ test_commit fourth &&
+ test_commit fifth &&
+ git history reorder :/second --before=:/fourth &&
+ cat >expect <<-EOF &&
+ fifth
+ fourth
+ second
+ third
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success '--before can make a commit a root commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ git history reorder :/third --before=:/first &&
+ cat >expect <<-EOF &&
+ second
+ first
+ third
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success '--after can move commit back in history' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ test_commit fourth &&
+ test_commit fifth &&
+ git history reorder :/fourth --after=:/second &&
+ cat >expect <<-EOF &&
+ fifth
+ third
+ fourth
+ second
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success '--after can move commit forward in history' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ test_commit fourth &&
+ test_commit fifth &&
+ git history reorder :/second --after=:/fourth &&
+ cat >expect <<-EOF &&
+ fifth
+ second
+ fourth
+ third
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success '--after can make commit the tip' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ git history reorder :/first --after=:/third &&
+ cat >expect <<-EOF &&
+ first
+ third
+ second
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'conflicts are detected' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ echo base >file &&
+ git add file &&
+ git commit -m base &&
+ echo "first edit" >file &&
+ git commit -am "first edit" &&
+ echo "second edit" >file &&
+ git commit -am "second edit" &&
+
+ git symbolic-ref HEAD >expect-head &&
+ test_must_fail git history reorder HEAD --before=HEAD~ &&
+ test_must_fail git symbolic-ref HEAD &&
+ echo "second edit" >file &&
+ git add file &&
+ test_must_fail git cherry-pick --continue &&
+ echo "first edit" >file &&
+ git add file &&
+ git cherry-pick --continue &&
+
+ cat >expect <<-EOF &&
+ first edit
+ second edit
+ base
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual &&
+
+ git symbolic-ref HEAD >actual-head &&
+ test_cmp expect-head actual-head
+ )
+'
+
+test_done
--
2.51.0.261.g7ce5a0a67e.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH RFC 06/11] builtin/history: implement "reorder" subcommand
2025-08-19 10:56 ` [PATCH RFC 06/11] builtin/history: implement "reorder" subcommand Patrick Steinhardt
@ 2025-08-23 16:24 ` Jean-Noël AVILA
2025-08-24 17:25 ` Kristoffer Haugsbakk
1 sibling, 0 replies; 278+ messages in thread
From: Jean-Noël AVILA @ 2025-08-23 16:24 UTC (permalink / raw)
To: git, Patrick Steinhardt
On Tuesday, 19 August 2025 12:56:02 CEST Patrick Steinhardt wrote:
> When working in projects where having nice commits matters it's quite
> common that developers end up reordering commits a lot. Tihs is
> typically done via interactive rebases, where they can then rearrange
> commits in the instruction sheet.
>
> Still, this operation is a frequent-enough operation to provide a more
> direct of doing this imperatively. As such, introduce a new "reorder"
> subcommand where users can reorder a commit A to come after or before
> another commit B:
>
> $ git log --oneline
> a978f73 fifth
> 57594ee fourth
> 04eb1c4 third
> d535e30 second
> bf7438d first
>
> $ git history reorder :/fourth --before=:/second
> $ git log --oneline
> 1610fe0 fifth
> 444f97d third
> 2f90797 second
> b0ae659 fourth
> bf7438d first
>
> $ git history reorder :/fourth --after=:/second
> $ git log --oneline
> c48729d fifth
> f44a46e third
> 26693b8 fourth
> 8cb4171 second
> bf7438d first
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> Documentation/git-history.adoc | 32 ++++++
> builtin/history.c | 135 +++++++++++++++++++++++++
> t/meson.build | 1 +
> t/t3451-history-reorder.sh | 218 ++++++++++++++++++++++++++++++++++++++
+++
> 4 files changed, 386 insertions(+)
>
> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
> index 3012445ddc..6e8b4e1326 100644
> --- a/Documentation/git-history.adoc
> +++ b/Documentation/git-history.adoc
> @@ -9,6 +9,7 @@ SYNOPSIS
> --------
> [synopsis]
> git history drop [<options>] <revision>
> +git history reorder [<options>] <revision> --(before|after)=<revision>
>
Same here about <revision> vs <commit>/<commit-ish>.
Also, the form --(foo|bar) has never been used in synopsis. I think it would
better be unrolled, just like in the description. Can you rename each
occurrence of placeholder <revision> on the line so that the description can
refer to them differently?
> DESCRIPTION
> -----------
>@@ -40,6 +41,12 @@ Dropping the root commit converts the child of that commit
>into the new
> root commit. It is invalid to drop a root commit that does not have any
> child commits, as that would lead to an empty branch.
>
>+reorder <revision> (--before=<revision>|--after=<revision>)::
>+ Reorder the commit so that it becomes either the parent
>+ (`--before=`) or child (`--after=`) of the other specified
>+ commit. The commits must be related to one another and must be
>+ reachable from the current `HEAD` commit.
with the renamed placeholders, this can be turned into something like:
Move <commit> so that it becomes either the parent of <following-commit> or
the child of <preceding-commit>.
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC 06/11] builtin/history: implement "reorder" subcommand
2025-08-19 10:56 ` [PATCH RFC 06/11] builtin/history: implement "reorder" subcommand Patrick Steinhardt
2025-08-23 16:24 ` Jean-Noël AVILA
@ 2025-08-24 17:25 ` Kristoffer Haugsbakk
2025-08-24 17:34 ` Patrick Steinhardt
1 sibling, 1 reply; 278+ messages in thread
From: Kristoffer Haugsbakk @ 2025-08-24 17:25 UTC (permalink / raw)
To: Patrick Steinhardt, git
Disclaimer that I’ve never used Jujutsu.
On Tue, Aug 19, 2025, at 12:56, Patrick Steinhardt wrote:
> When working in projects where having nice commits matters it's quite
> common that developers end up reordering commits a lot. Tihs is
s/Tihs/This/
> typically done via interactive rebases, where they can then rearrange
> commits in the instruction sheet.
>
> Still, this operation is a frequent-enough operation to provide a more
> direct of doing this imperatively. As such, introduce a new "reorder"
s/direct of/direct way of/
What’s a use-case for doing this imperatively? With a nice rebase
frontend you get to shuffle around some lines in your preferred editor.
This seems like using Ex mode in Vim. Which is legitimate but I don’t
quite see when you would do it.
The thing with e.g. the Drop subcommand is that I might have some
commits marked `TEMP` that I wanna quickly drop at some point. That’s
sort of a semi-interactive use case; I might want to lightly script it,
but I am always going to invoke it interactively. Doing that light
scripting on top of git-rebase(1) sounds like a hassle though.
But in this case I don’t understand when this would save you time over a
nice Rebase frontend. Because I don’t see when you want
semi-interactive history reordering.
> subcommand where users can reorder a commit A to come after or before
> another commit B:
>
> $ git log --oneline
> a978f73 fifth
> 57594ee fourth
> 04eb1c4 third
> d535e30 second
> bf7438d first
>
> $ git history reorder :/fourth --before=:/second
> $ git log --oneline
The `:/` notation makes sense here. You’ve probably just made these
commits so you want to match the newest ones.
Is this example meant to demonstrate how it works or to also demonstrate
how you would use it (i.e. with revision syntax)?
> 1610fe0 fifth
> 444f97d third
> 2f90797 second
> b0ae659 fourth
> bf7438d first
>
> $ git history reorder :/fourth --after=:/second
> $ git log --oneline
> c48729d fifth
> f44a46e third
> 26693b8 fourth
> 8cb4171 second
> bf7438d first
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> Documentation/git-history.adoc | 32 ++++++
> builtin/history.c | 135 +++++++++++++++++++++++++
> t/meson.build | 1 +
> t/t3451-history-reorder.sh | 218 +++++++++++++++++++++++++++++++++++++++++
> 4 files changed, 386 insertions(+)
>
> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
> index 3012445ddc..6e8b4e1326 100644
> --- a/Documentation/git-history.adoc
> +++ b/Documentation/git-history.adoc
> @@ -9,6 +9,7 @@ SYNOPSIS
> --------
> [synopsis]
> git history drop [<options>] <revision>
> +git history reorder [<options>] <revision> --(before|after)=<revision>
>
> DESCRIPTION
> -----------
> @@ -40,6 +41,12 @@ Dropping the root commit converts the child of that
> commit into the new
> root commit. It is invalid to drop a root commit that does not have any
> child commits, as that would lead to an empty branch.
>
> +reorder <revision> (--before=<revision>|--after=<revision>)::
> + Reorder the commit so that it becomes either the parent
> + (`--before=`) or child (`--after=`) of the other specified
> + commit. The commits must be related to one another and must be
> + reachable from the current `HEAD` commit.
s/current `HEAD` commit/current commit/ ?
> +
> EXAMPLES
> --------
>[snip]
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC 06/11] builtin/history: implement "reorder" subcommand
2025-08-24 17:25 ` Kristoffer Haugsbakk
@ 2025-08-24 17:34 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-24 17:34 UTC (permalink / raw)
To: Kristoffer Haugsbakk; +Cc: git
On Sun, Aug 24, 2025 at 07:25:48PM +0200, Kristoffer Haugsbakk wrote:
> On Tue, Aug 19, 2025, at 12:56, Patrick Steinhardt wrote:
> > When working in projects where having nice commits matters it's quite
> > common that developers end up reordering commits a lot. Tihs is
>
> s/Tihs/This/
>
> > typically done via interactive rebases, where they can then rearrange
> > commits in the instruction sheet.
> >
> > Still, this operation is a frequent-enough operation to provide a more
> > direct of doing this imperatively. As such, introduce a new "reorder"
>
> s/direct of/direct way of/
>
> What’s a use-case for doing this imperatively? With a nice rebase
> frontend you get to shuffle around some lines in your preferred editor.
> This seems like using Ex mode in Vim. Which is legitimate but I don’t
> quite see when you would do it.
>
> The thing with e.g. the Drop subcommand is that I might have some
> commits marked `TEMP` that I wanna quickly drop at some point. That’s
> sort of a semi-interactive use case; I might want to lightly script it,
> but I am always going to invoke it interactively. Doing that light
> scripting on top of git-rebase(1) sounds like a hassle though.
>
> But in this case I don’t understand when this would save you time over a
> nice Rebase frontend. Because I don’t see when you want
> semi-interactive history reordering.
All I can say is that I ended up using this feature quite regularly with
Jujutsu. It sometimes just feels a less heavy-weight to tell Git what to
do directly instead of doing so via the editor.
Most often this happens when I just created a new smallish commit
because I noticed something and quickly want to reorder it somewhere
earlier.
> > subcommand where users can reorder a commit A to come after or before
> > another commit B:
> >
> > $ git log --oneline
> > a978f73 fifth
> > 57594ee fourth
> > 04eb1c4 third
> > d535e30 second
> > bf7438d first
> >
> > $ git history reorder :/fourth --before=:/second
> > $ git log --oneline
>
> The `:/` notation makes sense here. You’ve probably just made these
> commits so you want to match the newest ones.
>
> Is this example meant to demonstrate how it works or to also demonstrate
> how you would use it (i.e. with revision syntax)?
Both, I guess. I often use this revision syntax myself, but in a lot of
other cases, especially when you just want to rebase HEAD, it may be
easier to just say "HEAD".
> > diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
> > index 3012445ddc..6e8b4e1326 100644
> > --- a/Documentation/git-history.adoc
> > +++ b/Documentation/git-history.adoc
> > @@ -40,6 +41,12 @@ Dropping the root commit converts the child of that
> > commit into the new
> > root commit. It is invalid to drop a root commit that does not have any
> > child commits, as that would lead to an empty branch.
> >
> > +reorder <revision> (--before=<revision>|--after=<revision>)::
> > + Reorder the commit so that it becomes either the parent
> > + (`--before=`) or child (`--after=`) of the other specified
> > + commit. The commits must be related to one another and must be
> > + reachable from the current `HEAD` commit.
>
> s/current `HEAD` commit/current commit/ ?
Let's maybe say "currently checked-out commit".
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH RFC 07/11] add-patch: split out header from "add-interactive.h"
2025-08-19 10:55 [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing Patrick Steinhardt
` (5 preceding siblings ...)
2025-08-19 10:56 ` [PATCH RFC 06/11] builtin/history: implement "reorder" subcommand Patrick Steinhardt
@ 2025-08-19 10:56 ` Patrick Steinhardt
2025-08-19 10:56 ` [PATCH RFC 08/11] add-patch: split out `struct interactive_options` Patrick Steinhardt
` (13 subsequent siblings)
20 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-19 10:56 UTC (permalink / raw)
To: git
While we have a "add-patch.c" code file, its declarations are part of
"add-interactive.h". This makes it somewhat harder than necessary to
find relevant code and to identify clear boundaries between the two
subsystems.
Split up concerns and move declarations that relate to "add-patch.c"
into a new "add-patch.h" header.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
add-interactive.h | 23 +++--------------------
add-patch.c | 1 +
add-patch.h | 26 ++++++++++++++++++++++++++
3 files changed, 30 insertions(+), 20 deletions(-)
diff --git a/add-interactive.h b/add-interactive.h
index 4213dcd67b..fb95b6ee05 100644
--- a/add-interactive.h
+++ b/add-interactive.h
@@ -1,14 +1,11 @@
#ifndef ADD_INTERACTIVE_H
#define ADD_INTERACTIVE_H
+#include "add-patch.h"
#include "color.h"
-struct add_p_opt {
- int context;
- int interhunkcontext;
-};
-
-#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }
+struct pathspec;
+struct repository;
struct add_i_state {
struct repository *r;
@@ -32,21 +29,7 @@ void init_add_i_state(struct add_i_state *s, struct repository *r,
struct add_p_opt *add_p_opt);
void clear_add_i_state(struct add_i_state *s);
-struct repository;
-struct pathspec;
int run_add_i(struct repository *r, const struct pathspec *ps,
struct add_p_opt *add_p_opt);
-enum add_p_mode {
- ADD_P_ADD,
- ADD_P_STASH,
- ADD_P_RESET,
- ADD_P_CHECKOUT,
- ADD_P_WORKTREE,
-};
-
-int run_add_p(struct repository *r, enum add_p_mode mode,
- struct add_p_opt *o, const char *revision,
- const struct pathspec *ps);
-
#endif
diff --git a/add-patch.c b/add-patch.c
index 302e6ba7d9..e2b002fa73 100644
--- a/add-patch.c
+++ b/add-patch.c
@@ -3,6 +3,7 @@
#include "git-compat-util.h"
#include "add-interactive.h"
+#include "add-patch.h"
#include "advice.h"
#include "editor.h"
#include "environment.h"
diff --git a/add-patch.h b/add-patch.h
new file mode 100644
index 0000000000..4394c74107
--- /dev/null
+++ b/add-patch.h
@@ -0,0 +1,26 @@
+#ifndef ADD_PATCH_H
+#define ADD_PATCH_H
+
+struct pathspec;
+struct repository;
+
+struct add_p_opt {
+ int context;
+ int interhunkcontext;
+};
+
+#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }
+
+enum add_p_mode {
+ ADD_P_ADD,
+ ADD_P_STASH,
+ ADD_P_RESET,
+ ADD_P_CHECKOUT,
+ ADD_P_WORKTREE,
+};
+
+int run_add_p(struct repository *r, enum add_p_mode mode,
+ struct add_p_opt *o, const char *revision,
+ const struct pathspec *ps);
+
+#endif
--
2.51.0.261.g7ce5a0a67e.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH RFC 08/11] add-patch: split out `struct interactive_options`
2025-08-19 10:55 [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing Patrick Steinhardt
` (6 preceding siblings ...)
2025-08-19 10:56 ` [PATCH RFC 07/11] add-patch: split out header from "add-interactive.h" Patrick Steinhardt
@ 2025-08-19 10:56 ` Patrick Steinhardt
2025-08-19 10:56 ` [PATCH RFC 09/11] add-patch: remove dependency on "add-interactive" subsystem Patrick Steinhardt
` (12 subsequent siblings)
20 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-19 10:56 UTC (permalink / raw)
To: git
The `struct add_p_opt` is reused both by our the infra for "git add -p"
and "git add -i". Users of `run_add_i()` for example are expected to
pass `struct add_p_opt`. This is somewhat confusing and raises the
question which options apply to what part of the stack.
But things are even more confusing than that: while callers are expected
to pass in `struct add_p_opt`, these options ultimately get used to
initialize a `struct add_i_state` that is used by both subsystems. So we
are basically going full circle here.
Refactor the code and split out a new `struct interactive_options` that
hosts common options used by both. These options are then applied to a
`struct interactive_config` that hosts common configuration.
This refactoring doesn't yet fully detangle the two subsystems from one
another, as we still end up calling `init_add_i_state()` in the "git add
-p" subsystem. This will be fixed in a subsequent commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
add-interactive.c | 151 +++++++++++++----------------------------------------
add-interactive.h | 20 ++-----
add-patch.c | 145 +++++++++++++++++++++++++++++++++++++++++---------
add-patch.h | 33 ++++++++++--
builtin/add.c | 22 ++++----
builtin/checkout.c | 4 +-
builtin/commit.c | 16 +++---
builtin/reset.c | 16 +++---
builtin/stash.c | 46 ++++++++--------
commit.h | 2 +-
10 files changed, 243 insertions(+), 212 deletions(-)
diff --git a/add-interactive.c b/add-interactive.c
index 3e692b47ec..3babc3e013 100644
--- a/add-interactive.c
+++ b/add-interactive.c
@@ -3,7 +3,6 @@
#include "git-compat-util.h"
#include "add-interactive.h"
#include "color.h"
-#include "config.h"
#include "diffcore.h"
#include "gettext.h"
#include "hash.h"
@@ -20,96 +19,18 @@
#include "prompt.h"
#include "tree.h"
-static void init_color(struct repository *r, struct add_i_state *s,
- const char *section_and_slot, char *dst,
- const char *default_color)
-{
- char *key = xstrfmt("color.%s", section_and_slot);
- const char *value;
-
- if (!s->use_color)
- dst[0] = '\0';
- else if (repo_config_get_value(r, key, &value) ||
- color_parse(value, dst))
- strlcpy(dst, default_color, COLOR_MAXLEN);
-
- free(key);
-}
-
void init_add_i_state(struct add_i_state *s, struct repository *r,
- struct add_p_opt *add_p_opt)
+ struct interactive_options *opts)
{
- const char *value;
-
s->r = r;
- s->context = -1;
- s->interhunkcontext = -1;
-
- if (repo_config_get_value(r, "color.interactive", &value))
- s->use_color = -1;
- else
- s->use_color =
- git_config_colorbool("color.interactive", value);
- s->use_color = want_color(s->use_color);
-
- init_color(r, s, "interactive.header", s->header_color, GIT_COLOR_BOLD);
- init_color(r, s, "interactive.help", s->help_color, GIT_COLOR_BOLD_RED);
- init_color(r, s, "interactive.prompt", s->prompt_color,
- GIT_COLOR_BOLD_BLUE);
- init_color(r, s, "interactive.error", s->error_color,
- GIT_COLOR_BOLD_RED);
-
- init_color(r, s, "diff.frag", s->fraginfo_color,
- diff_get_color(s->use_color, DIFF_FRAGINFO));
- init_color(r, s, "diff.context", s->context_color, "fall back");
- if (!strcmp(s->context_color, "fall back"))
- init_color(r, s, "diff.plain", s->context_color,
- diff_get_color(s->use_color, DIFF_CONTEXT));
- init_color(r, s, "diff.old", s->file_old_color,
- diff_get_color(s->use_color, DIFF_FILE_OLD));
- init_color(r, s, "diff.new", s->file_new_color,
- diff_get_color(s->use_color, DIFF_FILE_NEW));
-
- strlcpy(s->reset_color,
- s->use_color ? GIT_COLOR_RESET : "", COLOR_MAXLEN);
-
- FREE_AND_NULL(s->interactive_diff_filter);
- repo_config_get_string(r, "interactive.difffilter",
- &s->interactive_diff_filter);
-
- FREE_AND_NULL(s->interactive_diff_algorithm);
- repo_config_get_string(r, "diff.algorithm",
- &s->interactive_diff_algorithm);
-
- if (!repo_config_get_int(r, "diff.context", &s->context))
- if (s->context < 0)
- die(_("%s cannot be negative"), "diff.context");
- if (!repo_config_get_int(r, "diff.interHunkContext", &s->interhunkcontext))
- if (s->interhunkcontext < 0)
- die(_("%s cannot be negative"), "diff.interHunkContext");
-
- repo_config_get_bool(r, "interactive.singlekey", &s->use_single_key);
- if (s->use_single_key)
- setbuf(stdin, NULL);
-
- if (add_p_opt->context != -1) {
- if (add_p_opt->context < 0)
- die(_("%s cannot be negative"), "--unified");
- s->context = add_p_opt->context;
- }
- if (add_p_opt->interhunkcontext != -1) {
- if (add_p_opt->interhunkcontext < 0)
- die(_("%s cannot be negative"), "--inter-hunk-context");
- s->interhunkcontext = add_p_opt->interhunkcontext;
- }
+ interactive_config_init(&s->cfg, r, opts);
}
void clear_add_i_state(struct add_i_state *s)
{
- FREE_AND_NULL(s->interactive_diff_filter);
- FREE_AND_NULL(s->interactive_diff_algorithm);
+ interactive_config_clear(&s->cfg);
memset(s, 0, sizeof(*s));
- s->use_color = -1;
+ interactive_config_clear(&s->cfg);
}
/*
@@ -262,7 +183,7 @@ static void list(struct add_i_state *s, struct string_list *list, int *selected,
return;
if (opts->header)
- color_fprintf_ln(stdout, s->header_color,
+ color_fprintf_ln(stdout, s->cfg.header_color,
"%s", opts->header);
for (i = 0; i < list->nr; i++) {
@@ -330,7 +251,7 @@ static ssize_t list_and_choose(struct add_i_state *s,
list(s, &items->items, items->selected, &opts->list_opts);
- color_fprintf(stdout, s->prompt_color, "%s", opts->prompt);
+ color_fprintf(stdout, s->cfg.prompt_color, "%s", opts->prompt);
fputs(singleton ? "> " : ">> ", stdout);
fflush(stdout);
@@ -408,7 +329,7 @@ static ssize_t list_and_choose(struct add_i_state *s,
if (from < 0 || from >= items->items.nr ||
(singleton && from + 1 != to)) {
- color_fprintf_ln(stderr, s->error_color,
+ color_fprintf_ln(stderr, s->cfg.error_color,
_("Huh (%s)?"), p);
break;
} else if (singleton) {
@@ -968,7 +889,7 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps,
free(files->items.items[i].string);
} else if (item->index.unmerged ||
item->worktree.unmerged) {
- color_fprintf_ln(stderr, s->error_color,
+ color_fprintf_ln(stderr, s->cfg.error_color,
_("ignoring unmerged: %s"),
files->items.items[i].string);
free(item);
@@ -990,9 +911,9 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps,
opts->prompt = N_("Patch update");
count = list_and_choose(s, files, opts);
if (count > 0) {
- struct add_p_opt add_p_opt = {
- .context = s->context,
- .interhunkcontext = s->interhunkcontext,
+ struct interactive_options opts = {
+ .context = s->cfg.context,
+ .interhunkcontext = s->cfg.interhunkcontext,
};
struct strvec args = STRVEC_INIT;
struct pathspec ps_selected = { 0 };
@@ -1004,7 +925,7 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps,
parse_pathspec(&ps_selected,
PATHSPEC_ALL_MAGIC & ~PATHSPEC_LITERAL,
PATHSPEC_LITERAL_PATH, "", args.v);
- res = run_add_p(s->r, ADD_P_ADD, &add_p_opt, NULL, &ps_selected);
+ res = run_add_p(s->r, ADD_P_ADD, &opts, NULL, &ps_selected);
strvec_clear(&args);
clear_pathspec(&ps_selected);
}
@@ -1040,10 +961,10 @@ static int run_diff(struct add_i_state *s, const struct pathspec *ps,
struct child_process cmd = CHILD_PROCESS_INIT;
strvec_pushl(&cmd.args, "git", "diff", "-p", "--cached", NULL);
- if (s->context != -1)
- strvec_pushf(&cmd.args, "--unified=%i", s->context);
- if (s->interhunkcontext != -1)
- strvec_pushf(&cmd.args, "--inter-hunk-context=%i", s->interhunkcontext);
+ if (s->cfg.context != -1)
+ strvec_pushf(&cmd.args, "--unified=%i", s->cfg.context);
+ if (s->cfg.interhunkcontext != -1)
+ strvec_pushf(&cmd.args, "--inter-hunk-context=%i", s->cfg.interhunkcontext);
strvec_pushl(&cmd.args, oid_to_hex(!is_initial ? &oid :
s->r->hash_algo->empty_tree), "--", NULL);
for (i = 0; i < files->items.nr; i++)
@@ -1061,17 +982,17 @@ static int run_help(struct add_i_state *s, const struct pathspec *ps UNUSED,
struct prefix_item_list *files UNUSED,
struct list_and_choose_options *opts UNUSED)
{
- color_fprintf_ln(stdout, s->help_color, "status - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "status - %s",
_("show paths with changes"));
- color_fprintf_ln(stdout, s->help_color, "update - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "update - %s",
_("add working tree state to the staged set of changes"));
- color_fprintf_ln(stdout, s->help_color, "revert - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "revert - %s",
_("revert staged set of changes back to the HEAD version"));
- color_fprintf_ln(stdout, s->help_color, "patch - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "patch - %s",
_("pick hunks and update selectively"));
- color_fprintf_ln(stdout, s->help_color, "diff - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "diff - %s",
_("view diff between HEAD and index"));
- color_fprintf_ln(stdout, s->help_color, "add untracked - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "add untracked - %s",
_("add contents of untracked files to the staged set of changes"));
return 0;
@@ -1079,21 +1000,21 @@ static int run_help(struct add_i_state *s, const struct pathspec *ps UNUSED,
static void choose_prompt_help(struct add_i_state *s)
{
- color_fprintf_ln(stdout, s->help_color, "%s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "%s",
_("Prompt help:"));
- color_fprintf_ln(stdout, s->help_color, "1 - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "1 - %s",
_("select a single item"));
- color_fprintf_ln(stdout, s->help_color, "3-5 - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "3-5 - %s",
_("select a range of items"));
- color_fprintf_ln(stdout, s->help_color, "2-3,6-9 - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "2-3,6-9 - %s",
_("select multiple ranges"));
- color_fprintf_ln(stdout, s->help_color, "foo - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "foo - %s",
_("select item based on unique prefix"));
- color_fprintf_ln(stdout, s->help_color, "-... - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "-... - %s",
_("unselect specified items"));
- color_fprintf_ln(stdout, s->help_color, "* - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "* - %s",
_("choose all items"));
- color_fprintf_ln(stdout, s->help_color, " - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, " - %s",
_("(empty) finish selecting"));
}
@@ -1128,7 +1049,7 @@ static void print_command_item(int i, int selected UNUSED,
static void command_prompt_help(struct add_i_state *s)
{
- const char *help_color = s->help_color;
+ const char *help_color = s->cfg.help_color;
color_fprintf_ln(stdout, help_color, "%s", _("Prompt help:"));
color_fprintf_ln(stdout, help_color, "1 - %s",
_("select a numbered item"));
@@ -1139,7 +1060,7 @@ static void command_prompt_help(struct add_i_state *s)
}
int run_add_i(struct repository *r, const struct pathspec *ps,
- struct add_p_opt *add_p_opt)
+ struct interactive_options *interactive_opts)
{
struct add_i_state s = { NULL };
struct print_command_item_data data = { "[", "]" };
@@ -1182,15 +1103,15 @@ int run_add_i(struct repository *r, const struct pathspec *ps,
->util = util;
}
- init_add_i_state(&s, r, add_p_opt);
+ init_add_i_state(&s, r, interactive_opts);
/*
* When color was asked for, use the prompt color for
* highlighting, otherwise use square brackets.
*/
- if (s.use_color) {
- data.color = s.prompt_color;
- data.reset = s.reset_color;
+ if (s.cfg.use_color) {
+ data.color = s.cfg.prompt_color;
+ data.reset = s.cfg.reset_color;
}
print_file_item_data.color = data.color;
print_file_item_data.reset = data.reset;
diff --git a/add-interactive.h b/add-interactive.h
index fb95b6ee05..eefa2edc7c 100644
--- a/add-interactive.h
+++ b/add-interactive.h
@@ -2,34 +2,20 @@
#define ADD_INTERACTIVE_H
#include "add-patch.h"
-#include "color.h"
struct pathspec;
struct repository;
struct add_i_state {
struct repository *r;
- int use_color;
- char header_color[COLOR_MAXLEN];
- char help_color[COLOR_MAXLEN];
- char prompt_color[COLOR_MAXLEN];
- char error_color[COLOR_MAXLEN];
- char reset_color[COLOR_MAXLEN];
- char fraginfo_color[COLOR_MAXLEN];
- char context_color[COLOR_MAXLEN];
- char file_old_color[COLOR_MAXLEN];
- char file_new_color[COLOR_MAXLEN];
-
- int use_single_key;
- char *interactive_diff_filter, *interactive_diff_algorithm;
- int context, interhunkcontext;
+ struct interactive_config cfg;
};
void init_add_i_state(struct add_i_state *s, struct repository *r,
- struct add_p_opt *add_p_opt);
+ struct interactive_options *opts);
void clear_add_i_state(struct add_i_state *s);
int run_add_i(struct repository *r, const struct pathspec *ps,
- struct add_p_opt *add_p_opt);
+ struct interactive_options *opts);
#endif
diff --git a/add-patch.c b/add-patch.c
index e2b002fa73..45bc254e0c 100644
--- a/add-patch.c
+++ b/add-patch.c
@@ -5,6 +5,8 @@
#include "add-interactive.h"
#include "add-patch.h"
#include "advice.h"
+#include "config.h"
+#include "diff.h"
#include "editor.h"
#include "environment.h"
#include "gettext.h"
@@ -279,6 +281,99 @@ struct add_p_state {
const char *revision;
};
+static void init_color(struct repository *r,
+ struct interactive_config *cfg,
+ const char *section_and_slot, char *dst,
+ const char *default_color)
+{
+ char *key = xstrfmt("color.%s", section_and_slot);
+ const char *value;
+
+ if (!cfg->use_color)
+ dst[0] = '\0';
+ else if (repo_config_get_value(r, key, &value) ||
+ color_parse(value, dst))
+ strlcpy(dst, default_color, COLOR_MAXLEN);
+
+ free(key);
+}
+
+void interactive_config_init(struct interactive_config *cfg,
+ struct repository *r,
+ struct interactive_options *opts)
+{
+ const char *value;
+
+ cfg->context = -1;
+ cfg->interhunkcontext = -1;
+
+ if (repo_config_get_value(r, "color.interactive", &value))
+ cfg->use_color = -1;
+ else
+ cfg->use_color =
+ git_config_colorbool("color.interactive", value);
+ cfg->use_color = want_color(cfg->use_color);
+
+ init_color(r, cfg, "interactive.header", cfg->header_color, GIT_COLOR_BOLD);
+ init_color(r, cfg, "interactive.help", cfg->help_color, GIT_COLOR_BOLD_RED);
+ init_color(r, cfg, "interactive.prompt", cfg->prompt_color,
+ GIT_COLOR_BOLD_BLUE);
+ init_color(r, cfg, "interactive.error", cfg->error_color,
+ GIT_COLOR_BOLD_RED);
+
+ init_color(r, cfg, "diff.frag", cfg->fraginfo_color,
+ diff_get_color(cfg->use_color, DIFF_FRAGINFO));
+ init_color(r, cfg, "diff.context", cfg->context_color, "fall back");
+ if (!strcmp(cfg->context_color, "fall back"))
+ init_color(r, cfg, "diff.plain", cfg->context_color,
+ diff_get_color(cfg->use_color, DIFF_CONTEXT));
+ init_color(r, cfg, "diff.old", cfg->file_old_color,
+ diff_get_color(cfg->use_color, DIFF_FILE_OLD));
+ init_color(r, cfg, "diff.new", cfg->file_new_color,
+ diff_get_color(cfg->use_color, DIFF_FILE_NEW));
+
+ strlcpy(cfg->reset_color,
+ cfg->use_color ? GIT_COLOR_RESET : "", COLOR_MAXLEN);
+
+ FREE_AND_NULL(cfg->interactive_diff_filter);
+ repo_config_get_string(r, "interactive.difffilter",
+ &cfg->interactive_diff_filter);
+
+ FREE_AND_NULL(cfg->interactive_diff_algorithm);
+ repo_config_get_string(r, "diff.algorithm",
+ &cfg->interactive_diff_algorithm);
+
+ if (!repo_config_get_int(r, "diff.context", &cfg->context))
+ if (cfg->context < 0)
+ die(_("%s cannot be negative"), "diff.context");
+ if (!repo_config_get_int(r, "diff.interHunkContext", &cfg->interhunkcontext))
+ if (cfg->interhunkcontext < 0)
+ die(_("%s cannot be negative"), "diff.interHunkContext");
+
+ repo_config_get_bool(r, "interactive.singlekey", &cfg->use_single_key);
+ if (cfg->use_single_key)
+ setbuf(stdin, NULL);
+
+ if (opts->context != -1) {
+ if (opts->context < 0)
+ die(_("%s cannot be negative"), "--unified");
+ cfg->context = opts->context;
+ }
+ if (opts->interhunkcontext != -1) {
+ if (opts->interhunkcontext < 0)
+ die(_("%s cannot be negative"), "--inter-hunk-context");
+ cfg->interhunkcontext = opts->interhunkcontext;
+ }
+}
+
+void interactive_config_clear(struct interactive_config *cfg)
+{
+ FREE_AND_NULL(cfg->interactive_diff_filter);
+ FREE_AND_NULL(cfg->interactive_diff_algorithm);
+ memset(cfg, 0, sizeof(*cfg));
+ cfg->use_color = -1;
+}
+
static void add_p_state_clear(struct add_p_state *s)
{
size_t i;
@@ -299,9 +394,9 @@ static void err(struct add_p_state *s, const char *fmt, ...)
va_list args;
va_start(args, fmt);
- fputs(s->s.error_color, stdout);
+ fputs(s->s.cfg.error_color, stdout);
vprintf(fmt, args);
- puts(s->s.reset_color);
+ puts(s->s.cfg.reset_color);
va_end(args);
}
@@ -424,12 +519,12 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
int res;
strvec_pushv(&args, s->mode->diff_cmd);
- if (s->s.context != -1)
- strvec_pushf(&args, "--unified=%i", s->s.context);
- if (s->s.interhunkcontext != -1)
- strvec_pushf(&args, "--inter-hunk-context=%i", s->s.interhunkcontext);
- if (s->s.interactive_diff_algorithm)
- strvec_pushf(&args, "--diff-algorithm=%s", s->s.interactive_diff_algorithm);
+ if (s->s.cfg.context != -1)
+ strvec_pushf(&args, "--unified=%i", s->s.cfg.context);
+ if (s->s.cfg.interhunkcontext != -1)
+ strvec_pushf(&args, "--inter-hunk-context=%i", s->s.cfg.interhunkcontext);
+ if (s->s.cfg.interactive_diff_algorithm)
+ strvec_pushf(&args, "--diff-algorithm=%s", s->s.cfg.interactive_diff_algorithm);
if (s->revision) {
struct object_id oid;
strvec_push(&args,
@@ -460,7 +555,7 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
if (want_color_fd(1, -1)) {
struct child_process colored_cp = CHILD_PROCESS_INIT;
- const char *diff_filter = s->s.interactive_diff_filter;
+ const char *diff_filter = s->s.cfg.interactive_diff_filter;
setup_child_process(s, &colored_cp, NULL);
xsnprintf((char *)args.v[color_arg_index], 8, "--color");
@@ -693,7 +788,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
hunk->colored_end - hunk->colored_start);
return;
} else {
- strbuf_addstr(out, s->s.fraginfo_color);
+ strbuf_addstr(out, s->s.cfg.fraginfo_color);
p = s->colored.buf + header->colored_extra_start;
len = header->colored_extra_end
- header->colored_extra_start;
@@ -715,7 +810,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
if (len)
strbuf_add(out, p, len);
else if (colored)
- strbuf_addf(out, "%s\n", s->s.reset_color);
+ strbuf_addf(out, "%s\n", s->s.cfg.reset_color);
else
strbuf_addch(out, '\n');
}
@@ -1103,12 +1198,12 @@ static void recolor_hunk(struct add_p_state *s, struct hunk *hunk)
strbuf_addstr(&s->colored,
plain[current] == '-' ?
- s->s.file_old_color :
+ s->s.cfg.file_old_color :
plain[current] == '+' ?
- s->s.file_new_color :
- s->s.context_color);
+ s->s.cfg.file_new_color :
+ s->s.cfg.context_color);
strbuf_add(&s->colored, plain + current, eol - current);
- strbuf_addstr(&s->colored, s->s.reset_color);
+ strbuf_addstr(&s->colored, s->s.cfg.reset_color);
if (next > eol)
strbuf_add(&s->colored, plain + eol, next - eol);
current = next;
@@ -1227,7 +1322,7 @@ static int run_apply_check(struct add_p_state *s,
static int read_single_character(struct add_p_state *s)
{
- if (s->s.use_single_key) {
+ if (s->s.cfg.use_single_key) {
int res = read_key_without_echo(&s->answer);
printf("%s\n", res == EOF ? "" : s->answer.buf);
return res;
@@ -1241,7 +1336,7 @@ static int read_single_character(struct add_p_state *s)
static int prompt_yesno(struct add_p_state *s, const char *prompt)
{
for (;;) {
- color_fprintf(stdout, s->s.prompt_color, "%s", _(prompt));
+ color_fprintf(stdout, s->s.cfg.prompt_color, "%s", _(prompt));
fflush(stdout);
if (read_single_character(s) == EOF)
return -1;
@@ -1522,15 +1617,15 @@ static int patch_update_file(struct add_p_state *s,
else
prompt_mode_type = PROMPT_HUNK;
- printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->s.prompt_color,
+ printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->s.cfg.prompt_color,
(uintmax_t)hunk_index + 1,
(uintmax_t)(file_diff->hunk_nr
? file_diff->hunk_nr
: 1));
printf(_(s->mode->prompt_mode[prompt_mode_type]),
s->buf.buf);
- if (*s->s.reset_color)
- fputs(s->s.reset_color, stdout);
+ if (*s->s.cfg.reset_color)
+ fputs(s->s.cfg.reset_color, stdout);
fflush(stdout);
if (read_single_character(s) == EOF)
break;
@@ -1687,7 +1782,7 @@ static int patch_update_file(struct add_p_state *s,
err(s, _("Sorry, cannot split this hunk"));
} else if (!split_hunk(s, file_diff,
hunk - file_diff->hunk)) {
- color_fprintf_ln(stdout, s->s.header_color,
+ color_fprintf_ln(stdout, s->s.cfg.header_color,
_("Split into %d hunks."),
(int)splittable_into);
rendered_hunk_index = -1;
@@ -1705,7 +1800,7 @@ static int patch_update_file(struct add_p_state *s,
} else if (s->answer.buf[0] == '?') {
const char *p = _(help_patch_remainder), *eol = p;
- color_fprintf(stdout, s->s.help_color, "%s",
+ color_fprintf(stdout, s->s.cfg.help_color, "%s",
_(s->mode->help_patch_text));
/*
@@ -1723,7 +1818,7 @@ static int patch_update_file(struct add_p_state *s,
if (*p != '?' && !strchr(s->buf.buf, *p))
continue;
- color_fprintf_ln(stdout, s->s.help_color,
+ color_fprintf_ln(stdout, s->s.cfg.help_color,
"%.*s", (int)(eol - p), p);
}
} else {
@@ -1764,7 +1859,7 @@ static int patch_update_file(struct add_p_state *s,
}
int run_add_p(struct repository *r, enum add_p_mode mode,
- struct add_p_opt *o, const char *revision,
+ struct interactive_options *opts, const char *revision,
const struct pathspec *ps)
{
struct add_p_state s = {
@@ -1772,7 +1867,7 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
};
size_t i, binary_count = 0;
- init_add_i_state(&s.s, r, o);
+ init_add_i_state(&s.s, r, opts);
if (mode == ADD_P_STASH)
s.mode = &patch_mode_stash;
diff --git a/add-patch.h b/add-patch.h
index 4394c74107..51c0d7bce9 100644
--- a/add-patch.h
+++ b/add-patch.h
@@ -1,15 +1,42 @@
#ifndef ADD_PATCH_H
#define ADD_PATCH_H
+#include "color.h"
+
struct pathspec;
struct repository;
-struct add_p_opt {
+struct interactive_options {
int context;
int interhunkcontext;
};
-#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }
+#define INTERACTIVE_OPTIONS_INIT { \
+ .context = -1, \
+ .interhunkcontext = -1, \
+}
+
+struct interactive_config {
+ int use_color;
+ char header_color[COLOR_MAXLEN];
+ char help_color[COLOR_MAXLEN];
+ char prompt_color[COLOR_MAXLEN];
+ char error_color[COLOR_MAXLEN];
+ char reset_color[COLOR_MAXLEN];
+ char fraginfo_color[COLOR_MAXLEN];
+ char context_color[COLOR_MAXLEN];
+ char file_old_color[COLOR_MAXLEN];
+ char file_new_color[COLOR_MAXLEN];
+
+ int use_single_key;
+ char *interactive_diff_filter, *interactive_diff_algorithm;
+ int context, interhunkcontext;
+};
+
+void interactive_config_init(struct interactive_config *cfg,
+ struct repository *r,
+ struct interactive_options *opts);
+void interactive_config_clear(struct interactive_config *cfg);
enum add_p_mode {
ADD_P_ADD,
@@ -20,7 +47,7 @@ enum add_p_mode {
};
int run_add_p(struct repository *r, enum add_p_mode mode,
- struct add_p_opt *o, const char *revision,
+ struct interactive_options *opts, const char *revision,
const struct pathspec *ps);
#endif
diff --git a/builtin/add.c b/builtin/add.c
index 0235854f80..a94c826c14 100644
--- a/builtin/add.c
+++ b/builtin/add.c
@@ -30,7 +30,7 @@ static const char * const builtin_add_usage[] = {
NULL
};
static int patch_interactive, add_interactive, edit_interactive;
-static struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+static struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
static int take_worktree_changes;
static int add_renormalize;
static int pathspec_file_nul;
@@ -159,7 +159,7 @@ static int refresh(struct repository *repo, int verbose, const struct pathspec *
int interactive_add(struct repository *repo,
const char **argv,
const char *prefix,
- int patch, struct add_p_opt *add_p_opt)
+ int patch, struct interactive_options *interactive_opts)
{
struct pathspec pathspec;
int ret;
@@ -171,9 +171,9 @@ int interactive_add(struct repository *repo,
prefix, argv);
if (patch)
- ret = !!run_add_p(repo, ADD_P_ADD, add_p_opt, NULL, &pathspec);
+ ret = !!run_add_p(repo, ADD_P_ADD, interactive_opts, NULL, &pathspec);
else
- ret = !!run_add_i(repo, &pathspec, add_p_opt);
+ ret = !!run_add_i(repo, &pathspec, interactive_opts);
clear_pathspec(&pathspec);
return ret;
@@ -255,8 +255,8 @@ static struct option builtin_add_options[] = {
OPT_GROUP(""),
OPT_BOOL('i', "interactive", &add_interactive, N_("interactive picking")),
OPT_BOOL('p', "patch", &patch_interactive, N_("select hunks interactively")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT_BOOL('e', "edit", &edit_interactive, N_("edit current diff and apply")),
OPT__FORCE(&ignored_too, N_("allow adding otherwise ignored files"), 0),
OPT_BOOL('u', "update", &take_worktree_changes, N_("update tracked files")),
@@ -398,9 +398,9 @@ int cmd_add(int argc,
prepare_repo_settings(repo);
repo->settings.command_requires_full_index = 0;
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
if (patch_interactive)
@@ -410,11 +410,11 @@ int cmd_add(int argc,
die(_("options '%s' and '%s' cannot be used together"), "--dry-run", "--interactive/--patch");
if (pathspec_from_file)
die(_("options '%s' and '%s' cannot be used together"), "--pathspec-from-file", "--interactive/--patch");
- exit(interactive_add(repo, argv + 1, prefix, patch_interactive, &add_p_opt));
+ exit(interactive_add(repo, argv + 1, prefix, patch_interactive, &interactive_opts));
} else {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--interactive/--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--interactive/--patch");
}
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 43583c8d1b..0b90f398fe 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -546,7 +546,7 @@ static int checkout_paths(const struct checkout_opts *opts,
if (opts->patch_mode) {
enum add_p_mode patch_mode;
- struct add_p_opt add_p_opt = {
+ struct interactive_options interactive_opts = {
.context = opts->patch_context,
.interhunkcontext = opts->patch_interhunk_context,
};
@@ -575,7 +575,7 @@ static int checkout_paths(const struct checkout_opts *opts,
else
BUG("either flag must have been set, worktree=%d, index=%d",
opts->checkout_worktree, opts->checkout_index);
- return !!run_add_p(the_repository, patch_mode, &add_p_opt,
+ return !!run_add_p(the_repository, patch_mode, &interactive_opts,
rev, &opts->pathspec);
}
diff --git a/builtin/commit.c b/builtin/commit.c
index b5b9608813..767351fd87 100644
--- a/builtin/commit.c
+++ b/builtin/commit.c
@@ -123,7 +123,7 @@ static const char *edit_message, *use_message;
static char *fixup_message, *fixup_commit, *squash_message;
static const char *fixup_prefix;
static int all, also, interactive, patch_interactive, only, amend, signoff;
-static struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+static struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
static int edit_flag = -1; /* unspecified */
static int quiet, verbose, no_verify, allow_empty, dry_run, renew_authorship;
static int config_commit_verbose = -1; /* unspecified */
@@ -356,9 +356,9 @@ static const char *prepare_index(const char **argv, const char *prefix,
const char *ret;
char *path = NULL;
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
if (is_status)
@@ -407,7 +407,7 @@ static const char *prepare_index(const char **argv, const char *prefix,
old_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT));
setenv(INDEX_ENVIRONMENT, the_repository->index_file, 1);
- if (interactive_add(the_repository, argv, prefix, patch_interactive, &add_p_opt) != 0)
+ if (interactive_add(the_repository, argv, prefix, patch_interactive, &interactive_opts) != 0)
die(_("interactive add failed"));
the_repository->index_file = old_repo_index_file;
@@ -432,9 +432,9 @@ static const char *prepare_index(const char **argv, const char *prefix,
ret = get_lock_file_path(&index_lock);
goto out;
} else {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--interactive/--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--interactive/--patch");
}
@@ -1738,8 +1738,8 @@ int cmd_commit(int argc,
OPT_BOOL('i', "include", &also, N_("add specified files to index for commit")),
OPT_BOOL(0, "interactive", &interactive, N_("interactively add files")),
OPT_BOOL('p', "patch", &patch_interactive, N_("interactively add changes")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT_BOOL('o', "only", &only, N_("commit only specified files")),
OPT_BOOL('n', "no-verify", &no_verify, N_("bypass pre-commit and commit-msg hooks")),
OPT_BOOL(0, "dry-run", &dry_run, N_("show what would be committed")),
diff --git a/builtin/reset.c b/builtin/reset.c
index ed35802af1..088449e120 100644
--- a/builtin/reset.c
+++ b/builtin/reset.c
@@ -346,7 +346,7 @@ int cmd_reset(int argc,
struct object_id oid;
struct pathspec pathspec;
int intent_to_add = 0;
- struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
const struct option options[] = {
OPT__QUIET(&quiet, N_("be quiet, only report errors")),
OPT_BOOL(0, "no-refresh", &no_refresh,
@@ -371,8 +371,8 @@ int cmd_reset(int argc,
PARSE_OPT_OPTARG,
option_parse_recurse_submodules_worktree_updater),
OPT_BOOL('p', "patch", &patch_mode, N_("select hunks interactively")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT_BOOL('N', "intent-to-add", &intent_to_add,
N_("record only the fact that removed paths will be added later")),
OPT_PATHSPEC_FROM_FILE(&pathspec_from_file),
@@ -423,9 +423,9 @@ int cmd_reset(int argc,
oidcpy(&oid, &tree->object.oid);
}
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
prepare_repo_settings(the_repository);
@@ -436,12 +436,12 @@ int cmd_reset(int argc,
die(_("options '%s' and '%s' cannot be used together"), "--patch", "--{hard,mixed,soft}");
trace2_cmd_mode("patch-interactive");
update_ref_status = !!run_add_p(the_repository, ADD_P_RESET,
- &add_p_opt, rev, &pathspec);
+ &interactive_opts, rev, &pathspec);
goto cleanup;
} else {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--patch");
}
diff --git a/builtin/stash.c b/builtin/stash.c
index 1977e50df2..5070b8a88f 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -1302,7 +1302,7 @@ static int stash_staged(struct stash_info *info, struct strbuf *out_patch,
static int stash_patch(struct stash_info *info, const struct pathspec *ps,
struct strbuf *out_patch, int quiet,
- struct add_p_opt *add_p_opt)
+ struct interactive_options *interactive_opts)
{
int ret = 0;
struct child_process cp_read_tree = CHILD_PROCESS_INIT;
@@ -1327,7 +1327,7 @@ static int stash_patch(struct stash_info *info, const struct pathspec *ps,
old_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT));
setenv(INDEX_ENVIRONMENT, the_repository->index_file, 1);
- ret = !!run_add_p(the_repository, ADD_P_STASH, add_p_opt, NULL, ps);
+ ret = !!run_add_p(the_repository, ADD_P_STASH, interactive_opts, NULL, ps);
the_repository->index_file = old_repo_index_file;
if (old_index_env && *old_index_env)
@@ -1422,7 +1422,8 @@ static int stash_working_tree(struct stash_info *info, const struct pathspec *ps
}
static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_buf,
- int include_untracked, int patch_mode, struct add_p_opt *add_p_opt,
+ int include_untracked, int patch_mode,
+ struct interactive_options *interactive_opts,
int only_staged, struct stash_info *info, struct strbuf *patch,
int quiet)
{
@@ -1504,7 +1505,7 @@ static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_b
untracked_commit_option = 1;
}
if (patch_mode) {
- ret = stash_patch(info, ps, patch, quiet, add_p_opt);
+ ret = stash_patch(info, ps, patch, quiet, interactive_opts);
if (ret < 0) {
if (!quiet)
fprintf_ln(stderr, _("Cannot save the current "
@@ -1590,7 +1591,8 @@ static int create_stash(int argc, const char **argv, const char *prefix UNUSED,
}
static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int quiet,
- int keep_index, int patch_mode, struct add_p_opt *add_p_opt,
+ int keep_index, int patch_mode,
+ struct interactive_options *interactive_opts,
int include_untracked, int only_staged)
{
int ret = 0;
@@ -1662,7 +1664,7 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q
if (stash_msg)
strbuf_addstr(&stash_msg_buf, stash_msg);
if (do_create_stash(ps, &stash_msg_buf, include_untracked, patch_mode,
- add_p_opt, only_staged, &info, &patch, quiet)) {
+ interactive_opts, only_staged, &info, &patch, quiet)) {
ret = -1;
goto done;
}
@@ -1835,7 +1837,7 @@ static int push_stash(int argc, const char **argv, const char *prefix,
const char *stash_msg = NULL;
char *pathspec_from_file = NULL;
struct pathspec ps;
- struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
struct option options[] = {
OPT_BOOL('k', "keep-index", &keep_index,
N_("keep index")),
@@ -1843,8 +1845,8 @@ static int push_stash(int argc, const char **argv, const char *prefix,
N_("stash staged changes only")),
OPT_BOOL('p', "patch", &patch_mode,
N_("stash in patch mode")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT__QUIET(&quiet, N_("quiet mode")),
OPT_BOOL('u', "include-untracked", &include_untracked,
N_("include untracked files in stash")),
@@ -1901,19 +1903,19 @@ static int push_stash(int argc, const char **argv, const char *prefix,
}
if (!patch_mode) {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--patch");
}
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
ret = do_push_stash(&ps, stash_msg, quiet, keep_index, patch_mode,
- &add_p_opt, include_untracked, only_staged);
+ &interactive_opts, include_untracked, only_staged);
clear_pathspec(&ps);
free(pathspec_from_file);
@@ -1938,7 +1940,7 @@ static int save_stash(int argc, const char **argv, const char *prefix,
const char *stash_msg = NULL;
struct pathspec ps;
struct strbuf stash_msg_buf = STRBUF_INIT;
- struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
struct option options[] = {
OPT_BOOL('k', "keep-index", &keep_index,
N_("keep index")),
@@ -1946,8 +1948,8 @@ static int save_stash(int argc, const char **argv, const char *prefix,
N_("stash staged changes only")),
OPT_BOOL('p', "patch", &patch_mode,
N_("stash in patch mode")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT__QUIET(&quiet, N_("quiet mode")),
OPT_BOOL('u', "include-untracked", &include_untracked,
N_("include untracked files in stash")),
@@ -1967,20 +1969,20 @@ static int save_stash(int argc, const char **argv, const char *prefix,
memset(&ps, 0, sizeof(ps));
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
if (!patch_mode) {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--patch");
}
ret = do_push_stash(&ps, stash_msg, quiet, keep_index,
- patch_mode, &add_p_opt, include_untracked,
+ patch_mode, &interactive_opts, include_untracked,
only_staged);
strbuf_release(&stash_msg_buf);
diff --git a/commit.h b/commit.h
index 1d6e0c7518..7b6e59d6c1 100644
--- a/commit.h
+++ b/commit.h
@@ -258,7 +258,7 @@ int for_each_commit_graft(each_commit_graft_fn, void *);
int interactive_add(struct repository *repo,
const char **argv,
const char *prefix,
- int patch, struct add_p_opt *add_p_opt);
+ int patch, struct interactive_options *opts);
struct commit_extra_header {
struct commit_extra_header *next;
--
2.51.0.261.g7ce5a0a67e.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH RFC 09/11] add-patch: remove dependency on "add-interactive" subsystem
2025-08-19 10:55 [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing Patrick Steinhardt
` (7 preceding siblings ...)
2025-08-19 10:56 ` [PATCH RFC 08/11] add-patch: split out `struct interactive_options` Patrick Steinhardt
@ 2025-08-19 10:56 ` Patrick Steinhardt
2025-08-19 10:56 ` [PATCH RFC 10/11] add-patch: add support for in-memory index patching Patrick Steinhardt
` (11 subsequent siblings)
20 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-19 10:56 UTC (permalink / raw)
To: git
With the preceding commit we have split out interactive configuration
that is used by both "git add -p" and "git add -i". But we still
initialize that configuration in the "add -p" subsystem by calling
`init_add_i_state()`, even though we only do so to initialize the
interactive configuration as well as a repository pointer.
Stop doing so and instead store and initialize the interactive
configuration in `struct add_p_state` directly.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
add-patch.c | 68 ++++++++++++++++++++++++++++++++-----------------------------
1 file changed, 36 insertions(+), 32 deletions(-)
diff --git a/add-patch.c b/add-patch.c
index 45bc254e0c..1bcbc91de9 100644
--- a/add-patch.c
+++ b/add-patch.c
@@ -2,7 +2,6 @@
#define DISABLE_SIGN_COMPARE_WARNINGS
#include "git-compat-util.h"
-#include "add-interactive.h"
#include "add-patch.h"
#include "advice.h"
#include "config.h"
@@ -263,7 +262,8 @@ struct hunk {
};
struct add_p_state {
- struct add_i_state s;
+ struct repository *r;
+ struct interactive_config cfg;
struct strbuf answer, buf;
/* parsed diff */
@@ -385,7 +385,7 @@ static void add_p_state_clear(struct add_p_state *s)
for (i = 0; i < s->file_diff_nr; i++)
free(s->file_diff[i].hunk);
free(s->file_diff);
- clear_add_i_state(&s->s);
+ interactive_config_clear(&s->cfg);
}
__attribute__((format (printf, 2, 3)))
@@ -394,9 +394,9 @@ static void err(struct add_p_state *s, const char *fmt, ...)
va_list args;
va_start(args, fmt);
- fputs(s->s.cfg.error_color, stdout);
+ fputs(s->cfg.error_color, stdout);
vprintf(fmt, args);
- puts(s->s.cfg.reset_color);
+ puts(s->cfg.reset_color);
va_end(args);
}
@@ -414,7 +414,7 @@ static void setup_child_process(struct add_p_state *s,
cp->git_cmd = 1;
strvec_pushf(&cp->env,
- INDEX_ENVIRONMENT "=%s", s->s.r->index_file);
+ INDEX_ENVIRONMENT "=%s", s->r->index_file);
}
static int parse_range(const char **p,
@@ -519,12 +519,12 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
int res;
strvec_pushv(&args, s->mode->diff_cmd);
- if (s->s.cfg.context != -1)
- strvec_pushf(&args, "--unified=%i", s->s.cfg.context);
- if (s->s.cfg.interhunkcontext != -1)
- strvec_pushf(&args, "--inter-hunk-context=%i", s->s.cfg.interhunkcontext);
- if (s->s.cfg.interactive_diff_algorithm)
- strvec_pushf(&args, "--diff-algorithm=%s", s->s.cfg.interactive_diff_algorithm);
+ if (s->cfg.context != -1)
+ strvec_pushf(&args, "--unified=%i", s->cfg.context);
+ if (s->cfg.interhunkcontext != -1)
+ strvec_pushf(&args, "--inter-hunk-context=%i", s->cfg.interhunkcontext);
+ if (s->cfg.interactive_diff_algorithm)
+ strvec_pushf(&args, "--diff-algorithm=%s", s->cfg.interactive_diff_algorithm);
if (s->revision) {
struct object_id oid;
strvec_push(&args,
@@ -555,7 +555,7 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
if (want_color_fd(1, -1)) {
struct child_process colored_cp = CHILD_PROCESS_INIT;
- const char *diff_filter = s->s.cfg.interactive_diff_filter;
+ const char *diff_filter = s->cfg.interactive_diff_filter;
setup_child_process(s, &colored_cp, NULL);
xsnprintf((char *)args.v[color_arg_index], 8, "--color");
@@ -788,7 +788,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
hunk->colored_end - hunk->colored_start);
return;
} else {
- strbuf_addstr(out, s->s.cfg.fraginfo_color);
+ strbuf_addstr(out, s->cfg.fraginfo_color);
p = s->colored.buf + header->colored_extra_start;
len = header->colored_extra_end
- header->colored_extra_start;
@@ -810,7 +810,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
if (len)
strbuf_add(out, p, len);
else if (colored)
- strbuf_addf(out, "%s\n", s->s.cfg.reset_color);
+ strbuf_addf(out, "%s\n", s->cfg.reset_color);
else
strbuf_addch(out, '\n');
}
@@ -1198,12 +1198,12 @@ static void recolor_hunk(struct add_p_state *s, struct hunk *hunk)
strbuf_addstr(&s->colored,
plain[current] == '-' ?
- s->s.cfg.file_old_color :
+ s->cfg.file_old_color :
plain[current] == '+' ?
- s->s.cfg.file_new_color :
- s->s.cfg.context_color);
+ s->cfg.file_new_color :
+ s->cfg.context_color);
strbuf_add(&s->colored, plain + current, eol - current);
- strbuf_addstr(&s->colored, s->s.cfg.reset_color);
+ strbuf_addstr(&s->colored, s->cfg.reset_color);
if (next > eol)
strbuf_add(&s->colored, plain + eol, next - eol);
current = next;
@@ -1322,7 +1322,7 @@ static int run_apply_check(struct add_p_state *s,
static int read_single_character(struct add_p_state *s)
{
- if (s->s.cfg.use_single_key) {
+ if (s->cfg.use_single_key) {
int res = read_key_without_echo(&s->answer);
printf("%s\n", res == EOF ? "" : s->answer.buf);
return res;
@@ -1336,7 +1336,7 @@ static int read_single_character(struct add_p_state *s)
static int prompt_yesno(struct add_p_state *s, const char *prompt)
{
for (;;) {
- color_fprintf(stdout, s->s.cfg.prompt_color, "%s", _(prompt));
+ color_fprintf(stdout, s->cfg.prompt_color, "%s", _(prompt));
fflush(stdout);
if (read_single_character(s) == EOF)
return -1;
@@ -1617,15 +1617,15 @@ static int patch_update_file(struct add_p_state *s,
else
prompt_mode_type = PROMPT_HUNK;
- printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->s.cfg.prompt_color,
+ printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->cfg.prompt_color,
(uintmax_t)hunk_index + 1,
(uintmax_t)(file_diff->hunk_nr
? file_diff->hunk_nr
: 1));
printf(_(s->mode->prompt_mode[prompt_mode_type]),
s->buf.buf);
- if (*s->s.cfg.reset_color)
- fputs(s->s.cfg.reset_color, stdout);
+ if (*s->cfg.reset_color)
+ fputs(s->cfg.reset_color, stdout);
fflush(stdout);
if (read_single_character(s) == EOF)
break;
@@ -1782,7 +1782,7 @@ static int patch_update_file(struct add_p_state *s,
err(s, _("Sorry, cannot split this hunk"));
} else if (!split_hunk(s, file_diff,
hunk - file_diff->hunk)) {
- color_fprintf_ln(stdout, s->s.cfg.header_color,
+ color_fprintf_ln(stdout, s->cfg.header_color,
_("Split into %d hunks."),
(int)splittable_into);
rendered_hunk_index = -1;
@@ -1800,7 +1800,7 @@ static int patch_update_file(struct add_p_state *s,
} else if (s->answer.buf[0] == '?') {
const char *p = _(help_patch_remainder), *eol = p;
- color_fprintf(stdout, s->s.cfg.help_color, "%s",
+ color_fprintf(stdout, s->cfg.help_color, "%s",
_(s->mode->help_patch_text));
/*
@@ -1818,7 +1818,7 @@ static int patch_update_file(struct add_p_state *s,
if (*p != '?' && !strchr(s->buf.buf, *p))
continue;
- color_fprintf_ln(stdout, s->s.cfg.help_color,
+ color_fprintf_ln(stdout, s->cfg.help_color,
"%.*s", (int)(eol - p), p);
}
} else {
@@ -1838,7 +1838,7 @@ static int patch_update_file(struct add_p_state *s,
strbuf_reset(&s->buf);
reassemble_patch(s, file_diff, 0, &s->buf);
- discard_index(s->s.r->index);
+ discard_index(s->r->index);
if (s->mode->apply_for_checkout)
apply_for_checkout(s, &s->buf,
s->mode->is_reverse);
@@ -1849,8 +1849,8 @@ static int patch_update_file(struct add_p_state *s,
NULL, 0, NULL, 0))
error(_("'git apply' failed"));
}
- if (repo_read_index(s->s.r) >= 0)
- repo_refresh_and_write_index(s->s.r, REFRESH_QUIET, 0,
+ if (repo_read_index(s->r) >= 0)
+ repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,
1, NULL, NULL, NULL);
}
@@ -1863,11 +1863,15 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
const struct pathspec *ps)
{
struct add_p_state s = {
- { r }, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
+ .r = r,
+ .answer = STRBUF_INIT,
+ .buf = STRBUF_INIT,
+ .plain = STRBUF_INIT,
+ .colored = STRBUF_INIT,
};
size_t i, binary_count = 0;
- init_add_i_state(&s.s, r, opts);
+ interactive_config_init(&s.cfg, r, opts);
if (mode == ADD_P_STASH)
s.mode = &patch_mode_stash;
--
2.51.0.261.g7ce5a0a67e.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH RFC 10/11] add-patch: add support for in-memory index patching
2025-08-19 10:55 [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing Patrick Steinhardt
` (8 preceding siblings ...)
2025-08-19 10:56 ` [PATCH RFC 09/11] add-patch: remove dependency on "add-interactive" subsystem Patrick Steinhardt
@ 2025-08-19 10:56 ` Patrick Steinhardt
2025-08-20 21:15 ` D. Ben Knoble
2025-08-19 10:56 ` [PATCH RFC 11/11] builtin/history: implement "split" subcommand Patrick Steinhardt
` (10 subsequent siblings)
20 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-19 10:56 UTC (permalink / raw)
To: git
With `run_add_p()` callers have the ability to apply changes from a
specific revision to a repository's index. This infra supports several
different modes, like for example applying changes to the index,
worktree or both.
One feature that is missing though is the ability to apply changes to an
in-memory index different from the repository's index. Add a new
function `run_add_p_index()` to plug this gap.
This new function will be used in a subsequent commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
add-patch.c | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
add-patch.h | 8 +++++
2 files changed, 116 insertions(+), 3 deletions(-)
diff --git a/add-patch.c b/add-patch.c
index 1bcbc91de9..adef20c02b 100644
--- a/add-patch.c
+++ b/add-patch.c
@@ -4,11 +4,13 @@
#include "git-compat-util.h"
#include "add-patch.h"
#include "advice.h"
+#include "commit.h"
#include "config.h"
#include "diff.h"
#include "editor.h"
#include "environment.h"
#include "gettext.h"
+#include "hex.h"
#include "object-name.h"
#include "pager.h"
#include "read-cache-ll.h"
@@ -263,6 +265,8 @@ struct hunk {
struct add_p_state {
struct repository *r;
+ struct index_state *index;
+ const char *index_file;
struct interactive_config cfg;
struct strbuf answer, buf;
@@ -414,7 +418,7 @@ static void setup_child_process(struct add_p_state *s,
cp->git_cmd = 1;
strvec_pushf(&cp->env,
- INDEX_ENVIRONMENT "=%s", s->r->index_file);
+ INDEX_ENVIRONMENT "=%s", s->index_file);
}
static int parse_range(const char **p,
@@ -1838,7 +1842,7 @@ static int patch_update_file(struct add_p_state *s,
strbuf_reset(&s->buf);
reassemble_patch(s, file_diff, 0, &s->buf);
- discard_index(s->r->index);
+ discard_index(s->index);
if (s->mode->apply_for_checkout)
apply_for_checkout(s, &s->buf,
s->mode->is_reverse);
@@ -1849,9 +1853,12 @@ static int patch_update_file(struct add_p_state *s,
NULL, 0, NULL, 0))
error(_("'git apply' failed"));
}
- if (repo_read_index(s->r) >= 0)
+ read_index_from(s->index, s->index_file, s->r->gitdir);
+ if (read_index_from(s->index, s->index_file, s->r->gitdir) >= 0 &&
+ s->index == s->r->index) {
repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,
1, NULL, NULL, NULL);
+ }
}
putchar('\n');
@@ -1864,6 +1871,8 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
{
struct add_p_state s = {
.r = r,
+ .index = r->index,
+ .index_file = r->index_file,
.answer = STRBUF_INIT,
.buf = STRBUF_INIT,
.plain = STRBUF_INIT,
@@ -1922,3 +1931,99 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
add_p_state_clear(&s);
return 0;
}
+
+int run_add_p_index(struct repository *r,
+ struct index_state *index,
+ const char *index_file,
+ struct interactive_options *opts,
+ const char *revision,
+ const struct pathspec *ps)
+{
+ struct patch_mode mode = {
+ .apply_args = { "--cached", NULL },
+ .apply_check_args = { "--cached", NULL },
+ .prompt_mode = {
+ N_("Stage mode change [y,n,q,a,d%s,?]? "),
+ N_("Stage deletion [y,n,q,a,d%s,?]? "),
+ N_("Stage addition [y,n,q,a,d%s,?]? "),
+ N_("Stage this hunk [y,n,q,a,d%s,?]? ")
+ },
+ .edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk "
+ "will immediately be marked for staging."),
+ .help_patch_text =
+ N_("y - stage this hunk\n"
+ "n - do not stage this hunk\n"
+ "q - quit; do not stage this hunk or any of the remaining "
+ "ones\n"
+ "a - stage this hunk and all later hunks in the file\n"
+ "d - do not stage this hunk or any of the later hunks in "
+ "the file\n"),
+ .index_only = 1,
+ };
+ struct add_p_state s = {
+ .r = r,
+ .index = index,
+ .index_file = index_file,
+ .answer = STRBUF_INIT,
+ .buf = STRBUF_INIT,
+ .plain = STRBUF_INIT,
+ .colored = STRBUF_INIT,
+ .mode = &mode,
+ .revision = revision,
+ };
+ struct strbuf parent_revision = STRBUF_INIT;
+ char parent_tree_oid[GIT_MAX_HEXSZ + 1];
+ size_t binary_count = 0;
+ struct commit *commit;
+ int ret;
+
+ commit = lookup_commit_reference_by_name(revision);
+ if (!commit) {
+ err(&s, _("Revision does not refer to a commit"));
+ ret = -1;
+ goto out;
+ }
+
+ if (commit->parents)
+ oid_to_hex_r(parent_tree_oid, get_commit_tree_oid(commit->parents->item));
+ else
+ oid_to_hex_r(parent_tree_oid, r->hash_algo->empty_tree);
+
+ strbuf_addf(&parent_revision, "%s~", revision);
+ mode.diff_cmd[0] = "diff-tree";
+ mode.diff_cmd[1] = "-r";
+ mode.diff_cmd[2] = parent_tree_oid;
+
+ interactive_config_init(&s.cfg, r, opts);
+
+ if (parse_diff(&s, ps) < 0) {
+ ret = -1;
+ goto out;
+ }
+
+ for (size_t i = 0; i < s.file_diff_nr; i++) {
+ if (s.file_diff[i].binary && !s.file_diff[i].hunk_nr)
+ binary_count++;
+ else if (patch_update_file(&s, s.file_diff + i))
+ break;
+ }
+
+ if (s.file_diff_nr == 0) {
+ err(&s, _("No changes."));
+ ret = -1;
+ goto out;
+ }
+
+ if (binary_count == s.file_diff_nr) {
+ err(&s, _("Only binary files changed."));
+ ret = -1;
+ goto out;
+ }
+
+ ret = 0;
+
+out:
+ strbuf_release(&parent_revision);
+ add_p_state_clear(&s);
+ return ret;
+}
diff --git a/add-patch.h b/add-patch.h
index 51c0d7bce9..d0edfec936 100644
--- a/add-patch.h
+++ b/add-patch.h
@@ -3,6 +3,7 @@
#include "color.h"
+struct index_state;
struct pathspec;
struct repository;
@@ -50,4 +51,11 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
struct interactive_options *opts, const char *revision,
const struct pathspec *ps);
+int run_add_p_index(struct repository *r,
+ struct index_state *index,
+ const char *index_file,
+ struct interactive_options *opts,
+ const char *revision,
+ const struct pathspec *ps);
+
#endif
--
2.51.0.261.g7ce5a0a67e.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH RFC 10/11] add-patch: add support for in-memory index patching
2025-08-19 10:56 ` [PATCH RFC 10/11] add-patch: add support for in-memory index patching Patrick Steinhardt
@ 2025-08-20 21:15 ` D. Ben Knoble
2025-08-22 12:21 ` Patrick Steinhardt
0 siblings, 1 reply; 278+ messages in thread
From: D. Ben Knoble @ 2025-08-20 21:15 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git
On Wed, Aug 20, 2025 at 4:17 AM Patrick Steinhardt <ps@pks.im> wrote:
>
> With `run_add_p()` callers have the ability to apply changes from a
> specific revision to a repository's index. This infra supports several
> different modes, like for example applying changes to the index,
> worktree or both.
>
> One feature that is missing though is the ability to apply changes to an
> in-memory index different from the repository's index. Add a new
> function `run_add_p_index()` to plug this gap.
>
> This new function will be used in a subsequent commit.
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> add-patch.c | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
> add-patch.h | 8 +++++
> 2 files changed, 116 insertions(+), 3 deletions(-)
>
> diff --git a/add-patch.c b/add-patch.c
> index 1bcbc91de9..adef20c02b 100644
> --- a/add-patch.c
> +++ b/add-patch.c
> @@ -1849,9 +1853,12 @@ static int patch_update_file(struct add_p_state *s,
> NULL, 0, NULL, 0))
> error(_("'git apply' failed"));
> }
> - if (repo_read_index(s->r) >= 0)
> + read_index_from(s->index, s->index_file, s->r->gitdir);
> + if (read_index_from(s->index, s->index_file, s->r->gitdir) >= 0 &&
> + s->index == s->r->index) {
> repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,
> 1, NULL, NULL, NULL);
> + }
> }
Is this call to read_index_from duplicated? I don't see anything that
indicates that would be desirable here.
>
> putchar('\n');
> @@ -1864,6 +1871,8 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
> {
> struct add_p_state s = {
> .r = r,
> + .index = r->index,
> + .index_file = r->index_file,
> .answer = STRBUF_INIT,
> .buf = STRBUF_INIT,
> .plain = STRBUF_INIT,
> @@ -1922,3 +1931,99 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
> add_p_state_clear(&s);
> return 0;
> }
> +
> +int run_add_p_index(struct repository *r,
> + struct index_state *index,
> + const char *index_file,
> + struct interactive_options *opts,
> + const char *revision,
> + const struct pathspec *ps)
> +{
> + struct patch_mode mode = {
> + .apply_args = { "--cached", NULL },
> + .apply_check_args = { "--cached", NULL },
> + .prompt_mode = {
> + N_("Stage mode change [y,n,q,a,d%s,?]? "),
> + N_("Stage deletion [y,n,q,a,d%s,?]? "),
> + N_("Stage addition [y,n,q,a,d%s,?]? "),
> + N_("Stage this hunk [y,n,q,a,d%s,?]? ")
> + },
> + .edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk "
> + "will immediately be marked for staging."),
> + .help_patch_text =
> + N_("y - stage this hunk\n"
> + "n - do not stage this hunk\n"
> + "q - quit; do not stage this hunk or any of the remaining "
> + "ones\n"
> + "a - stage this hunk and all later hunks in the file\n"
> + "d - do not stage this hunk or any of the later hunks in "
> + "the file\n"),
> + .index_only = 1,
> + };
> + struct add_p_state s = {
> + .r = r,
> + .index = index,
> + .index_file = index_file,
> + .answer = STRBUF_INIT,
> + .buf = STRBUF_INIT,
> + .plain = STRBUF_INIT,
> + .colored = STRBUF_INIT,
> + .mode = &mode,
> + .revision = revision,
> + };
> + struct strbuf parent_revision = STRBUF_INIT;
> + char parent_tree_oid[GIT_MAX_HEXSZ + 1];
> + size_t binary_count = 0;
> + struct commit *commit;
> + int ret;
> +
> + commit = lookup_commit_reference_by_name(revision);
> + if (!commit) {
> + err(&s, _("Revision does not refer to a commit"));
> + ret = -1;
> + goto out;
> + }
> +
> + if (commit->parents)
> + oid_to_hex_r(parent_tree_oid, get_commit_tree_oid(commit->parents->item));
> + else
> + oid_to_hex_r(parent_tree_oid, r->hash_algo->empty_tree);
> +
> + strbuf_addf(&parent_revision, "%s~", revision);
> + mode.diff_cmd[0] = "diff-tree";
> + mode.diff_cmd[1] = "-r";
> + mode.diff_cmd[2] = parent_tree_oid;
> +
> + interactive_config_init(&s.cfg, r, opts);
> +
> + if (parse_diff(&s, ps) < 0) {
I noticed run_add_p() calls discard_index() right before parse_diff()
[but it also reads/refreshes the index there]. Sounds like that's not
something we need for in-memory indices?
> + ret = -1;
> + goto out;
> + }
> +
> + for (size_t i = 0; i < s.file_diff_nr; i++) {
> + if (s.file_diff[i].binary && !s.file_diff[i].hunk_nr)
> + binary_count++;
> + else if (patch_update_file(&s, s.file_diff + i))
> + break;
> + }
> +
> + if (s.file_diff_nr == 0) {
> + err(&s, _("No changes."));
> + ret = -1;
> + goto out;
> + }
> +
> + if (binary_count == s.file_diff_nr) {
> + err(&s, _("Only binary files changed."));
> + ret = -1;
> + goto out;
> + }
> +
> + ret = 0;
> +
> +out:
> + strbuf_release(&parent_revision);
> + add_p_state_clear(&s);
> + return ret;
> +}
This single call to add_p_state_clear() is probably easier to follow
than the original in run_add_p(). Nice.
--
D. Ben Knoble
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC 10/11] add-patch: add support for in-memory index patching
2025-08-20 21:15 ` D. Ben Knoble
@ 2025-08-22 12:21 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-22 12:21 UTC (permalink / raw)
To: D. Ben Knoble; +Cc: git
On Wed, Aug 20, 2025 at 05:15:03PM -0400, D. Ben Knoble wrote:
> On Wed, Aug 20, 2025 at 4:17 AM Patrick Steinhardt <ps@pks.im> wrote:
> > diff --git a/add-patch.c b/add-patch.c
> > index 1bcbc91de9..adef20c02b 100644
> > --- a/add-patch.c
> > +++ b/add-patch.c
> > @@ -1849,9 +1853,12 @@ static int patch_update_file(struct add_p_state *s,
> > NULL, 0, NULL, 0))
> > error(_("'git apply' failed"));
> > }
> > - if (repo_read_index(s->r) >= 0)
> > + read_index_from(s->index, s->index_file, s->r->gitdir);
> > + if (read_index_from(s->index, s->index_file, s->r->gitdir) >= 0 &&
> > + s->index == s->r->index) {
> > repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,
> > 1, NULL, NULL, NULL);
> > + }
> > }
>
> Is this call to read_index_from duplicated? I don't see anything that
> indicates that would be desirable here.
Indeed, good catch!
> > @@ -1922,3 +1931,99 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
> > add_p_state_clear(&s);
> > return 0;
> > }
> > +
> > +int run_add_p_index(struct repository *r,
> > + struct index_state *index,
> > + const char *index_file,
> > + struct interactive_options *opts,
> > + const char *revision,
> > + const struct pathspec *ps)
> > +{
> > + struct patch_mode mode = {
> > + .apply_args = { "--cached", NULL },
> > + .apply_check_args = { "--cached", NULL },
> > + .prompt_mode = {
> > + N_("Stage mode change [y,n,q,a,d%s,?]? "),
> > + N_("Stage deletion [y,n,q,a,d%s,?]? "),
> > + N_("Stage addition [y,n,q,a,d%s,?]? "),
> > + N_("Stage this hunk [y,n,q,a,d%s,?]? ")
> > + },
> > + .edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk "
> > + "will immediately be marked for staging."),
> > + .help_patch_text =
> > + N_("y - stage this hunk\n"
> > + "n - do not stage this hunk\n"
> > + "q - quit; do not stage this hunk or any of the remaining "
> > + "ones\n"
> > + "a - stage this hunk and all later hunks in the file\n"
> > + "d - do not stage this hunk or any of the later hunks in "
> > + "the file\n"),
> > + .index_only = 1,
> > + };
> > + struct add_p_state s = {
> > + .r = r,
> > + .index = index,
> > + .index_file = index_file,
> > + .answer = STRBUF_INIT,
> > + .buf = STRBUF_INIT,
> > + .plain = STRBUF_INIT,
> > + .colored = STRBUF_INIT,
> > + .mode = &mode,
> > + .revision = revision,
> > + };
> > + struct strbuf parent_revision = STRBUF_INIT;
> > + char parent_tree_oid[GIT_MAX_HEXSZ + 1];
> > + size_t binary_count = 0;
> > + struct commit *commit;
> > + int ret;
> > +
> > + commit = lookup_commit_reference_by_name(revision);
> > + if (!commit) {
> > + err(&s, _("Revision does not refer to a commit"));
> > + ret = -1;
> > + goto out;
> > + }
> > +
> > + if (commit->parents)
> > + oid_to_hex_r(parent_tree_oid, get_commit_tree_oid(commit->parents->item));
> > + else
> > + oid_to_hex_r(parent_tree_oid, r->hash_algo->empty_tree);
> > +
> > + strbuf_addf(&parent_revision, "%s~", revision);
> > + mode.diff_cmd[0] = "diff-tree";
> > + mode.diff_cmd[1] = "-r";
> > + mode.diff_cmd[2] = parent_tree_oid;
> > +
> > + interactive_config_init(&s.cfg, r, opts);
> > +
> > + if (parse_diff(&s, ps) < 0) {
>
> I noticed run_add_p() calls discard_index() right before parse_diff()
> [but it also reads/refreshes the index there]. Sounds like that's not
> something we need for in-memory indices?
No. We don't want to discard contents, as it might even be that somebody
has an index that we want to apply multiple revisions to. Discarding it
is not sensible in that case.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH RFC 11/11] builtin/history: implement "split" subcommand
2025-08-19 10:55 [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing Patrick Steinhardt
` (9 preceding siblings ...)
2025-08-19 10:56 ` [PATCH RFC 10/11] add-patch: add support for in-memory index patching Patrick Steinhardt
@ 2025-08-19 10:56 ` Patrick Steinhardt
2025-08-20 21:27 ` D. Ben Knoble
2025-08-23 16:37 ` Jean-Noël AVILA
2025-08-19 21:28 ` [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing D. Ben Knoble
` (9 subsequent siblings)
20 siblings, 2 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-19 10:56 UTC (permalink / raw)
To: git
It is quite a common use case that one wants to split up one commit into
multiple commits by moving parts of the changes of the original commit
out of it into a separate commit. This is quite an involved operation
though:
1. Identify the commit in question that is to be dropped.
2. Perform an interactive rebase on top of that commit's parent.
3. Modify the instruction sheet to "edit" the commit that is to be
split up.
4. Drop the commit via "git reset HEAD~".
5. Stage changes that should go into the first commit and commit it.
6. Stage changes that should go into the second commit and commit it.
7. Finalize the rebase.
This is quite complex, and overall I would claim that most people who
are not experts in Git would struggle with this flow.
Introduce a new "split" subcommand for git-history(1) to make this way
easier. All the user needs to do is to say `git history split $COMMIT`.
From hereon, Git asks the user which parts of the commit shall be moved
out into a separate commit and, once done, asks the user for the commit
message. Git then creates that split-out commit and applies the original
commit on top of it.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-history.adoc | 59 ++++++++
builtin/history.c | 245 +++++++++++++++++++++++++++++++++
t/meson.build | 1 +
t/t3452-history-split.sh | 304 +++++++++++++++++++++++++++++++++++++++++
4 files changed, 609 insertions(+)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index 6e8b4e1326..f0f1f2a093 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -10,6 +10,7 @@ SYNOPSIS
[synopsis]
git history drop [<options>] <revision>
git history reorder [<options>] <revision> --(before|after)=<revision>
+git history split [<options>] <revision> [--] [<pathspec>...]
DESCRIPTION
-----------
@@ -47,6 +48,26 @@ reorder <revision> (--before=<revision>|--after=<revision>)::
commit. The commits must be related to one another and must be
reachable from the current `HEAD` commit.
+split <revision> [--message=<message>] [--] [<pathspec>...]::
+ Interactively split up the commit into two commits by choosing
+ hunks introduced by it that will be moved into the new split-out
+ commit. These hunks will then be written into a new commit that
+ becomes the parent of the previous commit. The original commit
+ stays intact, except that its parent will be the newly split-out
+ commit.
++
+The commit message of the new commit will be asked for by launching the
+configured editor. Authorship of the commit will be the same as for the
+original commit.
++
+If passed, _<pathspec>_ can be used to limit which changes shall be split out
+of the original commit. Files not matching any of the pathspecs will remain
+part of the original commit. For more details about the _<pathspec>_ syntax,
+see the 'pathspec' entry.
++
+It is invalid to select either all or no hunks, as that would lead to
+one of the commits becoming empty.
+
EXAMPLES
--------
@@ -88,6 +109,44 @@ f44a46e third
bf7438d first
----------
+* Split a commit.
++
+----------
+$ git log --stat --oneline
+3f81232 (HEAD -> main) original
+ bar | 1 +
+ foo | 1 +
+ 2 files changed, 2 insertions(+)
+
+$ git history split HEAD --message="split-out commit"
+diff --git a/bar b/bar
+new file mode 100644
+index 0000000..5716ca5
+--- /dev/null
++++ b/bar
+@@ -0,0 +1 @@
++bar
+(1/1) Stage addition [y,n,q,a,d,e,p,?]? y
+
+diff --git a/foo b/foo
+new file mode 100644
+index 0000000..257cc56
+--- /dev/null
++++ b/foo
+@@ -0,0 +1 @@
++foo
+(1/1) Stage addition [y,n,q,a,d,e,p,?]? n
+
+$ git log --stat --oneline
+7cebe64 (HEAD -> main) original
+ foo | 1 +
+ 1 file changed, 1 insertion(+)
+d1582f3 split-out commit
+ bar | 1 +
+ 1 file changed, 1 insertion(+)
+----------
+
+
CONFIGURATION
-------------
diff --git a/builtin/history.c b/builtin/history.c
index de6073f557..b2dab826ac 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -1,16 +1,26 @@
+/* Required for `comment_line_str`. */
+#define USE_THE_REPOSITORY_VARIABLE
+
#include "builtin.h"
+#include "cache-tree.h"
#include "commit.h"
#include "commit-reach.h"
#include "config.h"
+#include "editor.h"
#include "environment.h"
#include "gettext.h"
#include "hex.h"
#include "object-name.h"
#include "parse-options.h"
+#include "path.h"
+#include "pathspec.h"
+#include "read-cache-ll.h"
#include "refs.h"
#include "reset.h"
#include "revision.h"
+#include "run-command.h"
#include "sequencer.h"
+#include "sparse-index.h"
static int collect_commits(struct repository *repo,
struct commit *old_commit,
@@ -424,6 +434,239 @@ static int cmd_history_reorder(int argc,
return ret;
}
+static int split_commit(struct repository *repo,
+ struct commit *original_commit,
+ struct pathspec *pathspec,
+ const char *commit_message,
+ struct object_id *out)
+{
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
+ struct strbuf index_file = STRBUF_INIT, split_message = STRBUF_INIT;
+ struct child_process read_tree_cmd = CHILD_PROCESS_INIT;
+ struct index_state index = INDEX_STATE_INIT(repo);
+ struct object_id original_commit_tree_oid, parent_tree_oid;
+ const char *original_message, *original_body, *ptr;
+ char original_commit_oid[GIT_MAX_HEXSZ + 1];
+ char *split_message_path = NULL, *original_author = NULL;
+ struct commit_list *parents = NULL;
+ struct commit *first_commit;
+ struct tree *split_tree;
+ size_t len;
+ int ret;
+
+ if (original_commit->parents)
+ parent_tree_oid = *get_commit_tree_oid(original_commit->parents->item);
+ else
+ oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
+ original_commit_tree_oid = *get_commit_tree_oid(original_commit);
+
+ /*
+ * Construct the first commit. This is done by taking the original
+ * commit parent's tree and selectively patching changes from the diff
+ * between that parent and its child.
+ */
+ repo_git_path_replace(repo, &index_file, "%s", "history-split.index");
+
+ read_tree_cmd.git_cmd = 1;
+ strvec_pushf(&read_tree_cmd.env, "GIT_INDEX_FILE=%s", index_file.buf);
+ strvec_push(&read_tree_cmd.args, "read-tree");
+ strvec_push(&read_tree_cmd.args, oid_to_hex(&parent_tree_oid));
+ ret = run_command(&read_tree_cmd);
+ if (ret < 0)
+ goto out;
+
+ ret = read_index_from(&index, index_file.buf, repo->gitdir);
+ if (ret < 0) {
+ ret = error(_("failed reading temporary index"));
+ goto out;
+ }
+
+ oid_to_hex_r(original_commit_oid, &original_commit->object.oid);
+ ret = run_add_p_index(repo, &index, index_file.buf, &interactive_opts,
+ original_commit_oid, pathspec);
+ if (ret < 0)
+ goto out;
+
+ split_tree = write_in_core_index_as_tree(repo, &index);
+ if (!split_tree) {
+ ret = error(_("failed split tree"));
+ goto out;
+ }
+
+ unlink(index_file.buf);
+
+ /*
+ * We disallow the cases where either the split-out commit or the
+ * original commit would become empty. Consequently, if we see that the
+ * new tree ID matches either of those trees we abort.
+ */
+ if (oideq(&split_tree->object.oid, &parent_tree_oid)) {
+ ret = error(_("split commit is empty"));
+ goto out;
+ } else if (oideq(&split_tree->object.oid, &original_commit_tree_oid)) {
+ ret = error(_("split commit tree matches original commit"));
+ goto out;
+ }
+
+ /* We retain authorship of the original commit. */
+ original_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);
+ ptr = find_commit_header(original_message, "author", &len);
+ if (ptr)
+ original_author = xmemdupz(ptr, len);
+
+ /*
+ * But we do ask the user for a new commit message. This is in contrast
+ * to the second commit, where we'll retain the original commit
+ * message.
+ */
+ if (!commit_message) {
+ split_message_path = repo_git_path(repo, "SPLIT_MSG");
+ strbuf_addch(&split_message, '\n');
+ strbuf_commented_addf(&split_message, comment_line_str,
+ _("Please enter a commit message for the split-out changes."));
+ write_file_buf(split_message_path, split_message.buf, split_message.len);
+
+ strbuf_reset(&split_message);
+ if (launch_editor(split_message_path, &split_message, NULL)) {
+ fprintf(stderr, _("Please supply the message using either -m or -F option.\n"));
+ ret = -1;
+ goto out;
+ }
+ strbuf_stripspace(&split_message, comment_line_str);
+ } else {
+ strbuf_addstr(&split_message, commit_message);
+ }
+ cleanup_message(&split_message, COMMIT_MSG_CLEANUP_ALL, 0);
+
+ ret = commit_tree(split_message.buf, split_message.len, &split_tree->object.oid,
+ original_commit->parents, &out[0], original_author, NULL);
+ if (ret < 0) {
+ ret = error(_("failed writing split-out commit"));
+ goto out;
+ }
+
+ /*
+ * The second commit is much simpler to construct, as we can simply use
+ * the original commit details, except that we adjust its parent to be
+ * the newly split-out commit.
+ */
+ find_commit_subject(original_message, &original_body);
+ first_commit = lookup_commit_reference(repo, &out[0]);
+ commit_list_append(first_commit, &parents);
+
+ ret = commit_tree(original_body, strlen(original_body), &original_commit_tree_oid,
+ parents, &out[1], original_author, NULL);
+ if (ret < 0) {
+ ret = error(_("failed writing second commit"));
+ goto out;
+ }
+
+ ret = 0;
+
+out:
+ if (index_file.len)
+ unlink(index_file.buf);
+ strbuf_release(&split_message);
+ strbuf_release(&index_file);
+ free_commit_list(parents);
+ free(split_message_path);
+ free(original_author);
+ release_index(&index);
+ return ret;
+}
+
+static int cmd_history_split(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo)
+{
+ const char * const usage[] = {
+ N_("git history split [<options>] <revision>"),
+ NULL,
+ };
+ const char *commit_message = NULL;
+ struct option options[] = {
+ OPT_STRING('m', "message", &commit_message, N_("message"), N_("commit message")),
+ OPT_END(),
+ };
+ struct commit *original_commit, *head;
+ struct strvec commits = STRVEC_INIT;
+ struct commit_list *list = NULL;
+ struct object_id split_commits[2];
+ struct pathspec pathspec = { 0 };
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ repo_config(repo, git_default_config, NULL);
+
+ original_commit = lookup_commit_reference_by_name(argv[0]);
+ if (!original_commit) {
+ ret = error(_("commit to be split cannot be found: %s"), argv[0]);
+ goto out;
+ }
+
+ if (original_commit->parents && original_commit->parents->next) {
+ ret = error(_("commit to be split must not be a merge commit"));
+ goto out;
+ }
+
+ head = lookup_commit_reference_by_name("HEAD");
+ if (!head) {
+ ret = error(_("could not resolve HEAD to a commit"));
+ goto out;
+ }
+
+ commit_list_append(original_commit, &list);
+ if (!repo_is_descendant_of(repo, original_commit, list)) {
+ ret = error (_("split commit must be reachable from current HEAD commit"));
+ goto out;
+ }
+
+ parse_pathspec(&pathspec, 0,
+ PATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,
+ prefix, argv + 1);
+
+ /*
+ * Collect the list of commits that we'll have to reapply now already.
+ * This ensures that we'll abort early on in case the range of commits
+ * contains merges, which we do not yet handle.
+ */
+ ret = collect_commits(repo, original_commit->parents ? original_commit->parents->item : NULL,
+ head, &commits);
+ if (ret < 0)
+ goto out;
+
+ /*
+ * Then we split up the commit and replace the original commit with the
+ * new new ones.
+ */
+ ret = split_commit(repo, original_commit, &pathspec,
+ commit_message, split_commits);
+ if (ret < 0)
+ goto out;
+
+ replace_commits(&commits, &original_commit->object.oid,
+ split_commits, ARRAY_SIZE(split_commits));
+
+ /*
+ * And now we pick commits in the new order on top of either the root
+ * commit or on top the old commit's parent.
+ */
+ ret = apply_commits(repo, &commits, head,
+ original_commit->parents ? original_commit->parents->item : NULL,
+ "split");
+ if (ret < 0)
+ goto out;
+
+ ret = 0;
+
+out:
+ clear_pathspec(&pathspec);
+ strvec_clear(&commits);
+ free_commit_list(list);
+ return ret;
+}
+
int cmd_history(int argc,
const char **argv,
const char *prefix,
@@ -432,12 +675,14 @@ int cmd_history(int argc,
const char * const usage[] = {
N_("git history drop [<options>] <revision>"),
N_("git history reorder [<options>] <revision> --(before|after)=<revision>"),
+ N_("git history split [<options>] <revision> [--] [<pathspec>...]"),
NULL,
};
parse_opt_subcommand_fn *fn = NULL;
struct option options[] = {
OPT_SUBCOMMAND("drop", &fn, cmd_history_drop),
OPT_SUBCOMMAND("reorder", &fn, cmd_history_reorder),
+ OPT_SUBCOMMAND("split", &fn, cmd_history_split),
OPT_END(),
};
diff --git a/t/meson.build b/t/meson.build
index 8eded9ec1b..80beac8c1f 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -378,6 +378,7 @@ integration_tests = [
't3438-rebase-broken-files.sh',
't3450-history-drop.sh',
't3451-history-reorder.sh',
+ 't3452-history-split.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
diff --git a/t/t3452-history-split.sh b/t/t3452-history-split.sh
new file mode 100755
index 0000000000..bc965b15b2
--- /dev/null
+++ b/t/t3452-history-split.sh
@@ -0,0 +1,304 @@
+#!/bin/sh
+
+test_description='tests for git-history split subcommand'
+
+. ./test-lib.sh
+
+set_fake_editor () {
+ write_script fake-editor.sh <<-\EOF &&
+ echo "split-out commit" >"$1"
+ EOF
+ test_set_editor "$(pwd)"/fake-editor.sh
+}
+
+expect_log () {
+ git log --format="%s" >actual &&
+ cat >expect &&
+ test_cmp expect actual
+}
+
+expect_tree_entries () {
+ git ls-tree --name-only "$1" >actual &&
+ cat >expect &&
+ test_cmp expect actual
+}
+
+test_expect_success 'refuses to work with merge commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ git branch branch &&
+ test_commit ours &&
+ git switch branch &&
+ test_commit theirs &&
+ git switch - &&
+ git merge theirs &&
+ test_must_fail git history split HEAD 2>err &&
+ test_grep "commit to be split must not be a merge commit" err &&
+ test_must_fail git history split HEAD~ 2>err &&
+ test_grep "cannot rearrange commit history with merges" err
+ )
+'
+
+test_expect_success 'can split up tip commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit initial &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ set_fake_editor &&
+ git history split HEAD <<-EOF &&
+ y
+ n
+ EOF
+
+ expect_log <<-EOF &&
+ split-me
+ split-out commit
+ initial
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ bar
+ initial.t
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ initial.t
+ EOF
+ )
+'
+
+test_expect_success 'can split up root commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m root &&
+ test_commit tip &&
+
+ set_fake_editor &&
+ git history split HEAD~ <<-EOF &&
+ y
+ n
+ EOF
+
+ expect_log <<-EOF &&
+ tip
+ root
+ split-out commit
+ EOF
+
+ expect_tree_entries HEAD~2 <<-EOF &&
+ bar
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ bar
+ foo
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ tip.t
+ EOF
+ )
+'
+
+test_expect_success 'can split up in-between commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit initial &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+ test_commit tip &&
+
+ set_fake_editor &&
+ git history split HEAD~ <<-EOF &&
+ y
+ n
+ EOF
+
+ expect_log <<-EOF &&
+ tip
+ split-me
+ split-out commit
+ initial
+ EOF
+
+ expect_tree_entries HEAD~2 <<-EOF &&
+ bar
+ initial.t
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ bar
+ foo
+ initial.t
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ initial.t
+ tip.t
+ EOF
+ )
+'
+
+test_expect_success 'can pick multiple hunks' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar baz foo qux &&
+ git add . &&
+ git commit -m split-me &&
+
+ git history split HEAD -m "split-out commit" <<-EOF &&
+ y
+ n
+ y
+ n
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ bar
+ foo
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ baz
+ foo
+ qux
+ EOF
+ )
+'
+
+
+test_expect_success 'can use only last hunk' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ git history split HEAD -m "split-out commit" <<-EOF &&
+ n
+ y
+ EOF
+
+ expect_log <<-EOF &&
+ split-me
+ split-out commit
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ foo
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ EOF
+ )
+'
+
+test_expect_success 'can specify message via option' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ git history split HEAD -m "message option" <<-EOF &&
+ y
+ n
+ EOF
+
+ expect_log <<-EOF
+ split-me
+ message option
+ EOF
+ )
+'
+
+test_expect_success 'can use pathspec to limit what gets split' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ git history split HEAD -m "message option" -- foo <<-EOF &&
+ y
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ foo
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ EOF
+ )
+'
+
+test_expect_success 'refuses to create empty split-out commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ test_must_fail git history split HEAD 2>err <<-EOF &&
+ n
+ n
+ EOF
+ test_grep "split commit is empty" err
+ )
+'
+
+test_expect_success 'refuses to create empty original commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ test_must_fail git history split HEAD 2>err <<-EOF &&
+ y
+ y
+ EOF
+ test_grep "split commit tree matches original commit" err
+ )
+'
+
+test_done
--
2.51.0.261.g7ce5a0a67e.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH RFC 11/11] builtin/history: implement "split" subcommand
2025-08-19 10:56 ` [PATCH RFC 11/11] builtin/history: implement "split" subcommand Patrick Steinhardt
@ 2025-08-20 21:27 ` D. Ben Knoble
2025-08-22 12:22 ` Patrick Steinhardt
2025-08-23 16:37 ` Jean-Noël AVILA
1 sibling, 1 reply; 278+ messages in thread
From: D. Ben Knoble @ 2025-08-20 21:27 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git
On Wed, Aug 20, 2025 at 5:05 AM Patrick Steinhardt <ps@pks.im> wrote:
> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
> index 6e8b4e1326..f0f1f2a093 100644
> --- a/Documentation/git-history.adoc
> +++ b/Documentation/git-history.adoc
> @@ -47,6 +48,26 @@ reorder <revision> (--before=<revision>|--after=<revision>)::
> commit. The commits must be related to one another and must be
> reachable from the current `HEAD` commit.
>
> +split <revision> [--message=<message>] [--] [<pathspec>...]::
> + Interactively split up the commit into two commits by choosing
> + hunks introduced by it that will be moved into the new split-out
> + commit. These hunks will then be written into a new commit that
> + becomes the parent of the previous commit. The original commit
> + stays intact, except that its parent will be the newly split-out
> + commit.
> ++
> +The commit message of the new commit will be asked for by launching the
> +configured editor. Authorship of the commit will be the same as for the
> +original commit.
> ++
> +If passed, _<pathspec>_ can be used to limit which changes shall be split out
> +of the original commit. Files not matching any of the pathspecs will remain
> +part of the original commit. For more details about the _<pathspec>_ syntax,
> +see the 'pathspec' entry.
Glossary entry?
> + /*
> + * But we do ask the user for a new commit message. This is in contrast
> + * to the second commit, where we'll retain the original commit
> + * message.
> + */
Interesting. I can see using the original as the template for _both_,
or the first instead of the second. jj's split works a little
differently (especially with their notion of descriptions), so I can't
use them as a reference for the behavior.
I suppose this is one of those "everybody has their preference"
things, but I think giving the message in both new commits as the
template gives splitters the most information available when writing
the message. (Of course, in my editor, I can presumably do something
like ":Git show -s <split-commit-ish>" if I want.)
> + if (!commit_message) {
> + split_message_path = repo_git_path(repo, "SPLIT_MSG");
> + strbuf_addch(&split_message, '\n');
> + strbuf_commented_addf(&split_message, comment_line_str,
> + _("Please enter a commit message for the split-out changes."));
> + write_file_buf(split_message_path, split_message.buf, split_message.len);
I also noticed the commented template differs substantially from the
regular commit template, and my editor doesn't recognize "SPLIT_MSG"
as a commit message file.
The latter can be fixed elsewhere, but for the former: perhaps it's
worth using the usual template with the wording here prepended?
Respecting commit.verbose / commit.status, too.
BTW, if I quit the editor with an error here, I'm left back where I
started. So I'd have to re-stage changes if I wanted to split again,
which is a bit different from how interactive rebase will leave me
with the partially staged changes. Obviously that's harder to do with
the in-memory index + automatic re-application of remaining patch when
finished, so maybe a note in the docs about this being "all or
nothing"?
Best,
D. Ben Knoble
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC 11/11] builtin/history: implement "split" subcommand
2025-08-20 21:27 ` D. Ben Knoble
@ 2025-08-22 12:22 ` Patrick Steinhardt
2025-08-22 18:08 ` Junio C Hamano
0 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-22 12:22 UTC (permalink / raw)
To: D. Ben Knoble; +Cc: git
On Wed, Aug 20, 2025 at 05:27:32PM -0400, D. Ben Knoble wrote:
> On Wed, Aug 20, 2025 at 5:05 AM Patrick Steinhardt <ps@pks.im> wrote:
> > diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
> > index 6e8b4e1326..f0f1f2a093 100644
> > --- a/Documentation/git-history.adoc
> > +++ b/Documentation/git-history.adoc
> > @@ -47,6 +48,26 @@ reorder <revision> (--before=<revision>|--after=<revision>)::
> > commit. The commits must be related to one another and must be
> > reachable from the current `HEAD` commit.
> >
> > +split <revision> [--message=<message>] [--] [<pathspec>...]::
> > + Interactively split up the commit into two commits by choosing
> > + hunks introduced by it that will be moved into the new split-out
> > + commit. These hunks will then be written into a new commit that
> > + becomes the parent of the previous commit. The original commit
> > + stays intact, except that its parent will be the newly split-out
> > + commit.
> > ++
> > +The commit message of the new commit will be asked for by launching the
> > +configured editor. Authorship of the commit will be the same as for the
> > +original commit.
> > ++
> > +If passed, _<pathspec>_ can be used to limit which changes shall be split out
> > +of the original commit. Files not matching any of the pathspecs will remain
> > +part of the original commit. For more details about the _<pathspec>_ syntax,
> > +see the 'pathspec' entry.
>
> Glossary entry?
Yup.
> > + /*
> > + * But we do ask the user for a new commit message. This is in contrast
> > + * to the second commit, where we'll retain the original commit
> > + * message.
> > + */
>
> Interesting. I can see using the original as the template for _both_,
> or the first instead of the second. jj's split works a little
> differently (especially with their notion of descriptions), so I can't
> use them as a reference for the behavior.
>
> I suppose this is one of those "everybody has their preference"
> things, but I think giving the message in both new commits as the
> template gives splitters the most information available when writing
> the message. (Of course, in my editor, I can presumably do something
> like ":Git show -s <split-commit-ish>" if I want.)
I think giving only the split-out changes is a reasonable default, but I
can totally see that we might eventually want to add a command line
option to change the behaviour.
> > + if (!commit_message) {
> > + split_message_path = repo_git_path(repo, "SPLIT_MSG");
> > + strbuf_addch(&split_message, '\n');
> > + strbuf_commented_addf(&split_message, comment_line_str,
> > + _("Please enter a commit message for the split-out changes."));
> > + write_file_buf(split_message_path, split_message.buf, split_message.len);
>
> I also noticed the commented template differs substantially from the
> regular commit template, and my editor doesn't recognize "SPLIT_MSG"
> as a commit message file.
>
> The latter can be fixed elsewhere, but for the former: perhaps it's
> worth using the usual template with the wording here prepended?
> Respecting commit.verbose / commit.status, too.
Yeah, that's something I wanted to get around to, but haven't yet. I
also noticed that it's not exactly easy to figure out what you're
currently editing without that lack of context.
I'll include that in v2.
> BTW, if I quit the editor with an error here, I'm left back where I
> started. So I'd have to re-stage changes if I wanted to split again,
> which is a bit different from how interactive rebase will leave me
> with the partially staged changes. Obviously that's harder to do with
> the in-memory index + automatic re-application of remaining patch when
> finished, so maybe a note in the docs about this being "all or
> nothing"?
Yeah, fair. I guess adding a note for now is the best way to go about
it, but this is certainly something we can and should iterate on in the
future.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC 11/11] builtin/history: implement "split" subcommand
2025-08-22 12:22 ` Patrick Steinhardt
@ 2025-08-22 18:08 ` Junio C Hamano
2025-08-24 16:03 ` Patrick Steinhardt
0 siblings, 1 reply; 278+ messages in thread
From: Junio C Hamano @ 2025-08-22 18:08 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: D. Ben Knoble, git
Patrick Steinhardt <ps@pks.im> writes:
>> Interesting. I can see using the original as the template for _both_,
>> or the first instead of the second. jj's split works a little
>> differently (especially with their notion of descriptions), so I can't
>> use them as a reference for the behavior.
>>
>> I suppose this is one of those "everybody has their preference"
>> things, but I think giving the message in both new commits as the
>> template gives splitters the most information available when writing
>> the message. (Of course, in my editor, I can presumably do something
>> like ":Git show -s <split-commit-ish>" if I want.)
In other words, removing is easy, while remembering and retyping is
harder.
When I split an existing commit, that is almost always because after
doing too many things in a single commit and the time I realize it
is when I am writing the commit message. So I would suggest to give
the same original message to both, to avoid losing information.
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC 11/11] builtin/history: implement "split" subcommand
2025-08-22 18:08 ` Junio C Hamano
@ 2025-08-24 16:03 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-24 16:03 UTC (permalink / raw)
To: Junio C Hamano; +Cc: D. Ben Knoble, git
On Fri, Aug 22, 2025 at 11:08:22AM -0700, Junio C Hamano wrote:
> Patrick Steinhardt <ps@pks.im> writes:
>
> >> Interesting. I can see using the original as the template for _both_,
> >> or the first instead of the second. jj's split works a little
> >> differently (especially with their notion of descriptions), so I can't
> >> use them as a reference for the behavior.
> >>
> >> I suppose this is one of those "everybody has their preference"
> >> things, but I think giving the message in both new commits as the
> >> template gives splitters the most information available when writing
> >> the message. (Of course, in my editor, I can presumably do something
> >> like ":Git show -s <split-commit-ish>" if I want.)
>
> In other words, removing is easy, while remembering and retyping is
> harder.
>
> When I split an existing commit, that is almost always because after
> doing too many things in a single commit and the time I realize it
> is when I am writing the commit message. So I would suggest to give
> the same original message to both, to avoid losing information.
For now I'll rework this a bit so that the editor will list all changes
in the split-out commit, similar to how git-commit(1) does it. That at
least makes it way easier to see what you're currently changing. I'll
think about this some more though.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC 11/11] builtin/history: implement "split" subcommand
2025-08-19 10:56 ` [PATCH RFC 11/11] builtin/history: implement "split" subcommand Patrick Steinhardt
2025-08-20 21:27 ` D. Ben Knoble
@ 2025-08-23 16:37 ` Jean-Noël AVILA
2025-08-24 16:02 ` Patrick Steinhardt
1 sibling, 1 reply; 278+ messages in thread
From: Jean-Noël AVILA @ 2025-08-23 16:37 UTC (permalink / raw)
To: git, Patrick Steinhardt
On Tuesday, 19 August 2025 12:56:07 CEST Patrick Steinhardt wrote:
> It is quite a common use case that one wants to split up one commit into
> multiple commits by moving parts of the changes of the original commit
> out of it into a separate commit. This is quite an involved operation
> though:
>
> 1. Identify the commit in question that is to be dropped.
>
> 2. Perform an interactive rebase on top of that commit's parent.
>
> 3. Modify the instruction sheet to "edit" the commit that is to be
> split up.
>
> 4. Drop the commit via "git reset HEAD~".
>
> 5. Stage changes that should go into the first commit and commit it.
>
> 6. Stage changes that should go into the second commit and commit it.
>
> 7. Finalize the rebase.
>
> This is quite complex, and overall I would claim that most people who
> are not experts in Git would struggle with this flow.
>
> Introduce a new "split" subcommand for git-history(1) to make this way
> easier. All the user needs to do is to say `git history split $COMMIT`.
> From hereon, Git asks the user which parts of the commit shall be moved
> out into a separate commit and, once done, asks the user for the commit
> message. Git then creates that split-out commit and applies the original
> commit on top of it.
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> Documentation/git-history.adoc | 59 ++++++++
> builtin/history.c | 245 +++++++++++++++++++++++++++++++++
> t/meson.build | 1 +
> t/t3452-history-split.sh | 304 ++++++++++++++++++++++++++++++++++++++
+++
> 4 files changed, 609 insertions(+)
>
> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
> index 6e8b4e1326..f0f1f2a093 100644
> --- a/Documentation/git-history.adoc
> +++ b/Documentation/git-history.adoc
> @@ -10,6 +10,7 @@ SYNOPSIS
> [synopsis]
> git history drop [<options>] <revision>
> git history reorder [<options>] <revision> --(before|after)=<revision>
> +git history split [<options>] <revision> [--] [<pathspec>...]
I just realized that there are no other <options> available for the first two
commands! So maybe simplify the synopsis for the time being.
The --message is specific to split, so maybe also cite is specifically here.
I
>
> DESCRIPTION
> -----------
> @@ -47,6 +48,26 @@ reorder <revision> (--before=<revision>|--
after=<revision>)::
> commit. The commits must be related to one another and must be
> reachable from the current `HEAD` commit.
>
> +split <revision> [--message=<message>] [--] [<pathspec>...]::
missing backticks. Also the order of options and <commit> are different from
the general synopsis. Is this order allowed?
> + Interactively split up the commit into two commits by choosing
You can use the placeholder in this sentence: "Interactively split up
<commit>..."
> + hunks introduced by it that will be moved into the new split-out
> + commit. These hunks will then be written into a new commit that
> + becomes the parent of the previous commit. The original commit
> + stays intact, except that its parent will be the newly split-out
> + commit.
> ++
> +The commit message of the new commit will be asked for by launching the
> +configured editor. Authorship of the commit will be the same as for the
> +original commit.
> ++
I guess this only happens if --message is not passed?
> +If passed, _<pathspec>_ can be used to limit which changes shall be split
out
> +of the original commit. Files not matching any of the pathspecs will remain
> +part of the original commit. For more details about the _<pathspec>_
syntax,
> +see the 'pathspec' entry.
You may have meant:
+
For more details, see the 'pathspec' entry in linkgit:gitglossary[7].
> ++
> +It is invalid to select either all or no hunks, as that would lead to
> +one of the commits becoming empty.
> +
> EXAMPLES
> --------
>
> @@ -88,6 +109,44 @@ f44a46e third
> bf7438d first
> ----------
>
> +* Split a commit.
> ++
> +----------
> +$ git log --stat --oneline
> +3f81232 (HEAD -> main) original
> + bar | 1 +
> + foo | 1 +
> + 2 files changed, 2 insertions(+)
> +
> +$ git history split HEAD --message="split-out commit"
> +diff --git a/bar b/bar
> +new file mode 100644
> +index 0000000..5716ca5
> +--- /dev/null
> ++++ b/bar
> +@@ -0,0 +1 @@
> ++bar
> +(1/1) Stage addition [y,n,q,a,d,e,p,?]? y
> +
> +diff --git a/foo b/foo
> +new file mode 100644
> +index 0000000..257cc56
> +--- /dev/null
> ++++ b/foo
> +@@ -0,0 +1 @@
> ++foo
> +(1/1) Stage addition [y,n,q,a,d,e,p,?]? n
> +
> +$ git log --stat --oneline
> +7cebe64 (HEAD -> main) original
> + foo | 1 +
> + 1 file changed, 1 insertion(+)
> +d1582f3 split-out commit
> + bar | 1 +
> + 1 file changed, 1 insertion(+)
> +----------
> +
> +
> CONFIGURATION
> -------------
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC 11/11] builtin/history: implement "split" subcommand
2025-08-23 16:37 ` Jean-Noël AVILA
@ 2025-08-24 16:02 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-24 16:02 UTC (permalink / raw)
To: Jean-Noël AVILA; +Cc: git
On Sat, Aug 23, 2025 at 06:37:23PM +0200, Jean-Noël AVILA wrote:
> On Tuesday, 19 August 2025 12:56:07 CEST Patrick Steinhardt wrote:
> > @@ -47,6 +48,26 @@ reorder <revision> (--before=<revision>|--
> after=<revision>)::
> > commit. The commits must be related to one another and must be
> > reachable from the current `HEAD` commit.
> >
> > +split <revision> [--message=<message>] [--] [<pathspec>...]::
>
> missing backticks. Also the order of options and <commit> are different from
> the general synopsis. Is this order allowed?
It is, but this usage definitely wasn't intended. Will fix.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing
2025-08-19 10:55 [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing Patrick Steinhardt
` (10 preceding siblings ...)
2025-08-19 10:56 ` [PATCH RFC 11/11] builtin/history: implement "split" subcommand Patrick Steinhardt
@ 2025-08-19 21:28 ` D. Ben Knoble
2025-08-20 6:54 ` Patrick Steinhardt
2025-08-20 17:36 ` Junio C Hamano
` (8 subsequent siblings)
20 siblings, 1 reply; 278+ messages in thread
From: D. Ben Knoble @ 2025-08-19 21:28 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git
On Tue, Aug 19, 2025 at 6:57 AM Patrick Steinhardt <ps@pks.im> wrote:
>
> Hi,
>
> over recent months I've been playing around with Jujutsu quite
> frequently. While I still prefer using Git, there's been a couple
> features in it that I really like and that I'd like to have in Git, as
> well.
Excellent! I'm looking forward to reading this series and playing with it.
Unfortunately, patches 8–11 got dropped on their way to me (but I see
the lore archive has them). Odd. (Not in spam or deleted messages,
either.)
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing
2025-08-19 21:28 ` [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing D. Ben Knoble
@ 2025-08-20 6:54 ` Patrick Steinhardt
2025-08-20 16:55 ` Ben Knoble
0 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-20 6:54 UTC (permalink / raw)
To: D. Ben Knoble; +Cc: git
On Tue, Aug 19, 2025 at 05:28:48PM -0400, D. Ben Knoble wrote:
> On Tue, Aug 19, 2025 at 6:57 AM Patrick Steinhardt <ps@pks.im> wrote:
> >
> > Hi,
> >
> > over recent months I've been playing around with Jujutsu quite
> > frequently. While I still prefer using Git, there's been a couple
> > features in it that I really like and that I'd like to have in Git, as
> > well.
>
> Excellent! I'm looking forward to reading this series and playing with it.
>
> Unfortunately, patches 8–11 got dropped on their way to me (but I see
> the lore archive has them). Odd. (Not in spam or deleted messages,
> either.)
It's a common issue with GMail that mails from the LKML get rate limited
quite aggressively. Konstantin Ryabitsev (kernel.org admin) often gives
the recommendation to not use GMail for mailing lists.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing
2025-08-20 6:54 ` Patrick Steinhardt
@ 2025-08-20 16:55 ` Ben Knoble
0 siblings, 0 replies; 278+ messages in thread
From: Ben Knoble @ 2025-08-20 16:55 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git
> Le 20 août 2025 à 03:56, Patrick Steinhardt <ps@pks.im> a écrit :
>
> On Tue, Aug 19, 2025 at 05:28:48PM -0400, D. Ben Knoble wrote:
>>> On Tue, Aug 19, 2025 at 6:57 AM Patrick Steinhardt <ps@pks.im> wrote:
>>>
>>> Hi,
>>>
>>> over recent months I've been playing around with Jujutsu quite
>>> frequently. While I still prefer using Git, there's been a couple
>>> features in it that I really like and that I'd like to have in Git, as
>>> well.
>>
>> Excellent! I'm looking forward to reading this series and playing with it.
>>
>> Unfortunately, patches 8–11 got dropped on their way to me (but I see
>> the lore archive has them). Odd. (Not in spam or deleted messages,
>> either.)
>
> It's a common issue with GMail that mails from the LKML get rate limited
> quite aggressively. Konstantin Ryabitsev (kernel.org admin) often gives
> the recommendation to not use GMail for mailing lists.
>
> Patrick
Indeed I just saw a mastodon thread on that topic today. I’ll get switched over eventually, but it’s not a top priority for me and hasn’t hindered my side so far. My apologies for any stress it puts on the ML infra, though.
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing
2025-08-19 10:55 [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing Patrick Steinhardt
` (11 preceding siblings ...)
2025-08-19 21:28 ` [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing D. Ben Knoble
@ 2025-08-20 17:36 ` Junio C Hamano
2025-08-20 17:49 ` Ben Knoble
2025-08-21 16:26 ` Sergey Organov
2025-08-24 1:25 ` Martin von Zweigbergk
` (7 subsequent siblings)
20 siblings, 2 replies; 278+ messages in thread
From: Junio C Hamano @ 2025-08-20 17:36 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git
Patrick Steinhardt <ps@pks.im> writes:
> In the end, I'd like us to learn from what people like about Jujutsu and
> apply those learnings to Git. We won't be able to apply all learnings
> from Jujutsu, as the workflow is quite different there due to the lack
> of the index. But other things we certainly can apply to Git directly.
>
> Note: This patch series currently builds on the cherry-pick infra.
> As such, when one hits a merge conflict one needs to `git cherry-pick
> --continue`, which is quite suboptimal. I didn't want to overpolish this
> series before getting some feedback, but it is something I'll fix in
> subsequent versions. Furthermore, the command for now bails out in the
> case where there's any merge commits in the history that is being
> rewritten. This is another restriction that can be lifted in the future.
Two comments.
- You would want to honor notes.rewriteref yourself, as cherry-pick
does not and that is deliberate [*].
- It is a sensible design decision to limit it to linear single
strand of pearls history. "history reword <commit>" when
<commit> can be reached from many branches along linear history
that rewrites all these commits on these branches would be handy.
There may need some way to say "these branches are protected, if
'history reword <commit>' needs to touch commits on any of these,
abort" and things like that.
[Footnote]
* "history edit" (aka "rebase") is an operation that "edits" the
history, once the edit finishes, the result is *the* history you
want, and the previous one is to be discarded (except for in
reflog). "cherry-pick" on the other hand is "I have this good
thing on this development track, I want an equivalent _copy_ of
it on _another_ track"---it merely is an easier and quicker way
than typing the same thing yourself on top of the other track,
and does not duplicate notes.
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing
2025-08-20 17:36 ` Junio C Hamano
@ 2025-08-20 17:49 ` Ben Knoble
2025-08-22 12:21 ` Patrick Steinhardt
2025-08-21 16:26 ` Sergey Organov
1 sibling, 1 reply; 278+ messages in thread
From: Ben Knoble @ 2025-08-20 17:49 UTC (permalink / raw)
To: Junio C Hamano; +Cc: Patrick Steinhardt, git
> Le 20 août 2025 à 13:39, Junio C Hamano <gitster@pobox.com> a écrit :
>
> Patrick Steinhardt <ps@pks.im> writes:
>
>> In the end, I'd like us to learn from what people like about Jujutsu and
>> apply those learnings to Git. We won't be able to apply all learnings
>> from Jujutsu, as the workflow is quite different there due to the lack
>> of the index. But other things we certainly can apply to Git directly.
>>
>> Note: This patch series currently builds on the cherry-pick infra.
>> As such, when one hits a merge conflict one needs to `git cherry-pick
>> --continue`, which is quite suboptimal. I didn't want to overpolish this
>> series before getting some feedback, but it is something I'll fix in
>> subsequent versions. Furthermore, the command for now bails out in the
>> case where there's any merge commits in the history that is being
>> rewritten. This is another restriction that can be lifted in the future.
>
> Two comments.
>
> - You would want to honor notes.rewriteref yourself, as cherry-pick
> does not and that is deliberate [*].
Seconded
> - It is a sensible design decision to limit it to linear single
> strand of pearls history. "history reword <commit>" when
> <commit> can be reached from many branches along linear history
> that rewrites all these commits on these branches would be handy.
> There may need some way to say "these branches are protected, if
> 'history reword <commit>' needs to touch commits on any of these,
> abort" and things like that.
This reminds me of looking at rebase’s update-refs through a mirror, and I think is similar to what jj actually does. In particular, editing a commit that is reachable from multiple non-overlapping branches could update all of them.
A reasonable heuristic for safety is “pushed,” but I would also want to be able to edit something in @{u}.. even if it’s in @{push} so that I can force-push a new version. I suspect jj might also have heuristics we can borrow.
PS thanks all for being willing to borrow improvements! Reminds me of Neovim inspiring improvements in Vim, which I get to benefit from without switching :)
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing
2025-08-20 17:49 ` Ben Knoble
@ 2025-08-22 12:21 ` Patrick Steinhardt
2025-08-22 17:58 ` Junio C Hamano
0 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-22 12:21 UTC (permalink / raw)
To: Ben Knoble; +Cc: Junio C Hamano, git
On Wed, Aug 20, 2025 at 01:49:40PM -0400, Ben Knoble wrote:
>
> > Le 20 août 2025 à 13:39, Junio C Hamano <gitster@pobox.com> a écrit :
> >
> > Patrick Steinhardt <ps@pks.im> writes:
> >
> >> In the end, I'd like us to learn from what people like about Jujutsu and
> >> apply those learnings to Git. We won't be able to apply all learnings
> >> from Jujutsu, as the workflow is quite different there due to the lack
> >> of the index. But other things we certainly can apply to Git directly.
> >>
> >> Note: This patch series currently builds on the cherry-pick infra.
> >> As such, when one hits a merge conflict one needs to `git cherry-pick
> >> --continue`, which is quite suboptimal. I didn't want to overpolish this
> >> series before getting some feedback, but it is something I'll fix in
> >> subsequent versions. Furthermore, the command for now bails out in the
> >> case where there's any merge commits in the history that is being
> >> rewritten. This is another restriction that can be lifted in the future.
> >
> > Two comments.
> >
> > - You would want to honor notes.rewriteref yourself, as cherry-pick
> > does not and that is deliberate [*].
>
> Seconded
Okay, I'll have a look at that. I'll probably not do that in v2 yet, but
will try to get it into v3.
> > - It is a sensible design decision to limit it to linear single
> > strand of pearls history. "history reword <commit>" when
> > <commit> can be reached from many branches along linear history
> > that rewrites all these commits on these branches would be handy.
> > There may need some way to say "these branches are protected, if
> > 'history reword <commit>' needs to touch commits on any of these,
> > abort" and things like that.
>
> This reminds me of looking at rebase’s update-refs through a mirror,
> and I think is similar to what jj actually does. In particular,
> editing a commit that is reachable from multiple non-overlapping
> branches could update all of them.
>
> A reasonable heuristic for safety is “pushed,” but I would also want
> to be able to edit something in @{u}.. even if it’s in @{push} so that
> I can force-push a new version. I suspect jj might also have
> heuristics we can borrow.
>
> PS thanks all for being willing to borrow improvements! Reminds me of
> Neovim inspiring improvements in Vim, which I get to benefit from
> without switching :)
At least initially I want to keep it simple, just to get this in at
first. So I'm trying to be quite defensive overall and die in all kinds
of situations that require more thought. That way it becomes way easier
to eventually extend the different subcommands to maybe lift some of the
current restrictions.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing
2025-08-22 12:21 ` Patrick Steinhardt
@ 2025-08-22 17:58 ` Junio C Hamano
0 siblings, 0 replies; 278+ messages in thread
From: Junio C Hamano @ 2025-08-22 17:58 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: Ben Knoble, git
Patrick Steinhardt <ps@pks.im> writes:
> At least initially I want to keep it simple, just to get this in at
> first. So I'm trying to be quite defensive overall and die in all kinds
> of situations that require more thought. That way it becomes way easier
> to eventually extend the different subcommands to maybe lift some of the
> current restrictions.
I like that approach very much. Thanks.
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing
2025-08-20 17:36 ` Junio C Hamano
2025-08-20 17:49 ` Ben Knoble
@ 2025-08-21 16:26 ` Sergey Organov
2025-08-21 17:21 ` Ben Knoble
1 sibling, 1 reply; 278+ messages in thread
From: Sergey Organov @ 2025-08-21 16:26 UTC (permalink / raw)
To: Junio C Hamano; +Cc: Patrick Steinhardt, git
Junio C Hamano <gitster@pobox.com> writes:
> Patrick Steinhardt <ps@pks.im> writes:
>
>> In the end, I'd like us to learn from what people like about Jujutsu and
>> apply those learnings to Git. We won't be able to apply all learnings
>> from Jujutsu, as the workflow is quite different there due to the lack
>> of the index. But other things we certainly can apply to Git directly.
>>
>> Note: This patch series currently builds on the cherry-pick infra.
>> As such, when one hits a merge conflict one needs to `git cherry-pick
>> --continue`, which is quite suboptimal. I didn't want to overpolish this
>> series before getting some feedback, but it is something I'll fix in
>> subsequent versions. Furthermore, the command for now bails out in the
>> case where there's any merge commits in the history that is being
>> rewritten. This is another restriction that can be lifted in the future.
>
> Two comments.
>
> - You would want to honor notes.rewriteref yourself, as cherry-pick
> does not and that is deliberate [*].
>
> - It is a sensible design decision to limit it to linear single
> strand of pearls history. "history reword <commit>" when
> <commit> can be reached from many branches along linear history
> that rewrites all these commits on these branches would be handy.
> There may need some way to say "these branches are protected, if
> 'history reword <commit>' needs to touch commits on any of these,
> abort" and things like that.
>
>
> [Footnote]
>
> * "history edit" (aka "rebase") is an operation that "edits" the
> history, once the edit finishes, the result is *the* history you
> want, and the previous one is to be discarded (except for in
> reflog). "cherry-pick" on the other hand is "I have this good
> thing on this development track, I want an equivalent _copy_ of
> it on _another_ track"---it merely is an easier and quicker way
> than typing the same thing yourself on top of the other track,
> and does not duplicate notes.
Unless I'm ignorant, "git rebase" (aka "history edit") lacks essential
feature though: in addition to saying: get "this" history and rebase it
"there", one should be able to say: get "that" history, and rebase it
"here" (aka cherry-pick on steroids), that also would eliminate the need
for 'git cherry-pick <range>' that (poorly) duplicates rebase
functionality.
If not to implement this as, say, "git rebase --pick", I'd like to see
this feature in the new "git history" command, but then it'd eventually
become yet another "git rebase"? Or will Git then aim to eventually
factor-out a lot of "git rebase" functionality into new "git history
rebase <what> <where>", or something like this?
Thanks,
Sergey Organov
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing
2025-08-21 16:26 ` Sergey Organov
@ 2025-08-21 17:21 ` Ben Knoble
2025-08-21 18:15 ` Sergey Organov
0 siblings, 1 reply; 278+ messages in thread
From: Ben Knoble @ 2025-08-21 17:21 UTC (permalink / raw)
To: Sergey Organov; +Cc: Junio C Hamano, Patrick Steinhardt, git
> Le 21 août 2025 à 12:51, Sergey Organov <sorganov@gmail.com> a écrit :
>
> Junio C Hamano <gitster@pobox.com> writes:
>
>> Patrick Steinhardt <ps@pks.im> writes:
>>
>>> In the end, I'd like us to learn from what people like about Jujutsu and
>>> apply those learnings to Git. We won't be able to apply all learnings
>>> from Jujutsu, as the workflow is quite different there due to the lack
>>> of the index. But other things we certainly can apply to Git directly.
>>>
>>> Note: This patch series currently builds on the cherry-pick infra.
>>> As such, when one hits a merge conflict one needs to `git cherry-pick
>>> --continue`, which is quite suboptimal. I didn't want to overpolish this
>>> series before getting some feedback, but it is something I'll fix in
>>> subsequent versions. Furthermore, the command for now bails out in the
>>> case where there's any merge commits in the history that is being
>>> rewritten. This is another restriction that can be lifted in the future.
>>
>> Two comments.
>>
>> - You would want to honor notes.rewriteref yourself, as cherry-pick
>> does not and that is deliberate [*].
>>
>> - It is a sensible design decision to limit it to linear single
>> strand of pearls history. "history reword <commit>" when
>> <commit> can be reached from many branches along linear history
>> that rewrites all these commits on these branches would be handy.
>> There may need some way to say "these branches are protected, if
>> 'history reword <commit>' needs to touch commits on any of these,
>> abort" and things like that.
>>
>>
>> [Footnote]
>>
>> * "history edit" (aka "rebase") is an operation that "edits" the
>> history, once the edit finishes, the result is *the* history you
>> want, and the previous one is to be discarded (except for in
>> reflog). "cherry-pick" on the other hand is "I have this good
>> thing on this development track, I want an equivalent _copy_ of
>> it on _another_ track"---it merely is an easier and quicker way
>> than typing the same thing yourself on top of the other track,
>> and does not duplicate notes.
>
> Unless I'm ignorant, "git rebase" (aka "history edit") lacks essential
> feature though: in addition to saying: get "this" history and rebase it
> "there", one should be able to say: get "that" history, and rebase it
> "here" (aka cherry-pick on steroids), that also would eliminate the need
> for 'git cherry-pick <range>' that (poorly) duplicates rebase
> functionality.
But isn’t that
git rebase --onto=<here> <that-upstream> <that>
? And why is that cherry-picking a range is a poor substitute [it is rather rebase that duplicates cherry-pick ;)]?
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing
2025-08-21 17:21 ` Ben Knoble
@ 2025-08-21 18:15 ` Sergey Organov
0 siblings, 0 replies; 278+ messages in thread
From: Sergey Organov @ 2025-08-21 18:15 UTC (permalink / raw)
To: Ben Knoble; +Cc: Junio C Hamano, Patrick Steinhardt, git
Ben Knoble <ben.knoble@gmail.com> writes:
>> Le 21 août 2025 à 12:51, Sergey Organov <sorganov@gmail.com> a écrit :
>>
>> Junio C Hamano <gitster@pobox.com> writes:
>>
>>> Patrick Steinhardt <ps@pks.im> writes:
>>>
>>>> In the end, I'd like us to learn from what people like about Jujutsu and
>>>> apply those learnings to Git. We won't be able to apply all learnings
>>>> from Jujutsu, as the workflow is quite different there due to the lack
>>>> of the index. But other things we certainly can apply to Git directly.
>>>>
>>>> Note: This patch series currently builds on the cherry-pick infra.
>>>> As such, when one hits a merge conflict one needs to `git cherry-pick
>>>> --continue`, which is quite suboptimal. I didn't want to overpolish this
>>>> series before getting some feedback, but it is something I'll fix in
>>>> subsequent versions. Furthermore, the command for now bails out in the
>>>> case where there's any merge commits in the history that is being
>>>> rewritten. This is another restriction that can be lifted in the future.
>>>
>>> Two comments.
>>>
>>> - You would want to honor notes.rewriteref yourself, as cherry-pick
>>> does not and that is deliberate [*].
>>>
>>> - It is a sensible design decision to limit it to linear single
>>> strand of pearls history. "history reword <commit>" when
>>> <commit> can be reached from many branches along linear history
>>> that rewrites all these commits on these branches would be handy.
>>> There may need some way to say "these branches are protected, if
>>> 'history reword <commit>' needs to touch commits on any of these,
>>> abort" and things like that.
>>>
>>>
>>> [Footnote]
>>>
>>> * "history edit" (aka "rebase") is an operation that "edits" the
>>> history, once the edit finishes, the result is *the* history you
>>> want, and the previous one is to be discarded (except for in
>>> reflog). "cherry-pick" on the other hand is "I have this good
>>> thing on this development track, I want an equivalent _copy_ of
>>> it on _another_ track"---it merely is an easier and quicker way
>>> than typing the same thing yourself on top of the other track,
>>> and does not duplicate notes.
>>
>> Unless I'm ignorant, "git rebase" (aka "history edit") lacks essential
>> feature though: in addition to saying: get "this" history and rebase it
>> "there", one should be able to say: get "that" history, and rebase it
>> "here" (aka cherry-pick on steroids), that also would eliminate the need
>> for 'git cherry-pick <range>' that (poorly) duplicates rebase
>> functionality.
>
> But isn’t that
>
> git rebase --onto=<here> <that-upstream> <that>
Cause we end-up being on modified <that> (unless my memory fails me)
rather than on modified <here> when intent is to keep <that> intact.
> ? And why is that cherry-picking a range is a poor substitute [it is
> rather rebase that duplicates cherry-pick ;)]?
Because rebase has a lot of useful features compared to cherry-pick,
such as --interactive, --exec, merges handling, etc. In fact, IMHO,
"cherry-pick" should better be Git plumbing, and every-day-user-needs
should be handled by "rebase".
Historically, when ranges support was added to "cherry-pick", it was
probably a mistake, as "rebase" appears to be a better place for needed
functionality, and what was needed is rather adding "--pick" support to
"rebase".
--
Sergey Organov
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing
2025-08-19 10:55 [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing Patrick Steinhardt
` (12 preceding siblings ...)
2025-08-20 17:36 ` Junio C Hamano
@ 2025-08-24 1:25 ` Martin von Zweigbergk
2025-08-24 16:03 ` Patrick Steinhardt
2025-08-24 17:31 ` Kristoffer Haugsbakk
` (6 subsequent siblings)
20 siblings, 1 reply; 278+ messages in thread
From: Martin von Zweigbergk @ 2025-08-24 1:25 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git
On Tue, Aug 19, 2025 at 3:57 AM Patrick Steinhardt <ps@pks.im> wrote:
>
> Hi,
>
> over recent months I've been playing around with Jujutsu quite
> frequently. While I still prefer using Git, there's been a couple
> features in it that I really like and that I'd like to have in Git, as
> well.
>
> A copule of these features relate to history editing. Most importantly,
> I really dig the following commands:
>
> - jj-abandon(1) to drop a specific commit from your history.
It also rebases all descendants on top of the parent(s) of the
abandoned commit(s). Branches pointing to the rebased commits are also
repointed. So is the working copy if it points to a rebased commit
(the closest equivalent in Git would be HEAD). Do you plan to make all
the `git history` commands behave that way too?
>
> - jj-absorb(1) to take some changes and automatically apply them to
> commits in your history that last modified the respective hunks.
>
> - jj-split(1) to split a commit into two.
>
> - jj-new(1) to insert a new commit after or before a specific other
> commit.
>
> Not all of these commands can be ported directly into Git. jj-new(1) for
> example doesn't really make a ton of sense for us, I'd claim. But some
> of these commands _do_ make sense.
>
> I thus had a look at implementing some of these commands in Git itself,
> where the result is this patch series. Specifically, the following
> commands are introduced by this patch series:
>
> - `git history drop` to drop a specific commit. This is basically the
> same as jj-abandon(1).
>
> - `git history reorder` to reorder a specific commit before or after
> another commit. This is inspired by jj-new(1).
It seems more similar to `jj rebase -r X -A/-B Y`, which rips X out of
the graph and inserts it after/before Y. Just FYI; I'm not asking for
any changes.
>
> - `git history split` takes a commit and splits it into two. This is
> basically the same as jj-split(1).
FYI, the default behavior of `jj split` is to split the commit into
parent and child, but there's also `-A/-B X` to take the selected
changes and insert them after/before X, or `-d X` to put them on top
of X.
>
> If this is something we want to have I think it'd be just a starting
> point. There's other commands that I think are quite common and that
> might make sense to introduce eventually:
>
> - An equivalent to jj-absorb(1) would be awesome to have.
>
> - `git history reword` to change only the commit message of a specific
> commit.
FYI, `jj describe` can also change the commit message of multiple
commits at once (e.g. `jj describe main..@` to edit your current chain
of commits). It concatenates each description with some separators
between in that case so you can update them all at once in your
$EDITOR.
I'm letting you know these things in case it impacts planning for the
CLI arguments.
>
> - `git history squash` to squash together multiple commits into one.
>
> In the end, I'd like us to learn from what people like about Jujutsu and
> apply those learnings to Git. We won't be able to apply all learnings
> from Jujutsu, as the workflow is quite different there due to the lack
> of the index. But other things we certainly can apply to Git directly.
Perhaps the simplest thing to copy is revsets (which we copied from
Mercurial). See https://jj-vcs.github.io/jj/latest/revsets/. It's not
at all simple to implement, but I think it should be relatively simple
from a UX point of view because it can probably be done in a mostly
backwards compatible way.
>
> Note: This patch series currently builds on the cherry-pick infra.
> As such, when one hits a merge conflict one needs to `git cherry-pick
> --continue`, which is quite suboptimal. I didn't want to overpolish this
> series before getting some feedback, but it is something I'll fix in
> subsequent versions. Furthermore, the command for now bails out in the
> case where there's any merge commits in the history that is being
> rewritten. This is another restriction that can be lifted in the future.
>
> Thanks!
>
> Patrick
>
> ---
> Patrick Steinhardt (11):
> sequencer: optionally skip printing commit summary
> sequencer: add option to rewind HEAD after picking commits
> cache-tree: allow writing in-memory index as tree
> builtin: add new "history" command
> builtin/history: implement "drop" subcommand
> builtin/history: implement "reorder" subcommand
> add-patch: split out header from "add-interactive.h"
> add-patch: split out `struct interactive_options`
> add-patch: remove dependency on "add-interactive" subsystem
> add-patch: add support for in-memory index patching
> builtin/history: implement "split" subcommand
>
> .gitignore | 1 +
> Documentation/git-history.adoc | 159 ++++++++++
> Documentation/meson.build | 1 +
> Makefile | 1 +
> add-interactive.c | 151 +++------
> add-interactive.h | 43 +--
> add-patch.c | 271 ++++++++++++++--
> add-patch.h | 61 ++++
> builtin.h | 1 +
> builtin/add.c | 22 +-
> builtin/checkout.c | 7 +-
> builtin/commit.c | 16 +-
> builtin/history.c | 691 +++++++++++++++++++++++++++++++++++++++++
> builtin/reset.c | 16 +-
> builtin/stash.c | 46 +--
> cache-tree.c | 5 +-
> cache-tree.h | 3 +-
> commit.h | 2 +-
> git.c | 1 +
> meson.build | 1 +
> sequencer.c | 36 ++-
> sequencer.h | 4 +
> t/meson.build | 5 +-
> t/t3450-history-drop.sh | 127 ++++++++
> t/t3451-history-reorder.sh | 218 +++++++++++++
> t/t3452-history-split.sh | 304 ++++++++++++++++++
> 26 files changed, 1947 insertions(+), 246 deletions(-)
>
>
> ---
> base-commit: c44beea485f0f2feaf460e2ac87fdd5608d63cf0
> change-id: 20250819-b4-pks-history-builtin-83398f9a05f0
>
>
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing
2025-08-24 1:25 ` Martin von Zweigbergk
@ 2025-08-24 16:03 ` Patrick Steinhardt
2025-09-17 20:12 ` SZEDER Gábor
0 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-24 16:03 UTC (permalink / raw)
To: Martin von Zweigbergk; +Cc: git
On Sat, Aug 23, 2025 at 06:25:06PM -0700, Martin von Zweigbergk wrote:
> On Tue, Aug 19, 2025 at 3:57 AM Patrick Steinhardt <ps@pks.im> wrote:
> >
> > Hi,
> >
> > over recent months I've been playing around with Jujutsu quite
> > frequently. While I still prefer using Git, there's been a couple
> > features in it that I really like and that I'd like to have in Git, as
> > well.
> >
> > A copule of these features relate to history editing. Most importantly,
> > I really dig the following commands:
> >
> > - jj-abandon(1) to drop a specific commit from your history.
>
> It also rebases all descendants on top of the parent(s) of the
> abandoned commit(s). Branches pointing to the rebased commits are also
> repointed. So is the working copy if it points to a rebased commit
> (the closest equivalent in Git would be HEAD). Do you plan to make all
> the `git history` commands behave that way too?
Yup.
[snip]
> > - `git history reorder` to reorder a specific commit before or after
> > another commit. This is inspired by jj-new(1).
>
> It seems more similar to `jj rebase -r X -A/-B Y`, which rips X out of
> the graph and inserts it after/before Y. Just FYI; I'm not asking for
> any changes.
Fair enough. I think calling this `git history rebase` would be
problematic though given that "rebase" is already a widely used term in
Git that means something else.
> > - `git history split` takes a commit and splits it into two. This is
> > basically the same as jj-split(1).
>
> FYI, the default behavior of `jj split` is to split the commit into
> parent and child, but there's also `-A/-B X` to take the selected
> changes and insert them after/before X, or `-d X` to put them on top
> of X.
Oh, that's neat and I can totally see how that is useful. I'll not yet
add such a change, but that's certainly something we can add at a later
point in time rather easily.
> > If this is something we want to have I think it'd be just a starting
> > point. There's other commands that I think are quite common and that
> > might make sense to introduce eventually:
> >
> > - An equivalent to jj-absorb(1) would be awesome to have.
> >
> > - `git history reword` to change only the commit message of a specific
> > commit.
>
> FYI, `jj describe` can also change the commit message of multiple
> commits at once (e.g. `jj describe main..@` to edit your current chain
> of commits). It concatenates each description with some separators
> between in that case so you can update them all at once in your
> $EDITOR.
>
> I'm letting you know these things in case it impacts planning for the
> CLI arguments.
And I very much appreciate it :) A bunch of the functionality in Git
will have different terminology so that it fits better into Git overall.
There for example is already the "reword" action in the interactive
rebase code, so it makes sense to keep on using it. Same for "drop", for
example.
> > - `git history squash` to squash together multiple commits into one.
> >
> > In the end, I'd like us to learn from what people like about Jujutsu and
> > apply those learnings to Git. We won't be able to apply all learnings
> > from Jujutsu, as the workflow is quite different there due to the lack
> > of the index. But other things we certainly can apply to Git directly.
>
> Perhaps the simplest thing to copy is revsets (which we copied from
> Mercurial). See https://jj-vcs.github.io/jj/latest/revsets/. It's not
> at all simple to implement, but I think it should be relatively simple
> from a UX point of view because it can probably be done in a mostly
> backwards compatible way.
True, I very much like the way revsets work in JJ. Whether it can be
used in a backwards-compatible way... no idea. I'd have to think about
this more closely, but for now I think there's other, lower-hanging
fruit :)
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing
2025-08-24 16:03 ` Patrick Steinhardt
@ 2025-09-17 20:12 ` SZEDER Gábor
2025-12-03 18:18 ` Matthias Beyer
0 siblings, 1 reply; 278+ messages in thread
From: SZEDER Gábor @ 2025-09-17 20:12 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: Martin von Zweigbergk, git
On Sun, Aug 24, 2025 at 06:03:02PM +0200, Patrick Steinhardt wrote:
> On Sat, Aug 23, 2025 at 06:25:06PM -0700, Martin von Zweigbergk wrote:
> > On Tue, Aug 19, 2025 at 3:57 AM Patrick Steinhardt <ps@pks.im> wrote:
> > >
> > > Hi,
> > >
> > > over recent months I've been playing around with Jujutsu quite
> > > frequently. While I still prefer using Git, there's been a couple
> > > features in it that I really like and that I'd like to have in Git, as
> > > well.
> > >
> > > A copule of these features relate to history editing. Most importantly,
> > > I really dig the following commands:
> > >
> > > - jj-abandon(1) to drop a specific commit from your history.
> >
> > It also rebases all descendants on top of the parent(s) of the
> > abandoned commit(s). Branches pointing to the rebased commits are also
> > repointed. So is the working copy if it points to a rebased commit
> > (the closest equivalent in Git would be HEAD). Do you plan to make all
> > the `git history` commands behave that way too?
>
> Yup.
That sounds scary... What does "all descendants" mean in this
context?
Let's suppose I have this piece of history, I'm on 'branch2', and I
drop commit B. Which commits will be rewritten and which branches
will be repointed?
A---B---C---D branch1
\ \
\ E---F branch2
\ \
\ G---H---I branch3
\
J---K---L branch4
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing
2025-09-17 20:12 ` SZEDER Gábor
@ 2025-12-03 18:18 ` Matthias Beyer
2025-12-10 9:58 ` Phillip Wood
0 siblings, 1 reply; 278+ messages in thread
From: Matthias Beyer @ 2025-12-03 18:18 UTC (permalink / raw)
To: SZEDER Gábor; +Cc: Patrick Steinhardt, Martin von Zweigbergk, git
Hi,
Am Wed, Sep 17, 2025 at 10:12:31PM +0200, schrieb SZEDER Gábor:
> On Sun, Aug 24, 2025 at 06:03:02PM +0200, Patrick Steinhardt wrote:
> > On Sat, Aug 23, 2025 at 06:25:06PM -0700, Martin von Zweigbergk wrote:
> > > On Tue, Aug 19, 2025 at 3:57 AM Patrick Steinhardt <ps@pks.im> wrote:
> > > >
> > > > Hi,
> > > >
> > > > over recent months I've been playing around with Jujutsu quite
> > > > frequently. While I still prefer using Git, there's been a couple
> > > > features in it that I really like and that I'd like to have in Git, as
> > > > well.
> > > >
> > > > A copule of these features relate to history editing. Most importantly,
> > > > I really dig the following commands:
> > > >
> > > > - jj-abandon(1) to drop a specific commit from your history.
> > >
> > > It also rebases all descendants on top of the parent(s) of the
> > > abandoned commit(s). Branches pointing to the rebased commits are also
> > > repointed. So is the working copy if it points to a rebased commit
> > > (the closest equivalent in Git would be HEAD). Do you plan to make all
> > > the `git history` commands behave that way too?
> >
> > Yup.
>
> That sounds scary... What does "all descendants" mean in this
> context?
>
> Let's suppose I have this piece of history, I'm on 'branch2', and I
> drop commit B. Which commits will be rewritten and which branches
> will be repointed?
>
> A---B---C---D branch1
> \ \
> \ E---F branch2
> \ \
> \ G---H---I branch3
> \
> J---K---L branch4
>
Just speaking as a user here, but my expectation in this scenario would
be that rewriting B would be denied by default here, as branch{1..4}
would be rewritten although I am at branch2.
In the scenario at hand, I would expect that I can only rewrite G, H, I
while on branch 3 and J, K, L while on branch4 (without passing some
extra flags for "yes, please also rewrite the other branches").
Just my 2cts
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing
2025-12-03 18:18 ` Matthias Beyer
@ 2025-12-10 9:58 ` Phillip Wood
2025-12-10 10:37 ` Matthias Beyer
0 siblings, 1 reply; 278+ messages in thread
From: Phillip Wood @ 2025-12-10 9:58 UTC (permalink / raw)
To: Matthias Beyer, SZEDER Gábor
Cc: Patrick Steinhardt, Martin von Zweigbergk, git
Hi Matthias
On 03/12/2025 18:18, Matthias Beyer wrote:
> Am Wed, Sep 17, 2025 at 10:12:31PM +0200, schrieb SZEDER Gábor:
>
>> Let's suppose I have this piece of history, I'm on 'branch2', and I
>> drop commit B. Which commits will be rewritten and which branches
>> will be repointed?
>>
>> A---B---C---D branch1
>> \ \
>> \ E---F branch2
>> \ \
>> \ G---H---I branch3
>> \
>> J---K---L branch4
>>
>
> Just speaking as a user here, but my expectation in this scenario would
> be that rewriting B would be denied by default here, as branch{1..4}
> would be rewritten although I am at branch2.
>
> In the scenario at hand, I would expect that I can only rewrite G, H, I
> while on branch 3 and J, K, L while on branch4 (without passing some
> extra flags for "yes, please also rewrite the other branches").
Is that because you have branches that you don't want to rewrite because
they've been merged upstream or is there another reason? If we start
rewriting multiple branches we should probably check that we're not
rewriting something that has been merged upstream but if I rewrite a
commits that's an ancestor of several branches it would be very helpful
to rewrite them all at the same time to keep them in sync.
Thanks
Phillip
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing
2025-12-10 9:58 ` Phillip Wood
@ 2025-12-10 10:37 ` Matthias Beyer
2025-12-10 11:34 ` Phillip Wood
` (2 more replies)
0 siblings, 3 replies; 278+ messages in thread
From: Matthias Beyer @ 2025-12-10 10:37 UTC (permalink / raw)
To: phillip.wood
Cc: SZEDER Gábor, Patrick Steinhardt, Martin von Zweigbergk, git
Am Wed, Dec 10, 2025 at 09:58:13AM +0000, schrieb Phillip Wood:
> Hi Matthias
>
> On 03/12/2025 18:18, Matthias Beyer wrote:
> > Am Wed, Sep 17, 2025 at 10:12:31PM +0200, schrieb SZEDER Gábor:
> >
> > > Let's suppose I have this piece of history, I'm on 'branch2', and I
> > > drop commit B. Which commits will be rewritten and which branches
> > > will be repointed?
> > >
> > > A---B---C---D branch1
> > > \ \
> > > \ E---F branch2
> > > \ \
> > > \ G---H---I branch3
> > > \
> > > J---K---L branch4
> > >
> >
> > Just speaking as a user here, but my expectation in this scenario would
> > be that rewriting B would be denied by default here, as branch{1..4}
> > would be rewritten although I am at branch2.
> >
> > In the scenario at hand, I would expect that I can only rewrite G, H, I
> > while on branch 3 and J, K, L while on branch4 (without passing some
> > extra flags for "yes, please also rewrite the other branches").
>
> Is that because you have branches that you don't want to rewrite because
> they've been merged upstream or is there another reason? If we start
> rewriting multiple branches we should probably check that we're not
> rewriting something that has been merged upstream but if I rewrite a commits
> that's an ancestor of several branches it would be very helpful to rewrite
> them all at the same time to keep them in sync.
Its mostly because I don't like too much magic and because I think being
explicit is always better than not.
So from my POV, I would expect "the simple case" to be "the simple CLI
call" and if I want the tool to do magic and "rewrite all the
things"^tm, that I would need to specify a flag for that.
Best,
Matthias
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing
2025-12-10 10:37 ` Matthias Beyer
@ 2025-12-10 11:34 ` Phillip Wood
2025-12-10 14:18 ` Junio C Hamano
2025-12-10 16:49 ` Martin von Zweigbergk
2025-12-15 23:50 ` Kristoffer Haugsbakk
2 siblings, 1 reply; 278+ messages in thread
From: Phillip Wood @ 2025-12-10 11:34 UTC (permalink / raw)
To: Matthias Beyer, phillip.wood
Cc: SZEDER Gábor, Patrick Steinhardt, Martin von Zweigbergk, git
On 10/12/2025 10:37, Matthias Beyer wrote:
> Am Wed, Dec 10, 2025 at 09:58:13AM +0000, schrieb Phillip Wood:
>> On 03/12/2025 18:18, Matthias Beyer wrote:
>>> Am Wed, Sep 17, 2025 at 10:12:31PM +0200, schrieb SZEDER Gábor:
>>>
>>>> Let's suppose I have this piece of history, I'm on 'branch2', and I
>>>> drop commit B. Which commits will be rewritten and which branches
>>>> will be repointed?
>>>>
>>>> A---B---C---D branch1
>>>> \ \
>>>> \ E---F branch2
>>>> \ \
>>>> \ G---H---I branch3
>>>> \
>>>> J---K---L branch4
>>>>
>>>
>>> Just speaking as a user here, but my expectation in this scenario would
>>> be that rewriting B would be denied by default here, as branch{1..4}
>>> would be rewritten although I am at branch2.
>>>
>>> In the scenario at hand, I would expect that I can only rewrite G, H, I
>>> while on branch 3 and J, K, L while on branch4 (without passing some
>>> extra flags for "yes, please also rewrite the other branches").
>>
>> Is that because you have branches that you don't want to rewrite because
>> they've been merged upstream or is there another reason? If we start
>> rewriting multiple branches we should probably check that we're not
>> rewriting something that has been merged upstream but if I rewrite a commits
>> that's an ancestor of several branches it would be very helpful to rewrite
>> them all at the same time to keep them in sync.
>
> Its mostly because I don't like too much magic and because I think being
> explicit is always better than not.
>
> So from my POV, I would expect "the simple case" to be "the simple CLI
> call" and if I want the tool to do magic and "rewrite all the
> things"^tm, that I would need to specify a flag for that.
Thanks, that's useful to know. I'd assumed rewriting all the branches
descended from the rewritten commit was the natural thing do do but
clearly not everyone thinks it is.
Phillip
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing
2025-12-10 11:34 ` Phillip Wood
@ 2025-12-10 14:18 ` Junio C Hamano
2025-12-19 12:22 ` Patrick Steinhardt
0 siblings, 1 reply; 278+ messages in thread
From: Junio C Hamano @ 2025-12-10 14:18 UTC (permalink / raw)
To: Phillip Wood
Cc: Matthias Beyer, phillip.wood, SZEDER Gábor,
Patrick Steinhardt, Martin von Zweigbergk, git
Phillip Wood <phillip.wood123@gmail.com> writes:
>> Its mostly because I don't like too much magic and because I think being
>> explicit is always better than not.
>>
>> So from my POV, I would expect "the simple case" to be "the simple CLI
>> call" and if I want the tool to do magic and "rewrite all the
>> things"^tm, that I would need to specify a flag for that.
>
> Thanks, that's useful to know. I'd assumed rewriting all the branches
> descended from the rewritten commit was the natural thing do do but
> clearly not everyone thinks it is.
It probably depends on the way one looks at the tool, as a building
block (in which case less magic may be preferrable) or a complete
solution for one part of workflow. I probably fall into former camp
more often than other people, but for this particular one, I tend to
think it is less confusing if we moved all branch refs away from the
commits that are obsoleted by rewriting/replaying.
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing
2025-12-10 14:18 ` Junio C Hamano
@ 2025-12-19 12:22 ` Patrick Steinhardt
2025-12-19 13:58 ` SZEDER Gábor
` (3 more replies)
0 siblings, 4 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-12-19 12:22 UTC (permalink / raw)
To: Junio C Hamano
Cc: Phillip Wood, Matthias Beyer, phillip.wood, SZEDER Gábor,
Martin von Zweigbergk, git, Elijah Newren
On Wed, Dec 10, 2025 at 11:18:29PM +0900, Junio C Hamano wrote:
> Phillip Wood <phillip.wood123@gmail.com> writes:
>
> >> Its mostly because I don't like too much magic and because I think being
> >> explicit is always better than not.
> >>
> >> So from my POV, I would expect "the simple case" to be "the simple CLI
> >> call" and if I want the tool to do magic and "rewrite all the
> >> things"^tm, that I would need to specify a flag for that.
> >
> > Thanks, that's useful to know. I'd assumed rewriting all the branches
> > descended from the rewritten commit was the natural thing do do but
> > clearly not everyone thinks it is.
>
> It probably depends on the way one looks at the tool, as a building
> block (in which case less magic may be preferrable) or a complete
> solution for one part of workflow. I probably fall into former camp
> more often than other people, but for this particular one, I tend to
> think it is less confusing if we moved all branch refs away from the
> commits that are obsoleted by rewriting/replaying.
Okay, so the majority of folks here seem to favor rewriting all
dependent branches, which is also the default that JJ uses here, and
git-replay(1) does it, too.
There is one major difference between git-replay(1) and git-history(1)
though: the former works with revision ranges, whereas the latter does
not. By using revision ranges we avoid the problem I have mentioned in a
different branch of this discussion, which is that we have no easy way
to figure out which branches we'd have to touch in the first place. This
is because we simply walk the revision range there and then look at
which of our references point into that range. That's simple enough.
But in our case we're not working with ranges, we are working with a
singular commit. In my head this meant that we'd have to basically do a
revision walk that starts from all of our branches so that we can figure
out which of them would eventually reach the commit that we are about to
rewrite. And that of course doesn't scale.
Now we could of course also introduce ranges into git-history(1). That
would indeed solve the issue, as we can reuse the same architecture as
we already have in git-replay(1). But I don't really want to go there as
it is leaking complexity to the user: they want to rewrite a single
commit, why should they have to think about ranges?
But now that I've thought about the problem a bit I think we can avoid
that issue by implicitly identifying the range: it's all the commits
between the commit we're about to rewrite and HEAD. So, same as with
git-replay(1), the set of branches that we'd need to rewrite is any one
branch that points into that range. It keeps the UI simple as the user
still only has to think about a singular commit, should be sufficiently
fast to compute in most cases, and it allows mega-merge workflows like
JJ supports.
Does that make sense to everyone? If so, I'll revise my stance and will
adapt the current implementation to do exactly that.
Thanks for the discussion!
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing
2025-12-19 12:22 ` Patrick Steinhardt
@ 2025-12-19 13:58 ` SZEDER Gábor
2025-12-19 14:09 ` Patrick Steinhardt
2025-12-19 16:30 ` Elijah Newren
` (2 subsequent siblings)
3 siblings, 1 reply; 278+ messages in thread
From: SZEDER Gábor @ 2025-12-19 13:58 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: Junio C Hamano, Phillip Wood, Matthias Beyer, phillip.wood,
Martin von Zweigbergk, git, Elijah Newren
On Fri, Dec 19, 2025 at 01:22:03PM +0100, Patrick Steinhardt wrote:
> On Wed, Dec 10, 2025 at 11:18:29PM +0900, Junio C Hamano wrote:
> > Phillip Wood <phillip.wood123@gmail.com> writes:
> >
> > >> Its mostly because I don't like too much magic and because I think being
> > >> explicit is always better than not.
> > >>
> > >> So from my POV, I would expect "the simple case" to be "the simple CLI
> > >> call" and if I want the tool to do magic and "rewrite all the
> > >> things"^tm, that I would need to specify a flag for that.
> > >
> > > Thanks, that's useful to know. I'd assumed rewriting all the branches
> > > descended from the rewritten commit was the natural thing do do but
> > > clearly not everyone thinks it is.
> >
> > It probably depends on the way one looks at the tool, as a building
> > block (in which case less magic may be preferrable) or a complete
> > solution for one part of workflow. I probably fall into former camp
> > more often than other people, but for this particular one, I tend to
> > think it is less confusing if we moved all branch refs away from the
> > commits that are obsoleted by rewriting/replaying.
>
> Okay, so the majority of folks here seem to favor rewriting all
> dependent branches, which is also the default that JJ uses here, and
> git-replay(1) does it, too.
I can't find the word "conflict" in this subthread, so let me bring
back that little history snippet from around the beginning of the
subthread:
Let's suppose I have this piece of history, I'm on 'branch2', and I
drop commit B. Which commits will be rewritten and which branches
will be repointed?
A---B---C---D branch1
\ \
\ E---F branch2
\ \
\ G---H---I branch3
\
J---K---L branch4
If we were to rewrite all dependent branches after dropping commit B,
then besides 'branch2' we would rewrite 'branch1', 'branch3' and
'branch4' as well, right?
Now, let's suppose that dropping B would cause conflicts when
rewriting commits G, H, I, J, K and L.
When does the user have to resolve these conflicts?
If not right now, then how exactly will those dependent branches be
rewritten? (I understand jj can store conflicts and they can be
resolved later... But are we there yet?)
If right now, when 'git history' applies each of those commits, then
rewriting all dependent branches seems to be a horrible idea.
> But now that I've thought about the problem a bit I think we can avoid
> that issue by implicitly identifying the range: it's all the commits
> between the commit we're about to rewrite and HEAD. So, same as with
> git-replay(1),
I think 'git rebase --update-refs' does the same as well.
> the set of branches that we'd need to rewrite is any one
> branch that points into that range. It keeps the UI simple as the user
> still only has to think about a singular commit, should be sufficiently
> fast to compute in most cases, and it allows mega-merge workflows like
> JJ supports.
>
> Does that make sense to everyone? If so, I'll revise my stance and will
> adapt the current implementation to do exactly that.
I would very much prefer that the tool would only rewrite branches
that I explicitly allowed to be rewritten, with an '--all' option that
would allow the rewrite of all dependent branches.
Or at the very least there must be an escape hatch.
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing
2025-12-19 13:58 ` SZEDER Gábor
@ 2025-12-19 14:09 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-12-19 14:09 UTC (permalink / raw)
To: SZEDER Gábor
Cc: Junio C Hamano, Phillip Wood, Matthias Beyer, phillip.wood,
Martin von Zweigbergk, git, Elijah Newren
On Fri, Dec 19, 2025 at 02:58:40PM +0100, SZEDER Gábor wrote:
> On Fri, Dec 19, 2025 at 01:22:03PM +0100, Patrick Steinhardt wrote:
> > On Wed, Dec 10, 2025 at 11:18:29PM +0900, Junio C Hamano wrote:
> > > Phillip Wood <phillip.wood123@gmail.com> writes:
> > >
> > > >> Its mostly because I don't like too much magic and because I think being
> > > >> explicit is always better than not.
> > > >>
> > > >> So from my POV, I would expect "the simple case" to be "the simple CLI
> > > >> call" and if I want the tool to do magic and "rewrite all the
> > > >> things"^tm, that I would need to specify a flag for that.
> > > >
> > > > Thanks, that's useful to know. I'd assumed rewriting all the branches
> > > > descended from the rewritten commit was the natural thing do do but
> > > > clearly not everyone thinks it is.
> > >
> > > It probably depends on the way one looks at the tool, as a building
> > > block (in which case less magic may be preferrable) or a complete
> > > solution for one part of workflow. I probably fall into former camp
> > > more often than other people, but for this particular one, I tend to
> > > think it is less confusing if we moved all branch refs away from the
> > > commits that are obsoleted by rewriting/replaying.
> >
> > Okay, so the majority of folks here seem to favor rewriting all
> > dependent branches, which is also the default that JJ uses here, and
> > git-replay(1) does it, too.
>
> I can't find the word "conflict" in this subthread, so let me bring
> back that little history snippet from around the beginning of the
> subthread:
>
> Let's suppose I have this piece of history, I'm on 'branch2', and I
> drop commit B. Which commits will be rewritten and which branches
> will be repointed?
>
> A---B---C---D branch1
> \ \
> \ E---F branch2
> \ \
> \ G---H---I branch3
> \
> J---K---L branch4
>
> If we were to rewrite all dependent branches after dropping commit B,
> then besides 'branch2' we would rewrite 'branch1', 'branch3' and
> 'branch4' as well, right?
>
> Now, let's suppose that dropping B would cause conflicts when
> rewriting commits G, H, I, J, K and L.
>
> When does the user have to resolve these conflicts?
In the current proposed subcommands there cannot be any conflicts, as
both reword and split do not change the resulting trees.
> If not right now, then how exactly will those dependent branches be
> rewritten? (I understand jj can store conflicts and they can be
> resolved later... But are we there yet?)
No, we aren't. I think Elijah wants to work on that, and I agree that
for a subset of commands it might be a required feature.
> > the set of branches that we'd need to rewrite is any one
> > branch that points into that range. It keeps the UI simple as the user
> > still only has to think about a singular commit, should be sufficiently
> > fast to compute in most cases, and it allows mega-merge workflows like
> > JJ supports.
> >
> > Does that make sense to everyone? If so, I'll revise my stance and will
> > adapt the current implementation to do exactly that.
>
> I would very much prefer that the tool would only rewrite branches
> that I explicitly allowed to be rewritten, with an '--all' option that
> would allow the rewrite of all dependent branches.
>
> Or at the very least there must be an escape hatch.
I agree a 100% about there being an escape hatch. You really don't want
to rewrite dependent branches in all cases. I'll include an option that
allows users to pick what to either:
- Rewrite all dependent branches.
- Rewrite only the currently checked-out branch or reference.
- Only give us what _would_ happen, so basically git-replay(1)'s
"--ref-action=print" mode.
Thanks!
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing
2025-12-19 12:22 ` Patrick Steinhardt
2025-12-19 13:58 ` SZEDER Gábor
@ 2025-12-19 16:30 ` Elijah Newren
2025-12-20 16:51 ` Elijah Newren
2025-12-22 10:46 ` Phillip Wood
2025-12-22 13:47 ` D. Ben Knoble
3 siblings, 1 reply; 278+ messages in thread
From: Elijah Newren @ 2025-12-19 16:30 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: Junio C Hamano, Phillip Wood, Matthias Beyer, phillip.wood,
SZEDER Gábor, Martin von Zweigbergk, git
On Fri, Dec 19, 2025 at 4:22 AM Patrick Steinhardt <ps@pks.im> wrote:
>
[...]
> Okay, so the majority of folks here seem to favor rewriting all
> dependent branches, which is also the default that JJ uses here, and
> git-replay(1) does it, too.
>
> There is one major difference between git-replay(1) and git-history(1)
> though: the former works with revision ranges, whereas the latter does
> not. By using revision ranges we avoid the problem I have mentioned in a
> different branch of this discussion, which is that we have no easy way
> to figure out which branches we'd have to touch in the first place. This
> is because we simply walk the revision range there and then look at
> which of our references point into that range. That's simple enough.
>
> But in our case we're not working with ranges, we are working with a
> singular commit.
I don't understand the distinction at all. `git replay edit` also
took a single commit, and then implemented the obvious (and jj-like)
behavior of rewriting all branches that descended from that commit.
> In my head this meant that we'd have to basically do a
> revision walk that starts from all of our branches so that we can figure
> out which of them would eventually reach the commit that we are about to
> rewrite.
Yes, and it's only a few lines of code, as I showed earlier.
> And that of course doesn't scale.
That's quite an assumption about scaling; I don't believe it. Under
what conditions would this be slow enough for users to notice and be
bothered? commit-graphs not enabled + weird local clone with
thousands of local branches? Also, isn't jj specifically designed for
large repositories and with scaling in mind, and yet this is their
default behavior?
More importantly, this is being used to justify a large principle of
least astonishment violation (disconnecting branches with shared
history), so we'd not only need to show that walking all branches was
slower enough for users to notice, but slower enough that the negative
user performance experience offsets the negative user experience from
the astonishing behavior. Typically, spending extra cycles to provide
users with good warnings/errors is a good use of time, especially when
it'll take them far longer to discover and recover from negative
surprises.
> Now we could of course also introduce ranges into git-history(1). That
> would indeed solve the issue,
I actually don't follow; how would this help? I'm not even sure how
it would make sense; am I missing something?
> as we can reuse the same architecture as
> we already have in git-replay(1). But I don't really want to go there as
> it is leaking complexity to the user: they want to rewrite a single
> commit, why should they have to think about ranges?
I totally agree that they shouldn't have to think about ranges. They
rewrite a commit and every branch that descends from it is rewritten
for them. If the commit the user tried to edit was part of an
immutable branch (to be implemented later), by default you throw an
error. For the special cases where users do want to disconnect
connected histories, you can provide an option for users to specify
that they only want the current branch (or only the branches which the
current branch contains).
> But now that I've thought about the problem a bit I think we can avoid
> that issue by implicitly identifying the range
Yes! As I've been saying, anything that descends from the commit
being rewritten.
> it's all the commits
> between the commit we're about to rewrite and HEAD.
Huh?
> So, same as with
> git-replay(1), the set of branches that we'd need to rewrite is any one
> branch that points into that range. It keeps the UI simple as the user
> still only has to think about a singular commit, should be sufficiently
> fast to compute in most cases, and it allows mega-merge workflows like
> JJ supports.
>
> Does that make sense to everyone? If so, I'll revise my stance and will
> adapt the current implementation to do exactly that.
No, it doesn't make any sense to me at all. It'll avoid the principle
of least astonishment violation in some cases, but leave it present
for others (e.g. other branches which contain this one, or other
branches which share the specified commit even if they diverge
afterwards). I think we shouldn't have a principle of least
astonishment violation. There are three ways to avoid such a
violation:
(1) rewrite all branches (refs/heads/*) that descend from the commit
(code for this already previously provided and is shorter than the
existing code in this series)
(2) walk all branches that descend from the commit so we can give
the user a warning/error when multiple branches are affected
(3) force the user to be explicit about what they want. Provide a
set of mutually exclusive flags, and error if none are provided. One
flag would be for rewriting all branches, one would be for rewriting
only the current branch, and we could add others (e.g. rewriting all
branches contained in the current branch).
I don't think retaining this POLA violation makes sense as a starting point.
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing
2025-12-19 16:30 ` Elijah Newren
@ 2025-12-20 16:51 ` Elijah Newren
0 siblings, 0 replies; 278+ messages in thread
From: Elijah Newren @ 2025-12-20 16:51 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: Junio C Hamano, Phillip Wood, Matthias Beyer, phillip.wood,
SZEDER Gábor, Martin von Zweigbergk, git
On Fri, Dec 19, 2025 at 8:30 AM Elijah Newren <newren@gmail.com> wrote:
>
> On Fri, Dec 19, 2025 at 4:22 AM Patrick Steinhardt <ps@pks.im> wrote:
> >
> [...]
> > Okay, so the majority of folks here seem to favor rewriting all
> > dependent branches, which is also the default that JJ uses here, and
> > git-replay(1) does it, too.
> >
> > There is one major difference between git-replay(1) and git-history(1)
> > though: the former works with revision ranges, whereas the latter does
> > not. By using revision ranges we avoid the problem I have mentioned in a
> > different branch of this discussion, which is that we have no easy way
> > to figure out which branches we'd have to touch in the first place. This
> > is because we simply walk the revision range there and then look at
> > which of our references point into that range. That's simple enough.
> >
> > But in our case we're not working with ranges, we are working with a
> > singular commit.
>
> I don't understand the distinction at all. `git replay edit` also
> took a single commit, and then implemented the obvious (and jj-like)
> behavior of rewriting all branches that descended from that commit.
>
> > In my head this meant that we'd have to basically do a
> > revision walk that starts from all of our branches so that we can figure
> > out which of them would eventually reach the commit that we are about to
> > rewrite.
>
> Yes, and it's only a few lines of code, as I showed earlier.
>
> > And that of course doesn't scale.
>
> That's quite an assumption about scaling; I don't believe it. Under
> what conditions would this be slow enough for users to notice and be
> bothered? commit-graphs not enabled + weird local clone with
> thousands of local branches? Also, isn't jj specifically designed for
> large repositories and with scaling in mind, and yet this is their
> default behavior?
>
> More importantly, this is being used to justify a large principle of
> least astonishment violation (disconnecting branches with shared
> history), so we'd not only need to show that walking all branches was
> slower enough for users to notice, but slower enough that the negative
> user performance experience offsets the negative user experience from
> the astonishing behavior. Typically, spending extra cycles to provide
> users with good warnings/errors is a good use of time, especially when
> it'll take them far longer to discover and recover from negative
> surprises.
A quick clarification in case I'm misunderstood above:
When I talk about rewriting branches descended from the commit, I am
specifically talking about refs/heads/*, not refs/remotes/origin/* or
refs/tags/* or anything else. Because:
* I suspect we'll soon implement an "immutable branches" concept, so
that e.g. a request to modify a commit in the history of "main" would
result in an error (by default). This leads to the idea that we're
just rewriting the user's local stuff they have on top of the
immutable history, i.e. their local branches.
* refs/tags/* are designed to be immutable, obviously. In fact, we
might want to automatically include tags in the set of "immutable
branches" by default.
* refs/remotes/* are designed to match what the corresponding remote
had, not to be independently rewritten. We'd really mess people up if
we changed that.
* The fact that users created local branches means they are marking
those parts of history as a relevant area of interest
* While it could be that some of refs/remotes/origin/* have shared
history with whatever commit is being reworded/split/edited, so too
could branches that were never pushed. I think focusing on the
branches of interest to the user (i.e. their local branches) makes
sense.
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing
2025-12-19 12:22 ` Patrick Steinhardt
2025-12-19 13:58 ` SZEDER Gábor
2025-12-19 16:30 ` Elijah Newren
@ 2025-12-22 10:46 ` Phillip Wood
2025-12-22 13:47 ` D. Ben Knoble
3 siblings, 0 replies; 278+ messages in thread
From: Phillip Wood @ 2025-12-22 10:46 UTC (permalink / raw)
To: Patrick Steinhardt, Junio C Hamano
Cc: Matthias Beyer, phillip.wood, SZEDER Gábor,
Martin von Zweigbergk, git, Elijah Newren
On 19/12/2025 12:22, Patrick Steinhardt wrote:
>
> But in our case we're not working with ranges, we are working with a
> singular commit. In my head this meant that we'd have to basically do a
> revision walk that starts from all of our branches so that we can figure
> out which of them would eventually reach the commit that we are about to
> rewrite. And that of course doesn't scale.
I'm not so sure about that. In repositories with lots of refs most of
them are likely to be tags or remote tracking branches rather than local
branches so I'd hope that the number of refs we have to walk was
manageable. I'd also expect the commit we're rewriting to be relatively
recent so the revision walk should quickly prune any branches that point
to commits older than the one we're rewriting which should further
reduce the number of commits we need to walk.
> But now that I've thought about the problem a bit I think we can avoid
> that issue by implicitly identifying the range: it's all the commits
> between the commit we're about to rewrite and HEAD. So, same as with
> git-replay(1), the set of branches that we'd need to rewrite is any one
> branch that points into that range. It keeps the UI simple as the user
> still only has to think about a singular commit, should be sufficiently
> fast to compute in most cases, and it allows mega-merge workflows like
> JJ supports.
I agree that users should not have to think about commit ranges, but
using an implicit range between the commit we're about to rewrite and
HEAD will not rewrite all the branches descended from that commit,
instead it will behave like "git rebase --update-refs".
> Does that make sense to everyone? If so, I'll revise my stance and will
> adapt the current implementation to do exactly that.
I'd much rather rewrite all the branches descended from the commit we're
about the rewrite rather than those that happen to point into the
revision range between that commit and HEAD as I think that ends up
being confusing.
Thanks
Phillip
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing
2025-12-19 12:22 ` Patrick Steinhardt
` (2 preceding siblings ...)
2025-12-22 10:46 ` Phillip Wood
@ 2025-12-22 13:47 ` D. Ben Knoble
3 siblings, 0 replies; 278+ messages in thread
From: D. Ben Knoble @ 2025-12-22 13:47 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: Junio C Hamano, Phillip Wood, Matthias Beyer, phillip.wood,
SZEDER Gábor, Martin von Zweigbergk, git, Elijah Newren
[resend] I originally wrote this before some of the fruitful
conversation replying to this message, so grain of salt. I think my
questions have been answered in terms of how --update-refs behaves,
etc.
I still don't think we lose anything by not deviating from other
commands now, but I also agree (perhaps to come later, actually using
the experimental status to break things?) that I don't want to have to
remember to rebase descendants myself.
On Fri, Dec 19, 2025 at 7:48 AM Patrick Steinhardt <ps@pks.im> wrote:
>
> On Wed, Dec 10, 2025 at 11:18:29PM +0900, Junio C Hamano wrote:
> > Phillip Wood <phillip.wood123@gmail.com> writes:
> >
> > >> Its mostly because I don't like too much magic and because I think being
> > >> explicit is always better than not.
> > >>
> > >> So from my POV, I would expect "the simple case" to be "the simple CLI
> > >> call" and if I want the tool to do magic and "rewrite all the
> > >> things"^tm, that I would need to specify a flag for that.
> > >
> > > Thanks, that's useful to know. I'd assumed rewriting all the branches
> > > descended from the rewritten commit was the natural thing do do but
> > > clearly not everyone thinks it is.
> >
> > It probably depends on the way one looks at the tool, as a building
> > block (in which case less magic may be preferrable) or a complete
> > solution for one part of workflow. I probably fall into former camp
> > more often than other people, but for this particular one, I tend to
> > think it is less confusing if we moved all branch refs away from the
> > commits that are obsoleted by rewriting/replaying.
>
> Okay, so the majority of folks here seem to favor rewriting all
> dependent branches, which is also the default that JJ uses here, and
> git-replay(1) does it, too.
>
> There is one major difference between git-replay(1) and git-history(1)
> though: the former works with revision ranges, whereas the latter does
> not. By using revision ranges we avoid the problem I have mentioned in a
> different branch of this discussion, which is that we have no easy way
> to figure out which branches we'd have to touch in the first place. This
> is because we simply walk the revision range there and then look at
> which of our references point into that range. That's simple enough.
>
> But in our case we're not working with ranges, we are working with a
> singular commit. In my head this meant that we'd have to basically do a
> revision walk that starts from all of our branches so that we can figure
> out which of them would eventually reach the commit that we are about to
> rewrite. And that of course doesn't scale.
>
> Now we could of course also introduce ranges into git-history(1). That
> would indeed solve the issue, as we can reuse the same architecture as
> we already have in git-replay(1). But I don't really want to go there as
> it is leaking complexity to the user: they want to rewrite a single
> commit, why should they have to think about ranges?
>
> But now that I've thought about the problem a bit I think we can avoid
> that issue by implicitly identifying the range: it's all the commits
> between the commit we're about to rewrite and HEAD. So, same as with
> git-replay(1), the set of branches that we'd need to rewrite is any one
> branch that points into that range. It keeps the UI simple as the user
> still only has to think about a singular commit, should be sufficiently
> fast to compute in most cases, and it allows mega-merge workflows like
> JJ supports.
>
> Does that make sense to everyone? If so, I'll revise my stance and will
> adapt the current implementation to do exactly that.
>
> Thanks for the discussion!
>
> Patrick
>
Makes sense to me, and is easily explainable.
One thing that I think JJ handles and which it sounds like replay,
history do not (I’m not sure about rebase with update-refs): stacked
branches that point to a chain of commits reaching into the range, but
whose reference is still outside it. For example:
A <- B <- C
If branchB points at B and similar for branchC, and branchB is HEAD,
then “replay <stuff> A..B“ and “history <cmd> <A|B>” sound like they
would leave branchC alone? That means I have to remember to do
something like “rebase --onto=branchB branchB@{1} branchC”, and if I
forget I usually have to later replace branchB@{1} with branchC~<n>.
OTOH, it means branchC serves as an additional backup of the original
branchB pre-rewrite :)
Anyway. If the other commands don’t support it yet, I don’t think we
lose anything by not rewriting descendants. But something to consider
in terms of workflow.
--
D. Ben Knoble
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing
2025-12-10 10:37 ` Matthias Beyer
2025-12-10 11:34 ` Phillip Wood
@ 2025-12-10 16:49 ` Martin von Zweigbergk
2025-12-10 18:27 ` Elijah Newren
2025-12-15 23:50 ` Kristoffer Haugsbakk
2 siblings, 1 reply; 278+ messages in thread
From: Martin von Zweigbergk @ 2025-12-10 16:49 UTC (permalink / raw)
To: Matthias Beyer; +Cc: phillip.wood, SZEDER Gábor, Patrick Steinhardt, git
On Wed, Dec 10, 2025 at 2:38 AM Matthias Beyer <mail@beyermatthias.de> wrote:
>
> Am Wed, Dec 10, 2025 at 09:58:13AM +0000, schrieb Phillip Wood:
> > Hi Matthias
> >
> > On 03/12/2025 18:18, Matthias Beyer wrote:
> > > Am Wed, Sep 17, 2025 at 10:12:31PM +0200, schrieb SZEDER Gábor:
> > >
> > > > Let's suppose I have this piece of history, I'm on 'branch2', and I
> > > > drop commit B. Which commits will be rewritten and which branches
> > > > will be repointed?
> > > >
> > > > A---B---C---D branch1
> > > > \ \
> > > > \ E---F branch2
> > > > \ \
> > > > \ G---H---I branch3
> > > > \
> > > > J---K---L branch4
> > > >
> > >
> > > Just speaking as a user here, but my expectation in this scenario would
> > > be that rewriting B would be denied by default here, as branch{1..4}
> > > would be rewritten although I am at branch2.
> > >
> > > In the scenario at hand, I would expect that I can only rewrite G, H, I
> > > while on branch 3 and J, K, L while on branch4 (without passing some
> > > extra flags for "yes, please also rewrite the other branches").
> >
> > Is that because you have branches that you don't want to rewrite because
> > they've been merged upstream or is there another reason?
I think that's a common reason even if it's not Matthias's reason.
Perhaps one way of doing it would be to have a configurable set of ref
patterns that are considered immutable. That's similar to what jj
does, though we use a more general language for selecting revisions
for it (https://docs.jj-vcs.dev/latest/config/#set-of-immutable-commits).
I think that has been well received. As you might expect, the set of
immutable revisions are respected by all commands.
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing
2025-12-10 16:49 ` Martin von Zweigbergk
@ 2025-12-10 18:27 ` Elijah Newren
2025-12-10 18:45 ` Martin von Zweigbergk
0 siblings, 1 reply; 278+ messages in thread
From: Elijah Newren @ 2025-12-10 18:27 UTC (permalink / raw)
To: Martin von Zweigbergk
Cc: Matthias Beyer, phillip.wood, SZEDER Gábor,
Patrick Steinhardt, git
On Wed, Dec 10, 2025 at 8:52 AM Martin von Zweigbergk
<martinvonz@gmail.com> wrote:
>
> On Wed, Dec 10, 2025 at 2:38 AM Matthias Beyer <mail@beyermatthias.de> wrote:
> >
> > Am Wed, Dec 10, 2025 at 09:58:13AM +0000, schrieb Phillip Wood:
> > > Hi Matthias
> > >
> > > On 03/12/2025 18:18, Matthias Beyer wrote:
> > > > Am Wed, Sep 17, 2025 at 10:12:31PM +0200, schrieb SZEDER Gábor:
> > > >
> > > > > Let's suppose I have this piece of history, I'm on 'branch2', and I
> > > > > drop commit B. Which commits will be rewritten and which branches
> > > > > will be repointed?
> > > > >
> > > > > A---B---C---D branch1
> > > > > \ \
> > > > > \ E---F branch2
> > > > > \ \
> > > > > \ G---H---I branch3
> > > > > \
> > > > > J---K---L branch4
> > > > >
> > > >
> > > > Just speaking as a user here, but my expectation in this scenario would
> > > > be that rewriting B would be denied by default here, as branch{1..4}
> > > > would be rewritten although I am at branch2.
> > > >
> > > > In the scenario at hand, I would expect that I can only rewrite G, H, I
> > > > while on branch 3 and J, K, L while on branch4 (without passing some
> > > > extra flags for "yes, please also rewrite the other branches").
> > >
> > > Is that because you have branches that you don't want to rewrite because
> > > they've been merged upstream or is there another reason?
>
> I think that's a common reason even if it's not Matthias's reason.
> Perhaps one way of doing it would be to have a configurable set of ref
> patterns that are considered immutable. That's similar to what jj
> does, though we use a more general language for selecting revisions
> for it (https://docs.jj-vcs.dev/latest/config/#set-of-immutable-commits).
> I think that has been well received. As you might expect, the set of
> immutable revisions are respected by all commands.
I like the idea of a set of immutable revisions...but wouldn't that
result in the request to drop commit B in the graph above being met
with an error rather than with a single branch being rewritten?
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing
2025-12-10 18:27 ` Elijah Newren
@ 2025-12-10 18:45 ` Martin von Zweigbergk
2025-12-10 19:55 ` Elijah Newren
0 siblings, 1 reply; 278+ messages in thread
From: Martin von Zweigbergk @ 2025-12-10 18:45 UTC (permalink / raw)
To: Elijah Newren
Cc: Matthias Beyer, phillip.wood, SZEDER Gábor,
Patrick Steinhardt, git
On Wed, Dec 10, 2025 at 10:27 AM Elijah Newren <newren@gmail.com> wrote:
>
> On Wed, Dec 10, 2025 at 8:52 AM Martin von Zweigbergk
> <martinvonz@gmail.com> wrote:
> >
> > On Wed, Dec 10, 2025 at 2:38 AM Matthias Beyer <mail@beyermatthias.de> wrote:
> > >
> > > Am Wed, Dec 10, 2025 at 09:58:13AM +0000, schrieb Phillip Wood:
> > > > Hi Matthias
> > > >
> > > > On 03/12/2025 18:18, Matthias Beyer wrote:
> > > > > Am Wed, Sep 17, 2025 at 10:12:31PM +0200, schrieb SZEDER Gábor:
> > > > >
> > > > > > Let's suppose I have this piece of history, I'm on 'branch2', and I
> > > > > > drop commit B. Which commits will be rewritten and which branches
> > > > > > will be repointed?
> > > > > >
> > > > > > A---B---C---D branch1
> > > > > > \ \
> > > > > > \ E---F branch2
> > > > > > \ \
> > > > > > \ G---H---I branch3
> > > > > > \
> > > > > > J---K---L branch4
> > > > > >
> > > > >
> > > > > Just speaking as a user here, but my expectation in this scenario would
> > > > > be that rewriting B would be denied by default here, as branch{1..4}
> > > > > would be rewritten although I am at branch2.
> > > > >
> > > > > In the scenario at hand, I would expect that I can only rewrite G, H, I
> > > > > while on branch 3 and J, K, L while on branch4 (without passing some
> > > > > extra flags for "yes, please also rewrite the other branches").
> > > >
> > > > Is that because you have branches that you don't want to rewrite because
> > > > they've been merged upstream or is there another reason?
> >
> > I think that's a common reason even if it's not Matthias's reason.
> > Perhaps one way of doing it would be to have a configurable set of ref
> > patterns that are considered immutable. That's similar to what jj
> > does, though we use a more general language for selecting revisions
> > for it (https://docs.jj-vcs.dev/latest/config/#set-of-immutable-commits).
> > I think that has been well received. As you might expect, the set of
> > immutable revisions are respected by all commands.
>
> I like the idea of a set of immutable revisions...but wouldn't that
> result in the request to drop commit B in the graph above being met
> with an error rather than with a single branch being rewritten?
If branch1 (or any of those branches, really) is configured as
immutable, then yes. But it's desirable to prevent rewriting or
dropping B in that case, right?
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing
2025-12-10 18:45 ` Martin von Zweigbergk
@ 2025-12-10 19:55 ` Elijah Newren
0 siblings, 0 replies; 278+ messages in thread
From: Elijah Newren @ 2025-12-10 19:55 UTC (permalink / raw)
To: Martin von Zweigbergk
Cc: Matthias Beyer, phillip.wood, SZEDER Gábor,
Patrick Steinhardt, git
On Wed, Dec 10, 2025 at 10:45 AM Martin von Zweigbergk
<martinvonz@gmail.com> wrote:
>
> On Wed, Dec 10, 2025 at 10:27 AM Elijah Newren <newren@gmail.com> wrote:
> >
> > On Wed, Dec 10, 2025 at 8:52 AM Martin von Zweigbergk
> > <martinvonz@gmail.com> wrote:
> > >
> > > On Wed, Dec 10, 2025 at 2:38 AM Matthias Beyer <mail@beyermatthias.de> wrote:
> > > >
> > > > Am Wed, Dec 10, 2025 at 09:58:13AM +0000, schrieb Phillip Wood:
> > > > > Hi Matthias
> > > > >
> > > > > On 03/12/2025 18:18, Matthias Beyer wrote:
> > > > > > Am Wed, Sep 17, 2025 at 10:12:31PM +0200, schrieb SZEDER Gábor:
> > > > > >
> > > > > > > Let's suppose I have this piece of history, I'm on 'branch2', and I
> > > > > > > drop commit B. Which commits will be rewritten and which branches
> > > > > > > will be repointed?
> > > > > > >
> > > > > > > A---B---C---D branch1
> > > > > > > \ \
> > > > > > > \ E---F branch2
> > > > > > > \ \
> > > > > > > \ G---H---I branch3
> > > > > > > \
> > > > > > > J---K---L branch4
> > > > > > >
> > > > > >
> > > > > > Just speaking as a user here, but my expectation in this scenario would
> > > > > > be that rewriting B would be denied by default here, as branch{1..4}
> > > > > > would be rewritten although I am at branch2.
> > > > > >
> > > > > > In the scenario at hand, I would expect that I can only rewrite G, H, I
> > > > > > while on branch 3 and J, K, L while on branch4 (without passing some
> > > > > > extra flags for "yes, please also rewrite the other branches").
> > > > >
> > > > > Is that because you have branches that you don't want to rewrite because
> > > > > they've been merged upstream or is there another reason?
> > >
> > > I think that's a common reason even if it's not Matthias's reason.
> > > Perhaps one way of doing it would be to have a configurable set of ref
> > > patterns that are considered immutable. That's similar to what jj
> > > does, though we use a more general language for selecting revisions
> > > for it (https://docs.jj-vcs.dev/latest/config/#set-of-immutable-commits).
> > > I think that has been well received. As you might expect, the set of
> > > immutable revisions are respected by all commands.
> >
> > I like the idea of a set of immutable revisions...but wouldn't that
> > result in the request to drop commit B in the graph above being met
> > with an error rather than with a single branch being rewritten?
>
> If branch1 (or any of those branches, really) is configured as
> immutable, then yes. But it's desirable to prevent rewriting or
> dropping B in that case, right?
Yes, I definitely think so. I was mixing up in my head the different
requests and thought that Matthias had asked to rewrite just one
branch, which sounded like it wasn't something you'd get through
immutable revisions and had me confused why you were suggesting that.
But, it was all just me mis-remembering and not re-reading. Sorry for
the mix-up.
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing
2025-12-10 10:37 ` Matthias Beyer
2025-12-10 11:34 ` Phillip Wood
2025-12-10 16:49 ` Martin von Zweigbergk
@ 2025-12-15 23:50 ` Kristoffer Haugsbakk
2 siblings, 0 replies; 278+ messages in thread
From: Kristoffer Haugsbakk @ 2025-12-15 23:50 UTC (permalink / raw)
To: Matthias Beyer, Phillip Wood
Cc: SZEDER Gábor, Patrick Steinhardt, Martin von Zweigbergk, git
On Wed, Dec 10, 2025, at 11:37, Matthias Beyer wrote:
>>[snip]
>>
>> Is that because you have branches that you don't want to rewrite because
>> they've been merged upstream or is there another reason? If we start
>> rewriting multiple branches we should probably check that we're not
>> rewriting something that has been merged upstream but if I rewrite a commits
>> that's an ancestor of several branches it would be very helpful to rewrite
>> them all at the same time to keep them in sync.
>
> Its mostly because I don't like too much magic and because I think being
> explicit is always better than not.
That the first thing here is not magic but the other thing is seems
arbitrary:
1. Change these commits, i.e. make new commits and all their descendants
2. Update all branches that point at the commits that have now been
replaced
These two feel conceptually similar to me in terms of complexity, and
neither of them are magical.
>
> So from my POV, I would expect "the simple case" to be "the simple CLI
> call" and if I want the tool to do magic and "rewrite all the
> things"^tm, that I would need to specify a flag for that.
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing
2025-08-19 10:55 [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing Patrick Steinhardt
` (13 preceding siblings ...)
2025-08-24 1:25 ` Martin von Zweigbergk
@ 2025-08-24 17:31 ` Kristoffer Haugsbakk
2025-08-24 17:38 ` Patrick Steinhardt
2025-08-24 17:42 ` [PATCH RFC v2 00/16] " Patrick Steinhardt
` (5 subsequent siblings)
20 siblings, 1 reply; 278+ messages in thread
From: Kristoffer Haugsbakk @ 2025-08-24 17:31 UTC (permalink / raw)
To: Patrick Steinhardt, git
On Tue, Aug 19, 2025, at 12:55, Patrick Steinhardt wrote:
> Hi,
>
> over recent months I've been playing around with Jujutsu quite
> frequently. While I still prefer using Git, there's been a couple
> features in it that I really like and that I'd like to have in Git, as
> well.
>
> A copule of these features relate to history editing. Most importantly,
> I really dig the following commands:
>
> - jj-abandon(1) to drop a specific commit from your history.
>
> - jj-absorb(1) to take some changes and automatically apply them to
> commits in your history that last modified the respective hunks.
>
> - jj-split(1) to split a commit into two.
>
> - jj-new(1) to insert a new commit after or before a specific other
> commit.
>
> Not all of these commands can be ported directly into Git. jj-new(1) for
> example doesn't really make a ton of sense for us, I'd claim. But some
> of these commands _do_ make sense.
>
> I thus had a look at implementing some of these commands in Git itself,
> where the result is this patch series. Specifically, the following
> commands are introduced by this patch series:
>
> - `git history drop` to drop a specific commit. This is basically the
> same as jj-abandon(1).
>
> - `git history reorder` to reorder a specific commit before or after
> another commit. This is inspired by jj-new(1).
>
> - `git history split` takes a commit and splits it into two. This is
> basically the same as jj-split(1).
I think it would be nice if git-history(1) called the `post-rewrite` hook.
In particular for Split; then all the possible rewrite modes are covered
(one-to-one, many-to-one (squash), and one-to-many).
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing
2025-08-24 17:31 ` Kristoffer Haugsbakk
@ 2025-08-24 17:38 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-24 17:38 UTC (permalink / raw)
To: Kristoffer Haugsbakk; +Cc: git
On Sun, Aug 24, 2025 at 07:31:45PM +0200, Kristoffer Haugsbakk wrote:
> On Tue, Aug 19, 2025, at 12:55, Patrick Steinhardt wrote:
> > Hi,
> >
> > over recent months I've been playing around with Jujutsu quite
> > frequently. While I still prefer using Git, there's been a couple
> > features in it that I really like and that I'd like to have in Git, as
> > well.
> >
> > A copule of these features relate to history editing. Most importantly,
> > I really dig the following commands:
> >
> > - jj-abandon(1) to drop a specific commit from your history.
> >
> > - jj-absorb(1) to take some changes and automatically apply them to
> > commits in your history that last modified the respective hunks.
> >
> > - jj-split(1) to split a commit into two.
> >
> > - jj-new(1) to insert a new commit after or before a specific other
> > commit.
> >
> > Not all of these commands can be ported directly into Git. jj-new(1) for
> > example doesn't really make a ton of sense for us, I'd claim. But some
> > of these commands _do_ make sense.
> >
> > I thus had a look at implementing some of these commands in Git itself,
> > where the result is this patch series. Specifically, the following
> > commands are introduced by this patch series:
> >
> > - `git history drop` to drop a specific commit. This is basically the
> > same as jj-abandon(1).
> >
> > - `git history reorder` to reorder a specific commit before or after
> > another commit. This is inspired by jj-new(1).
> >
> > - `git history split` takes a commit and splits it into two. This is
> > basically the same as jj-split(1).
>
> I think it would be nice if git-history(1) called the `post-rewrite` hook.
> In particular for Split; then all the possible rewrite modes are covered
> (one-to-one, many-to-one (squash), and one-to-many).
That's a sensible thing indeed. I'll add this to my todo list for v3.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH RFC v2 00/16] Introduce git-history(1) command for easy history editing
2025-08-19 10:55 [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing Patrick Steinhardt
` (14 preceding siblings ...)
2025-08-24 17:31 ` Kristoffer Haugsbakk
@ 2025-08-24 17:42 ` Patrick Steinhardt
2025-08-24 17:42 ` [PATCH RFC v2 01/16] sequencer: optionally skip printing commit summary Patrick Steinhardt
` (16 more replies)
2025-09-04 14:27 ` [PATCH RFC v3 00/18] " Patrick Steinhardt
` (4 subsequent siblings)
20 siblings, 17 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-24 17:42 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
Hi,
over recent months I've been playing around with Jujutsu quite
frequently. While I still prefer using Git, there's been a couple
features in it that I really like and that I'd like to have in Git, as
well.
A copule of these features relate to history editing. Most importantly,
I really dig the following commands:
- jj-abandon(1) to drop a specific commit from your history.
- jj-absorb(1) to take some changes and automatically apply them to
commits in your history that last modified the respective hunks.
- jj-split(1) to split a commit into two.
- jj-new(1) to insert a new commit after or before a specific other
commit.
Not all of these commands can be ported directly into Git. jj-new(1) for
example doesn't really make a ton of sense for us, I'd claim. But some
of these commands _do_ make sense.
I thus had a look at implementing some of these commands in Git itself,
where the result is this patch series. Specifically, the following
commands are introduced by this patch series:
- `git history drop` to drop a specific commit. This is basically the
same as jj-abandon(1).
- `git history reorder` to reorder a specific commit before or after
another commit. This is inspired by jj-new(1).
- `git history split` takes a commit and splits it into two. This is
basically the same as jj-split(1).
If this is something we want to have I think it'd be just a starting
point. There's other commands that I think are quite common and that
might make sense to introduce eventually:
- An equivalent to jj-absorb(1) would be awesome to have.
- `git history reword` to change only the commit message of a specific
commit.
- `git history squash` to squash together multiple commits into one.
In the end, I'd like us to learn from what people like about Jujutsu and
apply those learnings to Git. We won't be able to apply all learnings
from Jujutsu, as the workflow is quite different there due to the lack
of the index. But other things we certainly can apply to Git directly.
Note: This patch series currently builds on the cherry-pick infra.
As such, when one hits a merge conflict one needs to `git cherry-pick
--continue`, which is quite suboptimal. I didn't want to overpolish this
series before getting some feedback, but it is something I'll fix in
subsequent versions. Furthermore, the command for now bails out in the
case where there's any merge commits in the history that is being
rewritten. This is another restriction that can be lifted in the future.
Changes in v2:
- Add a new "reword" subcommand.
- List git-history(1) in "command-list.txt".
- Add some missing error handling.
- Simplify calling convention of `apply_commits()` to handle root
commits internally instead of requiring every caller to do so.
- Add tests to verify that git-history(1) refuses to work with changes
in the worktree or index.
- Mark git-history(1) as experimental.
- Introduce commands to manage interrupted history edits.
- A bunch of improvements to the manpage.
- Link to v1: https://lore.kernel.org/r/20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im
Thanks!
Patrick
---
Patrick Steinhardt (16):
sequencer: optionally skip printing commit summary
sequencer: add option to rewind HEAD after picking commits
sequencer: introduce new history editing mode
sequencer: stop using `the_repository` in `sequencer_remove_state()`
cache-tree: allow writing in-memory index as tree
builtin: add new "history" command
builtin/history: introduce subcommands to manage interrupted rewrites
builtin/history: implement "drop" subcommand
builtin/history: implement "reorder" subcommand
add-patch: split out header from "add-interactive.h"
add-patch: split out `struct interactive_options`
add-patch: remove dependency on "add-interactive" subsystem
add-patch: add support for in-memory index patching
wt-status: provide function to expose status for trees
builtin/history: implement "split" subcommand
builtin/history: implement "reword" subcommand
.gitignore | 1 +
Documentation/git-history.adoc | 188 +++++++++
Documentation/meson.build | 1 +
Makefile | 1 +
add-interactive.c | 151 ++-----
add-interactive.h | 43 +-
add-patch.c | 270 ++++++++++--
add-patch.h | 61 +++
builtin.h | 1 +
builtin/add.c | 22 +-
builtin/checkout.c | 7 +-
builtin/commit.c | 16 +-
builtin/history.c | 932 +++++++++++++++++++++++++++++++++++++++++
builtin/rebase.c | 4 +-
builtin/reset.c | 16 +-
builtin/revert.c | 2 +-
builtin/stash.c | 46 +-
cache-tree.c | 5 +-
cache-tree.h | 3 +-
command-list.txt | 1 +
commit.h | 2 +-
git.c | 1 +
meson.build | 1 +
sequencer.c | 187 +++++++--
sequencer.h | 9 +-
t/meson.build | 7 +-
t/t3450-history.sh | 42 ++
t/t3451-history-drop.sh | 174 ++++++++
t/t3452-history-reorder.sh | 234 +++++++++++
t/t3453-history-split.sh | 387 +++++++++++++++++
t/t3454-history-reword.sh | 158 +++++++
wt-status.c | 24 ++
wt-status.h | 3 +
33 files changed, 2714 insertions(+), 286 deletions(-)
Range-diff versus v1:
1: c1ce1b2e20 = 1: 6348d1ff69 sequencer: optionally skip printing commit summary
2: 969d896da1 ! 2: d70cb727bc sequencer: add option to rewind HEAD after picking commits
@@ sequencer.c: static int pick_commits(struct repository *r,
/*
* Sequence of picks finished successfully; cleanup by
* removing the .git/sequencer directory
+@@ sequencer.c: int sequencer_pick_revisions(struct repository *r,
+ if (opts->revs->cmdline.nr == 1 &&
+ opts->revs->cmdline.rev->whence == REV_CMD_REV &&
+ opts->revs->no_walk &&
+- !opts->revs->cmdline.rev->flags) {
++ !opts->revs->cmdline.rev->flags &&
++ !opts->restore_head_target) {
+ struct commit *cmit;
+
+ if (prepare_revision_walk(opts->revs)) {
## sequencer.h ##
@@ sequencer.h: struct replay_opts {
-: ---------- > 3: 9717385fba sequencer: introduce new history editing mode
-: ---------- > 4: 360e5cf08f sequencer: stop using `the_repository` in `sequencer_remove_state()`
3: 658cff279e = 5: 74be65b8f6 cache-tree: allow writing in-memory index as tree
4: 886ea1e088 ! 6: a84728f097 builtin: add new "history" command
@@ Documentation/git-history.adoc (new)
+
+NAME
+----
-+git-history - Rewrite history of the current branch
++git-history - EXPERIMENTAL: Rewrite history of the current branch
+
+SYNOPSIS
+--------
@@ Documentation/git-history.adoc (new)
+merges. You should use linkgit:git-rebase[1] with the `--rebase-merges`
+flag instead.
+
++THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
++
+COMMANDS
+--------
+
+This command requires a subcommand. Several subcommands are available to
-+rewrite history in different ways.
++rewrite history in different ways:
+
+CONFIGURATION
+-------------
@@ builtin/history.c (new)
+ return 0;
+}
+ ## command-list.txt ##
+@@ command-list.txt: git-grep mainporcelain info
+ git-gui mainporcelain
+ git-hash-object plumbingmanipulators
+ git-help ancillaryinterrogators complete
++git-history mainporcelain history
+ git-hook purehelpers
+ git-http-backend synchingrepositories
+ git-http-fetch synchelpers
+
## git.c ##
@@ git.c: static struct cmd_struct commands[] = {
{ "grep", cmd_grep, RUN_SETUP_GENTLY },
-: ---------- > 7: 234ad69878 builtin/history: introduce subcommands to manage interrupted rewrites
5: 084312482f ! 8: a81669a4d1 builtin/history: implement "drop" subcommand
@@ Commit message
Signed-off-by: Patrick Steinhardt <ps@pks.im>
## Documentation/git-history.adoc ##
-@@ Documentation/git-history.adoc: git-history - Rewrite history of the current branch
- SYNOPSIS
- --------
- [synopsis]
--git history [<options>]
-+git history drop [<options>] <revision>
+@@ Documentation/git-history.adoc: SYNOPSIS
+ git history abort
+ git history continue
+ git history quit
++git history drop <commit>
DESCRIPTION
-----------
@@ Documentation/git-history.adoc: COMMANDS
This command requires a subcommand. Several subcommands are available to
- rewrite history in different ways.
+ rewrite history in different ways:
-+drop <revision>::
++`drop <commit>`::
+ Drop a commit from the history and reapply all children of that
+ commit on top of the commit's parent. The commit that is to be
+ dropped must be reachable from the current `HEAD` commit.
@@ Documentation/git-history.adoc: COMMANDS
+root commit. It is invalid to drop a root commit that does not have any
+child commits, as that would lead to an empty branch.
+
+ The following commands are used to manage an interrupted history-rewriting
+ operation:
+
+@@ Documentation/git-history.adoc: operation:
+ the original branch. The index and working tree are also left unchanged
+ as a result.
+
+EXAMPLES
+--------
+
-+* Drop a commit from history.
-++
++Drop a commit from history
++~~~~~~~~~~~~~~~~~~~~~~~~~~
++
+----------
+$ git log --oneline
+2d4cd6d third
@@ Documentation/git-history.adoc: COMMANDS
## builtin/history.c ##
@@
#include "builtin.h"
+ #include "branch.h"
+#include "commit.h"
+#include "commit-reach.h"
+#include "config.h"
@@ builtin/history.c
+#include "refs.h"
+#include "reset.h"
+#include "revision.h"
-+#include "sequencer.h"
-+
+ #include "sequencer.h"
+
+ static int cmd_history_abort(int argc,
+@@ builtin/history.c: static int cmd_history_quit(int argc,
+ return ret;
+ }
+
+static int collect_commits(struct repository *repo,
+ struct commit *old_commit,
+ struct commit *new_commit,
@@ builtin/history.c
+ int ret;
+
+ /*
-+ * Check that the old actually is an ancestor of HEAD. If not
++ * Check that the old commit actually is an ancestor of HEAD. If not
+ * the whole request becomes nonsensical.
+ */
+ if (old_commit) {
@@ builtin/history.c
+
+ /*
+ * Revisions are in newest-order-first. We have to reverse the
-+ * array though so that we pick the oldest commits first. Note
-+ * that we keep the first string untouched, as it is the
-+ * equivalent of `argv[0]` to `setup_revisions()`.
++ * array though so that we pick the oldest commits first.
+ */
+ for (size_t i = 0, j = out->nr - 1; i < j; i++, j--)
+ SWAP(out->v[i], out->v[j]);
@@ builtin/history.c
+ * We have performed all safety checks, so we now prepare
+ * replaying the commits.
+ */
-+ replay_opts.action = REPLAY_PICK;
++ replay_opts.action = REPLAY_HISTORY_EDIT;
+ sequencer_init_config(&replay_opts);
+ replay_opts.quiet = 1;
+ replay_opts.skip_commit_summary = 1;
@@ builtin/history.c
+ * squash that root commit with the first commit we're picking
+ * onto it.
+ */
-+ if (!base) {
++ if (!base->parents) {
+ if (commit_tree("", 0, repo->hash_algo->empty_tree, NULL,
+ &root_commit, NULL, NULL) < 0) {
+ ret = error(_("Could not create new root commit"));
@@ builtin/history.c
+ replay_opts.have_squash_onto = 1;
+ reset_opts.oid = &root_commit;
+ } else {
-+ reset_opts.oid = &base->object.oid;
++ reset_opts.oid = &base->parents->item->object.oid;
+ }
+
+ replay_opts.restore_head_target =
@@ builtin/history.c
+ struct repository *repo)
+{
+ const char * const usage[] = {
-+ N_("git history drop [<options>] <revision>"),
++ N_("git history drop <commit>"),
+ NULL,
+ };
+ struct option options[] = {
@@ builtin/history.c
+ if (ret < 0)
+ goto out;
+
-+ ret = apply_commits(repo, &commits, head, commit_to_drop->parents ?
-+ commit_to_drop->parents->item : NULL, "drop");
++ ret = apply_commits(repo, &commits, head, commit_to_drop, "drop");
+ if (ret < 0)
+ goto out;
+ }
@@ builtin/history.c
+ strbuf_release(&buf);
+ return ret;
+}
-
++
int cmd_history(int argc,
const char **argv,
const char *prefix,
-- struct repository *repo UNUSED)
-+ struct repository *repo)
- {
- const char * const usage[] = {
-- N_("git history [<options>]"),
-+ N_("git history drop [<options>] <revision>"),
+@@ builtin/history.c: int cmd_history(int argc,
+ N_("git history abort"),
+ N_("git history continue"),
+ N_("git history quit"),
++ N_("git history drop <commit>"),
NULL,
};
-+ parse_opt_subcommand_fn *fn = NULL;
- struct option options[] = {
+ parse_opt_subcommand_fn *fn = NULL;
+@@ builtin/history.c: int cmd_history(int argc,
+ OPT_SUBCOMMAND("abort", &fn, cmd_history_abort),
+ OPT_SUBCOMMAND("continue", &fn, cmd_history_continue),
+ OPT_SUBCOMMAND("quit", &fn, cmd_history_quit),
+ OPT_SUBCOMMAND("drop", &fn, cmd_history_drop),
OPT_END(),
};
- argc = parse_options(argc, argv, prefix, options, usage, 0);
-- return 0;
-+ return fn(argc, argv, prefix, repo);
- }
## t/meson.build ##
@@ t/meson.build: integration_tests = [
- 't3436-rebase-more-options.sh',
't3437-rebase-fixup-options.sh',
't3438-rebase-broken-files.sh',
-+ 't3450-history-drop.sh',
+ 't3450-history.sh',
++ 't3451-history-drop.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
-@@ t/meson.build: if perl.found() and time.found()
- timeout: 0,
- )
- endforeach
--endif
- \ No newline at end of file
-+endif
- ## t/t3450-history-drop.sh (new) ##
+ ## t/t3451-history-drop.sh (new) ##
@@
+#!/bin/sh
+
@@ t/t3450-history-drop.sh (new)
+ )
+'
+
++test_expect_success 'refuses to work with changes in the worktree or index' '
++ test_when_finished "rm -rf repo" &&
++ git init repo &&
++ (
++ cd repo &&
++ test_commit initial &&
++ test_commit file file &&
++ echo foo >file &&
++ test_must_fail git history drop HEAD 2>err &&
++ test_grep "Your local changes to the following files would be overwritten" err &&
++ git add file &&
++ test_must_fail git history drop HEAD 2>err &&
++ test_grep "Your local changes to the following files would be overwritten" err
++ )
++'
++
+test_expect_success 'can drop tip of a branch' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
@@ t/t3450-history-drop.sh (new)
+ )
+'
+
++test_expect_success 'conflicts are detected' '
++ test_when_finished "rm -rf repo" &&
++ git init repo &&
++ (
++ cd repo &&
++ test_commit base &&
++ echo original >file &&
++ git add . &&
++ git commit -m original &&
++ echo modified >file &&
++ git commit -am modified &&
++
++ test_must_fail git history drop HEAD~ >err 2>&1 &&
++ test_grep CONFLICT err &&
++ test_grep "git history continue" err &&
++ echo resolved >file &&
++ git add file &&
++ git history continue &&
++
++ cat >expect <<-EOF &&
++ modified
++ base
++ EOF
++ git log --format=%s >actual &&
++ test_cmp expect actual &&
++ echo resolved >expect &&
++ git cat-file -p HEAD:file >actual &&
++ test_cmp expect actual
++ )
++'
++
+test_done
6: 5ba28ca5e5 ! 9: 95ce67205d builtin/history: implement "reorder" subcommand
@@ Commit message
builtin/history: implement "reorder" subcommand
When working in projects where having nice commits matters it's quite
- common that developers end up reordering commits a lot. Tihs is
+ common that developers end up reordering commits a lot. This is
typically done via interactive rebases, where they can then rearrange
commits in the instruction sheet.
Still, this operation is a frequent-enough operation to provide a more
- direct of doing this imperatively. As such, introduce a new "reorder"
- subcommand where users can reorder a commit A to come after or before
- another commit B:
+ direct way of doing this imperatively. As such, introduce a new
+ "reorder" subcommand where users can reorder a commit A to come after or
+ before another commit B:
$ git log --oneline
a978f73 fifth
@@ Commit message
Signed-off-by: Patrick Steinhardt <ps@pks.im>
## Documentation/git-history.adoc ##
-@@ Documentation/git-history.adoc: SYNOPSIS
- --------
- [synopsis]
- git history drop [<options>] <revision>
-+git history reorder [<options>] <revision> --(before|after)=<revision>
+@@ Documentation/git-history.adoc: git history abort
+ git history continue
+ git history quit
+ git history drop <commit>
++git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)
DESCRIPTION
-----------
-@@ Documentation/git-history.adoc: Dropping the root commit converts the child of that commit into the new
+@@ Documentation/git-history.adoc: rewrite history in different ways:
+ `drop <commit>`::
+ Drop a commit from the history and reapply all children of that
+ commit on top of the commit's parent. The commit that is to be
+- dropped must be reachable from the current `HEAD` commit.
++ dropped must be reachable from the currently checked-out commit.
+ +
+ Dropping the root commit converts the child of that commit into the new
root commit. It is invalid to drop a root commit that does not have any
child commits, as that would lead to an empty branch.
-+reorder <revision> (--before=<revision>|--after=<revision>)::
-+ Reorder the commit so that it becomes either the parent
-+ (`--before=`) or child (`--after=`) of the other specified
-+ commit. The commits must be related to one another and must be
-+ reachable from the current `HEAD` commit.
-+
- EXAMPLES
- --------
++`reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)`::
++ Move the commit so that it becomes either the parent of
++ <following-commit> or the child of <preceding-commit>. The commits must
++ be related to one another and must be reachable from the current `HEAD`
++ commit.
++
+ The following commands are used to manage an interrupted history-rewriting
+ operation:
@@ Documentation/git-history.adoc: b1bc1bd third
e098c27 first
----------
-+* Reorder a commit.
-++
++Reorder a commit
++~~~~~~~~~~~~~~~~
++
+----------
+$ git log --oneline
+a978f73 fifth
@@ builtin/history.c: static int cmd_history_drop(int argc,
+ struct repository *repo)
+{
+ const char * const usage[] = {
-+ N_("git history reorder [<options>] <revision> (--before=<commit>|--after=<commit>)"),
++ N_("git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)"),
+ NULL,
+ };
+ const char *before = NULL, *after = NULL;
@@ builtin/history.c: static int cmd_history_drop(int argc,
+ replace_commits(&commits, &commit_to_reorder->object.oid, NULL, 0);
+ replace_commits(&commits, &anchor->object.oid, replacement, ARRAY_SIZE(replacement));
+
-+ /*
-+ * And now we pick commits in the new order on top of either the root
-+ * commit or on top the old commit's parent.
-+ */
-+ ret = apply_commits(repo, &commits, head,
-+ old->parents ? old->parents->item : NULL, "reorder");
++ ret = apply_commits(repo, &commits, head, old, "reorder");
+ if (ret < 0)
+ goto out;
+
@@ builtin/history.c: static int cmd_history_drop(int argc,
const char **argv,
const char *prefix,
@@ builtin/history.c: int cmd_history(int argc,
- {
- const char * const usage[] = {
- N_("git history drop [<options>] <revision>"),
-+ N_("git history reorder [<options>] <revision> --(before|after)=<revision>"),
+ N_("git history continue"),
+ N_("git history quit"),
+ N_("git history drop <commit>"),
++ N_("git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)"),
NULL,
};
parse_opt_subcommand_fn *fn = NULL;
- struct option options[] = {
+@@ builtin/history.c: int cmd_history(int argc,
+ OPT_SUBCOMMAND("continue", &fn, cmd_history_continue),
+ OPT_SUBCOMMAND("quit", &fn, cmd_history_quit),
OPT_SUBCOMMAND("drop", &fn, cmd_history_drop),
+ OPT_SUBCOMMAND("reorder", &fn, cmd_history_reorder),
OPT_END(),
@@ builtin/history.c: int cmd_history(int argc,
## t/meson.build ##
@@ t/meson.build: integration_tests = [
- 't3437-rebase-fixup-options.sh',
't3438-rebase-broken-files.sh',
- 't3450-history-drop.sh',
-+ 't3451-history-reorder.sh',
+ 't3450-history.sh',
+ 't3451-history-drop.sh',
++ 't3452-history-reorder.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
- ## t/t3451-history-reorder.sh (new) ##
+ ## t/t3452-history-reorder.sh (new) ##
@@
+#!/bin/sh
+
@@ t/t3451-history-reorder.sh (new)
+
+. ./test-lib.sh
+
-+test_expect_success 'reorder refuses to work with merge commits' '
++test_expect_success 'refuses to work with merge commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
@@ t/t3451-history-reorder.sh (new)
+ )
+'
+
-+test_expect_success 'reorder requires exactly one of --before or --after' '
++test_expect_success 'refuses to work with changes in the worktree or index' '
++ test_when_finished "rm -rf repo" &&
++ git init repo &&
++ (
++ cd repo &&
++ test_commit initial &&
++ test_commit file file &&
++ echo foo >file &&
++ test_must_fail git history reorder HEAD --before=HEAD~ 2>err &&
++ test_grep "Your local changes to the following files would be overwritten" err &&
++ git add file &&
++ test_must_fail git history reorder HEAD --before=HEAD~ 2>err &&
++ test_grep "Your local changes to the following files would be overwritten" err
++ )
++'
++
++test_expect_success 'requires exactly one of --before or --after' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
@@ t/t3451-history-reorder.sh (new)
+ )
+'
+
-+test_expect_success 'reorder refuses to reorder commit with itself' '
++test_expect_success 'refuses to reorder commit with itself' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
@@ t/t3451-history-reorder.sh (new)
+ test_must_fail git symbolic-ref HEAD &&
+ echo "second edit" >file &&
+ git add file &&
-+ test_must_fail git cherry-pick --continue &&
++ test_must_fail git history continue &&
+ echo "first edit" >file &&
+ git add file &&
-+ git cherry-pick --continue &&
++ git history continue &&
+
+ cat >expect <<-EOF &&
+ first edit
7: 91221a3883 = 10: 1bc1d4f06c add-patch: split out header from "add-interactive.h"
8: 018d7bd8ea = 11: fed38713fa add-patch: split out `struct interactive_options`
9: 915376b78a = 12: d8ba71c015 add-patch: remove dependency on "add-interactive" subsystem
10: 3fa285e5b8 ! 13: b56722b519 add-patch: add support for in-memory index patching
@@ add-patch.c: static int patch_update_file(struct add_p_state *s,
error(_("'git apply' failed"));
}
- if (repo_read_index(s->r) >= 0)
-+ read_index_from(s->index, s->index_file, s->r->gitdir);
+ if (read_index_from(s->index, s->index_file, s->r->gitdir) >= 0 &&
+ s->index == s->r->index) {
repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,
-: ---------- > 14: 5a6b18855e wt-status: provide function to expose status for trees
11: 81e16d3c01 ! 15: 4701b4dff2 builtin/history: implement "split" subcommand
@@ Commit message
Signed-off-by: Patrick Steinhardt <ps@pks.im>
## Documentation/git-history.adoc ##
-@@ Documentation/git-history.adoc: SYNOPSIS
- [synopsis]
- git history drop [<options>] <revision>
- git history reorder [<options>] <revision> --(before|after)=<revision>
-+git history split [<options>] <revision> [--] [<pathspec>...]
+@@ Documentation/git-history.adoc: git history continue
+ git history quit
+ git history drop <commit>
+ git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)
++git history split [<options>] <commit> [--] [<pathspec>...]
DESCRIPTION
-----------
-@@ Documentation/git-history.adoc: reorder <revision> (--before=<revision>|--after=<revision>)::
- commit. The commits must be related to one another and must be
- reachable from the current `HEAD` commit.
+@@ Documentation/git-history.adoc: child commits, as that would lead to an empty branch.
+ be related to one another and must be reachable from the current `HEAD`
+ commit.
-+split <revision> [--message=<message>] [--] [<pathspec>...]::
-+ Interactively split up the commit into two commits by choosing
++`split [--message=<message>] <commit> [--] [<pathspec>...]`::
++ Interactively split up <commit> into two commits by choosing
+ hunks introduced by it that will be moved into the new split-out
+ commit. These hunks will then be written into a new commit that
+ becomes the parent of the previous commit. The original commit
@@ Documentation/git-history.adoc: reorder <revision> (--before=<revision>|--after=
+ commit.
++
+The commit message of the new commit will be asked for by launching the
-+configured editor. Authorship of the commit will be the same as for the
-+original commit.
++configured editor, unless it has been specified with the `-m` option.
++Authorship of the commit will be the same as for the original commit.
++
+If passed, _<pathspec>_ can be used to limit which changes shall be split out
+of the original commit. Files not matching any of the pathspecs will remain
-+part of the original commit. For more details about the _<pathspec>_ syntax,
-+see the 'pathspec' entry.
++part of the original commit. For more details, see the 'pathspec' entry in
++linkgit:gitglossary[7].
++
+It is invalid to select either all or no hunks, as that would lead to
+one of the commits becoming empty.
+
- EXAMPLES
- --------
+ The following commands are used to manage an interrupted history-rewriting
+ operation:
@@ Documentation/git-history.adoc: f44a46e third
bf7438d first
----------
-+* Split a commit.
-++
++Split a commit
++~~~~~~~~~~~~~~
++
+----------
+$ git log --stat --oneline
+3f81232 (HEAD -> main) original
@@ builtin/history.c
+#define USE_THE_REPOSITORY_VARIABLE
+
#include "builtin.h"
+ #include "branch.h"
+#include "cache-tree.h"
#include "commit.h"
#include "commit-reach.h"
@@ builtin/history.c
#include "sequencer.h"
+#include "sparse-index.h"
- static int collect_commits(struct repository *repo,
- struct commit *old_commit,
+ static int cmd_history_abort(int argc,
+ const char **argv,
@@ builtin/history.c: static int cmd_history_reorder(int argc,
return ret;
}
++static void change_data_free(void *util, const char *str UNUSED)
++{
++ struct wt_status_change_data *d = util;
++ free(d->rename_source);
++ free(d);
++}
++
++static int fill_commit_message(struct repository *repo,
++ const struct object_id *old_tree,
++ const struct object_id *new_tree,
++ const char *default_message,
++ const char *provided_message,
++ const char *action,
++ struct strbuf *out)
++{
++ if (!provided_message) {
++ struct wt_status s;
++ const char *path = git_path_commit_editmsg();
++ const char *hint =
++ _("Please enter the commit message for the %s changes. Lines starting\n"
++ "with '%s' will be kept; you may remove them yourself if you want to.\n");
++
++ strbuf_addstr(out, default_message);
++ strbuf_addch(out, '\n');
++ strbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);
++ write_file_buf(path, out->buf, out->len);
++
++ wt_status_prepare(repo, &s);
++ FREE_AND_NULL(s.branch);
++ s.ahead_behind_flags = AHEAD_BEHIND_QUICK;
++ s.commit_template = 1;
++ s.colopts = 0;
++ s.display_comment_prefix = 1;
++ s.hints = 0;
++ s.use_color = 0;
++ s.whence = FROM_COMMIT;
++ s.committable = 1;
++
++ s.fp = fopen(git_path_commit_editmsg(), "a");
++ if (!s.fp)
++ return error_errno(_("could not open '%s'"), git_path_commit_editmsg());
++
++ wt_status_collect_changes_trees(&s, old_tree, new_tree);
++ wt_status_print(&s);
++ wt_status_collect_free_buffers(&s);
++ string_list_clear_func(&s.change, change_data_free);
++
++ strbuf_reset(out);
++ if (launch_editor(path, out, NULL)) {
++ fprintf(stderr, _("Please supply the message using either -m or -F option.\n"));
++ return -1;
++ }
++ strbuf_stripspace(out, comment_line_str);
++
++ } else {
++ strbuf_addstr(out, provided_message);
++ }
++
++ cleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);
++
++ if (!out->len) {
++ fprintf(stderr, _("Aborting commit due to empty commit message.\n"));
++ return -1;
++ }
++
++ return 0;
++}
++
+static int split_commit(struct repository *repo,
+ struct commit *original_commit,
+ struct pathspec *pathspec,
@@ builtin/history.c: static int cmd_history_reorder(int argc,
+ struct object_id original_commit_tree_oid, parent_tree_oid;
+ const char *original_message, *original_body, *ptr;
+ char original_commit_oid[GIT_MAX_HEXSZ + 1];
-+ char *split_message_path = NULL, *original_author = NULL;
++ char *original_author = NULL;
+ struct commit_list *parents = NULL;
+ struct commit *first_commit;
+ struct tree *split_tree;
@@ builtin/history.c: static int cmd_history_reorder(int argc,
+ if (ptr)
+ original_author = xmemdupz(ptr, len);
+
-+ /*
-+ * But we do ask the user for a new commit message. This is in contrast
-+ * to the second commit, where we'll retain the original commit
-+ * message.
-+ */
-+ if (!commit_message) {
-+ split_message_path = repo_git_path(repo, "SPLIT_MSG");
-+ strbuf_addch(&split_message, '\n');
-+ strbuf_commented_addf(&split_message, comment_line_str,
-+ _("Please enter a commit message for the split-out changes."));
-+ write_file_buf(split_message_path, split_message.buf, split_message.len);
-+
-+ strbuf_reset(&split_message);
-+ if (launch_editor(split_message_path, &split_message, NULL)) {
-+ fprintf(stderr, _("Please supply the message using either -m or -F option.\n"));
-+ ret = -1;
-+ goto out;
-+ }
-+ strbuf_stripspace(&split_message, comment_line_str);
-+ } else {
-+ strbuf_addstr(&split_message, commit_message);
-+ }
-+ cleanup_message(&split_message, COMMIT_MSG_CLEANUP_ALL, 0);
++ ret = fill_commit_message(repo, &parent_tree_oid, &split_tree->object.oid,
++ "", commit_message, "split-out", &split_message);
++ if (ret < 0)
++ goto out;
+
+ ret = commit_tree(split_message.buf, split_message.len, &split_tree->object.oid,
+ original_commit->parents, &out[0], original_author, NULL);
@@ builtin/history.c: static int cmd_history_reorder(int argc,
+ strbuf_release(&split_message);
+ strbuf_release(&index_file);
+ free_commit_list(parents);
-+ free(split_message_path);
+ free(original_author);
+ release_index(&index);
+ return ret;
@@ builtin/history.c: static int cmd_history_reorder(int argc,
+ struct repository *repo)
+{
+ const char * const usage[] = {
-+ N_("git history split [<options>] <revision>"),
++ N_("git history split [<options>] <commit>"),
+ NULL,
+ };
+ const char *commit_message = NULL;
@@ builtin/history.c: static int cmd_history_reorder(int argc,
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
++ if (argc < 1) {
++ ret = error(_("command expects a revision"));
++ goto out;
++ }
+ repo_config(repo, git_default_config, NULL);
+
+ original_commit = lookup_commit_reference_by_name(argv[0]);
@@ builtin/history.c: static int cmd_history_reorder(int argc,
+ replace_commits(&commits, &original_commit->object.oid,
+ split_commits, ARRAY_SIZE(split_commits));
+
-+ /*
-+ * And now we pick commits in the new order on top of either the root
-+ * commit or on top the old commit's parent.
-+ */
-+ ret = apply_commits(repo, &commits, head,
-+ original_commit->parents ? original_commit->parents->item : NULL,
-+ "split");
++ ret = apply_commits(repo, &commits, head, original_commit, "split");
+ if (ret < 0)
+ goto out;
+
@@ builtin/history.c: static int cmd_history_reorder(int argc,
const char **argv,
const char *prefix,
@@ builtin/history.c: int cmd_history(int argc,
- const char * const usage[] = {
- N_("git history drop [<options>] <revision>"),
- N_("git history reorder [<options>] <revision> --(before|after)=<revision>"),
-+ N_("git history split [<options>] <revision> [--] [<pathspec>...]"),
+ N_("git history quit"),
+ N_("git history drop <commit>"),
+ N_("git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)"),
++ N_("git history split [<options>] <commit> [--] [<pathspec>...]"),
NULL,
};
parse_opt_subcommand_fn *fn = NULL;
- struct option options[] = {
+@@ builtin/history.c: int cmd_history(int argc,
+ OPT_SUBCOMMAND("quit", &fn, cmd_history_quit),
OPT_SUBCOMMAND("drop", &fn, cmd_history_drop),
OPT_SUBCOMMAND("reorder", &fn, cmd_history_reorder),
+ OPT_SUBCOMMAND("split", &fn, cmd_history_split),
@@ builtin/history.c: int cmd_history(int argc,
## t/meson.build ##
@@ t/meson.build: integration_tests = [
- 't3438-rebase-broken-files.sh',
- 't3450-history-drop.sh',
- 't3451-history-reorder.sh',
-+ 't3452-history-split.sh',
+ 't3450-history.sh',
+ 't3451-history-drop.sh',
+ 't3452-history-reorder.sh',
++ 't3453-history-split.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
- ## t/t3452-history-split.sh (new) ##
+ ## t/t3453-history-split.sh (new) ##
@@
+#!/bin/sh
+
@@ t/t3452-history-split.sh (new)
+ )
+'
+
++test_expect_success 'refuses to work with changes in the worktree or index' '
++ test_when_finished "rm -rf repo" &&
++ git init repo &&
++ (
++ cd repo &&
++ test_commit initial &&
++ touch bar foo &&
++ git add . &&
++ git commit -m split-me &&
++
++ echo changed >bar &&
++ test_must_fail git history split -m message HEAD 2>err <<-EOF &&
++ y
++ n
++ EOF
++ test_grep "Your local changes to the following files would be overwritten" err &&
++
++ git add bar &&
++ test_must_fail git history split -m message HEAD 2>err <<-EOF &&
++ y
++ n
++ EOF
++ test_grep "Your local changes to the following files would be overwritten" err
++ )
++'
++
+test_expect_success 'can split up tip commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
@@ t/t3452-history-split.sh (new)
+ git add . &&
+ git commit -m split-me &&
+
++ git symbolic-ref HEAD >expect &&
+ set_fake_editor &&
+ git history split HEAD <<-EOF &&
+ y
+ n
+ EOF
++ git symbolic-ref HEAD >actual &&
++ test_cmp expect actual &&
+
+ expect_log <<-EOF &&
+ split-me
@@ t/t3452-history-split.sh (new)
+ )
+'
+
++test_expect_success 'aborts with empty commit message' '
++ test_when_finished "rm -rf repo" &&
++ git init repo &&
++ (
++ cd repo &&
++ touch bar foo &&
++ git add . &&
++ git commit -m split-me &&
++
++ test_must_fail git history split HEAD -m "" <<-EOF 2>err &&
++ y
++ n
++ EOF
++ test_grep "Aborting commit due to empty commit message." err
++ )
++'
++
+test_expect_success 'can specify message via option' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
@@ t/t3452-history-split.sh (new)
+ )
+'
+
++test_expect_success 'commit message editor sees split-out changes' '
++ test_when_finished "rm -rf repo" &&
++ git init repo &&
++ (
++ cd repo &&
++ touch bar foo &&
++ git add . &&
++ git commit -m split-me &&
++
++ write_script fake-editor.sh <<-\EOF &&
++ cp "$1" . &&
++ echo "some commit message" >>"$1"
++ EOF
++ test_set_editor "$(pwd)"/fake-editor.sh &&
++
++ git history split HEAD <<-EOF &&
++ y
++ n
++ EOF
++
++ cat >expect <<-EOF &&
++
++ # Please enter the commit message for the split-out changes. Lines starting
++ # with ${SQ}#${SQ} will be kept; you may remove them yourself if you want to.
++ # Changes to be committed:
++ # new file: bar
++ #
++ EOF
++ test_cmp expect COMMIT_EDITMSG &&
++
++ expect_log <<-EOF
++ split-me
++ some commit message
++ EOF
++ )
++'
++
+test_expect_success 'can use pathspec to limit what gets split' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
-: ---------- > 16: 3e3587d375 builtin/history: implement "reword" subcommand
---
base-commit: c44beea485f0f2feaf460e2ac87fdd5608d63cf0
change-id: 20250819-b4-pks-history-builtin-83398f9a05f0
^ permalink raw reply [flat|nested] 278+ messages in thread* [PATCH RFC v2 01/16] sequencer: optionally skip printing commit summary
2025-08-24 17:42 ` [PATCH RFC v2 00/16] " Patrick Steinhardt
@ 2025-08-24 17:42 ` Patrick Steinhardt
2025-08-24 17:42 ` [PATCH RFC v2 02/16] sequencer: add option to rewind HEAD after picking commits Patrick Steinhardt
` (15 subsequent siblings)
16 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-24 17:42 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
When picking commits by using for example git-cherry-pick(1) we end up
printing a commit summary that gives the reader information around what
exactly we have been picking:
```
$ git cherry-pick main
[other 76c8456] bar
Date: Tue Aug 19 08:07:26 2025 +0200
1 file changed, 1 insertion(+)
create mode 100644 bar
```
While useful for some commands, we're about to introduce a new command
where this output will be less so. But right now there is no way to
disable printing this commit summary.
Introduce a new `skip_commit_summary` replay option that does so.
Persist the option into the sequencer configuration so that it persists
across different processes, e.g. when we need to stop due to a merge
conflict.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
sequencer.c | 12 +++++++++---
sequencer.h | 1 +
2 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/sequencer.c b/sequencer.c
index aaf2e4df64..7066cdc939 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -1742,7 +1742,7 @@ static int do_commit(struct repository *r,
refs_delete_ref(get_main_ref_store(r), "",
"CHERRY_PICK_HEAD", NULL, REF_NO_DEREF);
unlink(git_path_merge_msg(r));
- if (!is_rebase_i(opts))
+ if (!is_rebase_i(opts) && !opts->skip_commit_summary)
print_commit_summary(r, NULL, &oid,
SUMMARY_SHOW_AUTHOR_DATE);
return res;
@@ -3139,8 +3139,12 @@ static int populate_opts_cb(const char *key, const char *value,
else if (!strcmp(key, "options.default-msg-cleanup")) {
opts->explicit_cleanup = 1;
opts->default_msg_cleanup = get_cleanup_mode(value, 1);
- } else
+ } else if (!strcmp(key, "options.skip-commit-summary")) {
+ opts->skip_commit_summary =
+ git_config_bool_or_int(key, value, ctx->kvi, &error_flag);
+ } else {
return error(_("invalid key: %s"), key);
+ }
if (!error_flag)
return error(_("invalid value for '%s': '%s'"), key, value);
@@ -3698,11 +3702,13 @@ static int save_opts(struct replay_opts *opts)
"options.allow-rerere-auto", NULL,
opts->allow_rerere_auto == RERERE_AUTOUPDATE ?
"true" : "false");
-
if (opts->explicit_cleanup)
res |= repo_config_set_in_file_gently(the_repository, opts_file,
"options.default-msg-cleanup", NULL,
describe_cleanup_mode(opts->default_msg_cleanup));
+ if (opts->skip_commit_summary)
+ res |= repo_config_set_in_file_gently(the_repository, opts_file,
+ "options.skip-commit-summary", NULL, "true");
return res;
}
diff --git a/sequencer.h b/sequencer.h
index 304ba4b4d3..1767fd737e 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -52,6 +52,7 @@ struct replay_opts {
int keep_redundant_commits;
int verbose;
int quiet;
+ int skip_commit_summary;
int reschedule_failed_exec;
int committer_date_is_author_date;
int ignore_date;
--
2.51.0.308.g032396e0da.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH RFC v2 02/16] sequencer: add option to rewind HEAD after picking commits
2025-08-24 17:42 ` [PATCH RFC v2 00/16] " Patrick Steinhardt
2025-08-24 17:42 ` [PATCH RFC v2 01/16] sequencer: optionally skip printing commit summary Patrick Steinhardt
@ 2025-08-24 17:42 ` Patrick Steinhardt
2025-08-24 17:42 ` [PATCH RFC v2 03/16] sequencer: introduce new history editing mode Patrick Steinhardt
` (14 subsequent siblings)
16 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-24 17:42 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
While the sequencer infrastructure knows to rewind "HEAD" to whatever it
was pointing to before a rebase, it doesn't do the same for non-rebase
operations like cherry-picks. This is because the expectation is that
the user directly picks commits on top of whatever "HEAD" points to, and
we advance the reference pointed to by "HEAD" instead of updating it
directly.
We're about to introduce a new command though that needs to detach
"HEAD" while being more similar to git-cherry-pick(1) rathen than to
git-rebase(1). As such, we'll want to restore "HEAD" to point to the
branch that we started on while not using the more heavy-weight rebase
machinery.
Introduce a new option `restore_head_target` to do so. Persist the
option into the sequencer configuration so that it persists across
different processes, e.g. when we need to stop due to a merge conflict.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
sequencer.c | 27 ++++++++++++++++++++++++++-
sequencer.h | 3 +++
2 files changed, 29 insertions(+), 1 deletion(-)
diff --git a/sequencer.c b/sequencer.c
index 7066cdc939..bff181df76 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -413,6 +413,7 @@ void replay_opts_release(struct replay_opts *opts)
struct replay_ctx *ctx = opts->ctx;
free(opts->gpg_sign);
+ free(opts->restore_head_target);
free(opts->reflog_action);
free(opts->default_strategy);
free(opts->strategy);
@@ -3142,6 +3143,8 @@ static int populate_opts_cb(const char *key, const char *value,
} else if (!strcmp(key, "options.skip-commit-summary")) {
opts->skip_commit_summary =
git_config_bool_or_int(key, value, ctx->kvi, &error_flag);
+ } else if (!strcmp(key, "options.restore-head-target")) {
+ git_config_string_dup(&opts->restore_head_target, key, value);
} else {
return error(_("invalid key: %s"), key);
}
@@ -3709,6 +3712,10 @@ static int save_opts(struct replay_opts *opts)
if (opts->skip_commit_summary)
res |= repo_config_set_in_file_gently(the_repository, opts_file,
"options.skip-commit-summary", NULL, "true");
+ if (opts->restore_head_target)
+ res |= repo_config_set_in_file_gently(the_repository, opts_file,
+ "options.restore-head-target", NULL, opts->restore_head_target);
+
return res;
}
@@ -5177,6 +5184,23 @@ static int pick_commits(struct repository *r,
return -1;
}
+ if (opts->restore_head_target) {
+ struct reset_head_opts reset_opts = { 0 };
+ const char *msg;
+
+ msg = reflog_message(opts, "finish", "returning to %s", opts->restore_head_target);
+
+ reset_opts.branch = opts->restore_head_target;
+ reset_opts.flags = RESET_HEAD_REFS_ONLY;
+ reset_opts.branch_msg = msg;
+ reset_opts.head_msg = msg;
+
+ if (reset_head(r, &reset_opts)) {
+ error(_("could not switch HEAD back to %s"), opts->restore_head_target);
+ return -1;
+ }
+ }
+
/*
* Sequence of picks finished successfully; cleanup by
* removing the .git/sequencer directory
@@ -5533,7 +5557,8 @@ int sequencer_pick_revisions(struct repository *r,
if (opts->revs->cmdline.nr == 1 &&
opts->revs->cmdline.rev->whence == REV_CMD_REV &&
opts->revs->no_walk &&
- !opts->revs->cmdline.rev->flags) {
+ !opts->revs->cmdline.rev->flags &&
+ !opts->restore_head_target) {
struct commit *cmit;
if (prepare_revision_walk(opts->revs)) {
diff --git a/sequencer.h b/sequencer.h
index 1767fd737e..a905f6afc7 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -72,6 +72,9 @@ struct replay_opts {
/* Reflog */
char *reflog_action;
+ /* Reference to which HEAD shall be reset to after the operation. */
+ char *restore_head_target;
+
/* placeholder commit for -i --root */
struct object_id squash_onto;
int have_squash_onto;
--
2.51.0.308.g032396e0da.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH RFC v2 03/16] sequencer: introduce new history editing mode
2025-08-24 17:42 ` [PATCH RFC v2 00/16] " Patrick Steinhardt
2025-08-24 17:42 ` [PATCH RFC v2 01/16] sequencer: optionally skip printing commit summary Patrick Steinhardt
2025-08-24 17:42 ` [PATCH RFC v2 02/16] sequencer: add option to rewind HEAD after picking commits Patrick Steinhardt
@ 2025-08-24 17:42 ` Patrick Steinhardt
2025-08-26 12:55 ` D. Ben Knoble
2025-08-24 17:42 ` [PATCH RFC v2 04/16] sequencer: stop using `the_repository` in `sequencer_remove_state()` Patrick Steinhardt
` (13 subsequent siblings)
16 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-24 17:42 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
Introduce a new history editing mode into our sequencer machinery. This
mode is basically the same as `REBASE_CHERRY`, but will be used by the
new git-history(1) command that is to be introduced in a subsequent
commit.
Note that the advice already points towards the git-history(1) command.
This advice is bogus right now, but we'll introduce the relevant infra
in a subsequent commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
sequencer.c | 130 ++++++++++++++++++++++++++++++++++++++++++-----------
sequencer.h | 3 +-
t/t3450-history.sh | 12 +++++
3 files changed, 119 insertions(+), 26 deletions(-)
diff --git a/sequencer.c b/sequencer.c
index bff181df76..898ac1a2a8 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -465,6 +465,8 @@ static const char *action_name(const struct replay_opts *opts)
return N_("cherry-pick");
case REPLAY_INTERACTIVE_REBASE:
return N_("rebase");
+ case REPLAY_HISTORY_EDIT:
+ return N_("history edit");
}
die(_("unknown action: %d"), opts->action);
}
@@ -557,6 +559,13 @@ static void print_advice(struct repository *r, int show_hint,
"You can instead skip this commit with \"git revert --skip\".\n"
"To abort and get back to the state before \"git revert\",\n"
"run \"git revert --abort\"."));
+ else if (opts->action == REPLAY_HISTORY_EDIT)
+ advise_if_enabled(ADVICE_MERGE_CONFLICT,
+ _("After resolving the conflicts, mark them with\n"
+ "\"git add/rm <pathspec>\", then run\n"
+ "\"git history continue\".\n"
+ "To abort and get back to the state before \"git history\",\n"
+ "run \"git history abort\"."));
else
BUG("unexpected pick action in print_advice()");
}
@@ -1742,6 +1751,8 @@ static int do_commit(struct repository *r,
if (!res) {
refs_delete_ref(get_main_ref_store(r), "",
"CHERRY_PICK_HEAD", NULL, REF_NO_DEREF);
+ refs_delete_ref(get_main_ref_store(r), "",
+ "HISTORY_EDIT_HEAD", NULL, REF_NO_DEREF);
unlink(git_path_merge_msg(r));
if (!is_rebase_i(opts) && !opts->skip_commit_summary)
print_commit_summary(r, NULL, &oid,
@@ -2491,16 +2502,24 @@ static int do_pick_commit(struct repository *r,
* However, if the merge did not even start, then we don't want to
* write it at all.
*/
- if ((command == TODO_PICK || command == TODO_REWORD ||
- command == TODO_EDIT) && !opts->no_commit &&
- (res == 0 || res == 1) &&
- refs_update_ref(get_main_ref_store(the_repository), NULL, "CHERRY_PICK_HEAD", &commit->object.oid, NULL,
- REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR))
- res = -1;
- if (command == TODO_REVERT && ((opts->no_commit && res == 0) || res == 1) &&
- refs_update_ref(get_main_ref_store(the_repository), NULL, "REVERT_HEAD", &commit->object.oid, NULL,
- REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR))
- res = -1;
+ if (opts->action == REPLAY_HISTORY_EDIT && command == TODO_PICK &&
+ !opts->no_commit && (res == 0 || res == 1)) {
+ if (refs_update_ref(get_main_ref_store(the_repository), NULL,
+ "HISTORY_EDIT_HEAD", &commit->object.oid, NULL,
+ REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR))
+ res = -1;
+ } else if ((command == TODO_PICK || command == TODO_REWORD ||
+ command == TODO_EDIT) && !opts->no_commit && (res == 0 || res == 1)) {
+ if (refs_update_ref(get_main_ref_store(the_repository), NULL,
+ "CHERRY_PICK_HEAD", &commit->object.oid, NULL,
+ REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR))
+ res = -1;
+ } else if (command == TODO_REVERT && ((opts->no_commit && res == 0) || res == 1)) {
+ if (refs_update_ref(get_main_ref_store(the_repository), NULL,
+ "REVERT_HEAD", &commit->object.oid, NULL,
+ REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR))
+ res = -1;
+ }
if (res) {
error(command == TODO_REVERT
@@ -2526,6 +2545,8 @@ static int do_pick_commit(struct repository *r,
unlink(git_path_merge_msg(r));
refs_delete_ref(get_main_ref_store(r), "", "AUTO_MERGE",
NULL, REF_NO_DEREF);
+ refs_delete_ref(get_main_ref_store(r), "", "HISTORY_EDIT_HEAD",
+ NULL, REF_NO_DEREF);
fprintf(stderr,
_("dropping %s %s -- patch contents already upstream\n"),
oid_to_hex(&commit->object.oid), msg.subject);
@@ -2843,12 +2864,17 @@ static int parse_insn_line(struct repository *r, struct replay_opts *opts,
return 0;
}
-int sequencer_get_last_command(struct repository *r UNUSED, enum replay_action *action)
+int sequencer_get_last_command(struct repository *r, enum replay_action *action)
{
const char *todo_file, *bol;
struct strbuf buf = STRBUF_INIT;
int ret = 0;
+ if (refs_ref_exists(get_main_ref_store(r), "HISTORY_EDIT_HEAD")) {
+ *action = REPLAY_HISTORY_EDIT;
+ return 0;
+ }
+
todo_file = git_path_todo_file();
if (strbuf_read_file(&buf, todo_file, 0) < 0) {
if (errno == ENOENT || errno == ENOTDIR)
@@ -2995,6 +3021,15 @@ void sequencer_post_commit_cleanup(struct repository *r, int verbose)
need_cleanup = 1;
}
+ if (refs_ref_exists(get_main_ref_store(r), "HISTORY_EDIT_HEAD")) {
+ if (!refs_delete_ref(get_main_ref_store(r), "",
+ "HISTORY_EDIT_HEAD", NULL, REF_NO_DEREF) &&
+ verbose)
+ warning(_("cancelling a history edit in progress"));
+ opts.action = REPLAY_HISTORY_EDIT;
+ need_cleanup = 1;
+ }
+
if (refs_ref_exists(get_main_ref_store(r), "REVERT_HEAD")) {
if (!refs_delete_ref(get_main_ref_store(r), "", "REVERT_HEAD",
NULL, REF_NO_DEREF) &&
@@ -3052,17 +3087,29 @@ static int read_populate_todo(struct repository *r,
return error(_("no commits parsed."));
if (!is_rebase_i(opts)) {
- enum todo_command valid =
- opts->action == REPLAY_PICK ? TODO_PICK : TODO_REVERT;
+ enum todo_command valid;
int i;
- for (i = 0; i < todo_list->nr; i++)
+ switch (opts->action) {
+ case REPLAY_PICK:
+ case REPLAY_HISTORY_EDIT:
+ valid = TODO_PICK;
+ break;
+ default:
+ valid = TODO_REVERT;
+ break;
+ }
+
+ for (i = 0; i < todo_list->nr; i++) {
if (valid == todo_list->items[i].command)
continue;
else if (valid == TODO_PICK)
- return error(_("cannot cherry-pick during a revert."));
+ return error(_("cannot cherry-pick during a %s."),
+ action_name(opts));
else
- return error(_("cannot revert during a cherry-pick."));
+ return error(_("cannot revert during a %s."),
+ action_name(opts));
+ }
}
if (is_rebase_i(opts)) {
@@ -3353,15 +3400,25 @@ int write_basic_state(struct replay_opts *opts, const char *head_name,
static int walk_revs_populate_todo(struct todo_list *todo_list,
struct replay_opts *opts)
{
- enum todo_command command = opts->action == REPLAY_PICK ?
- TODO_PICK : TODO_REVERT;
- const char *command_string = todo_command_info[command].str;
+ enum todo_command command;
+ const char *command_string;
const char *encoding;
struct commit *commit;
if (prepare_revs(opts))
return -1;
+ switch (opts->action) {
+ case REPLAY_PICK:
+ case REPLAY_HISTORY_EDIT:
+ command = TODO_PICK;
+ break;
+ default:
+ command = TODO_REVERT;
+ break;
+ }
+
+ command_string = todo_command_info[command].str;
encoding = get_log_output_encoding();
while ((commit = get_revision(opts->revs))) {
@@ -3412,6 +3469,11 @@ static int create_seq_dir(struct repository *r)
in_progress_advice =
_("try \"git cherry-pick (--continue | %s--abort | --quit)\"");
break;
+ case REPLAY_HISTORY_EDIT:
+ in_progress_error = _("history edit is already in progress");
+ in_progress_advice =
+ _("try \"git history (continue | abort | quit)\"");
+ break;
default:
BUG("unexpected action in create_seq_dir");
}
@@ -3472,13 +3534,14 @@ static int reset_merge(const struct object_id *oid)
return run_command(&cmd);
}
-static int rollback_single_pick(struct repository *r)
+static int rollback_single_pick(struct repository *r, struct replay_opts *opts)
{
struct object_id head_oid;
if (!refs_ref_exists(get_main_ref_store(r), "CHERRY_PICK_HEAD") &&
+ !refs_ref_exists(get_main_ref_store(r), "HISTORY_EDIT_HEAD") &&
!refs_ref_exists(get_main_ref_store(r), "REVERT_HEAD"))
- return error(_("no cherry-pick or revert in progress"));
+ return error(_("no %s in progress"), action_name(opts));
if (refs_read_ref_full(get_main_ref_store(the_repository), "HEAD", 0, &head_oid, NULL))
return error(_("cannot resolve HEAD"));
if (is_null_oid(&head_oid))
@@ -3509,7 +3572,7 @@ int sequencer_rollback(struct repository *r, struct replay_opts *opts)
* If CHERRY_PICK_HEAD or REVERT_HEAD indicates
* a single-cherry-pick in progress, abort that.
*/
- return rollback_single_pick(r);
+ return rollback_single_pick(r, opts);
}
if (!f)
return error_errno(_("cannot open '%s'"), git_path_head_file());
@@ -5213,8 +5276,9 @@ static int continue_single_pick(struct repository *r, struct replay_opts *opts)
struct child_process cmd = CHILD_PROCESS_INIT;
if (!refs_ref_exists(get_main_ref_store(r), "CHERRY_PICK_HEAD") &&
+ !refs_ref_exists(get_main_ref_store(r), "HISTORY_EDIT_HEAD") &&
!refs_ref_exists(get_main_ref_store(r), "REVERT_HEAD"))
- return error(_("no cherry-pick or revert in progress"));
+ return error(_("no %s in progress"), action_name(opts));
cmd.git_cmd = 1;
strvec_push(&cmd.args, "commit");
@@ -5393,6 +5457,14 @@ static int commit_staged_changes(struct repository *r,
goto out;
}
+ if (refs_ref_exists(get_main_ref_store(r),
+ "HISTORY_EDIT_HEAD") &&
+ refs_delete_ref(get_main_ref_store(r), "",
+ "HISTORY_EDIT_HEAD", NULL, REF_NO_DEREF)) {
+ ret = error(_("could not remove HISTORY_EDIT_HEAD"));
+ goto out;
+ }
+
if (unlink(git_path_merge_msg(r)) && errno != ENOENT) {
ret = error_errno(_("could not remove '%s'"),
git_path_merge_msg(r));
@@ -5471,6 +5543,7 @@ int sequencer_continue(struct repository *r, struct replay_opts *opts)
/* Verify that the conflict has been resolved */
if (refs_ref_exists(get_main_ref_store(r),
"CHERRY_PICK_HEAD") ||
+ refs_ref_exists(get_main_ref_store(r), "HISTORY_EDIT_HEAD") ||
refs_ref_exists(get_main_ref_store(r), "REVERT_HEAD")) {
res = continue_single_pick(r, opts);
if (res)
@@ -5505,8 +5578,15 @@ static int single_pick(struct repository *r,
int check_todo;
struct todo_item item;
- item.command = opts->action == REPLAY_PICK ?
- TODO_PICK : TODO_REVERT;
+ switch (opts->action) {
+ case REPLAY_PICK:
+ case REPLAY_HISTORY_EDIT:
+ item.command = TODO_PICK;
+ break;
+ default:
+ item.command = TODO_REVERT;
+ break;
+ }
item.commit = cmit;
return do_pick_commit(r, &item, opts, 0, &check_todo);
diff --git a/sequencer.h b/sequencer.h
index a905f6afc7..082fbe3e35 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -21,7 +21,8 @@ extern const char *rebase_resolvemsg;
enum replay_action {
REPLAY_REVERT,
REPLAY_PICK,
- REPLAY_INTERACTIVE_REBASE
+ REPLAY_INTERACTIVE_REBASE,
+ REPLAY_HISTORY_EDIT,
};
enum commit_msg_cleanup_mode {
diff --git a/t/t3450-history.sh b/t/t3450-history.sh
new file mode 100755
index 0000000000..9eb1ed6749
--- /dev/null
+++ b/t/t3450-history.sh
@@ -0,0 +1,12 @@
+#!/bin/sh
+
+test_description='tests for git-history command'
+
+. ./test-lib.sh
+
+test_expect_success 'refuses to do anything without subcommand' '
+ test_must_fail git history 2>err &&
+ test_grep foo err
+'
+
+test_done
--
2.51.0.308.g032396e0da.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH RFC v2 03/16] sequencer: introduce new history editing mode
2025-08-24 17:42 ` [PATCH RFC v2 03/16] sequencer: introduce new history editing mode Patrick Steinhardt
@ 2025-08-26 12:55 ` D. Ben Knoble
2025-09-03 12:19 ` Patrick Steinhardt
0 siblings, 1 reply; 278+ messages in thread
From: D. Ben Knoble @ 2025-08-26 12:55 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, Junio C Hamano, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk
On Sun, Aug 24, 2025 at 1:42 PM Patrick Steinhardt <ps@pks.im> wrote:
> diff --git a/sequencer.c b/sequencer.c
> index bff181df76..898ac1a2a8 100644
> --- a/sequencer.c
> +++ b/sequencer.c
> @@ -3052,17 +3087,29 @@ static int read_populate_todo(struct repository *r,
> return error(_("no commits parsed."));
>
> if (!is_rebase_i(opts)) {
> - enum todo_command valid =
> - opts->action == REPLAY_PICK ? TODO_PICK : TODO_REVERT;
> + enum todo_command valid;
> int i;
>
> - for (i = 0; i < todo_list->nr; i++)
> + switch (opts->action) {
> + case REPLAY_PICK:
> + case REPLAY_HISTORY_EDIT:
> + valid = TODO_PICK;
> + break;
> + default:
> + valid = TODO_REVERT;
> + break;
> + }
I think I see this hunk repeated in a few places—maybe some
leftoverbits for a refactor?
--
D. Ben Knoble
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC v2 03/16] sequencer: introduce new history editing mode
2025-08-26 12:55 ` D. Ben Knoble
@ 2025-09-03 12:19 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-03 12:19 UTC (permalink / raw)
To: D. Ben Knoble
Cc: git, Junio C Hamano, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk
On Tue, Aug 26, 2025 at 08:55:13AM -0400, D. Ben Knoble wrote:
> On Sun, Aug 24, 2025 at 1:42 PM Patrick Steinhardt <ps@pks.im> wrote:
> > diff --git a/sequencer.c b/sequencer.c
> > index bff181df76..898ac1a2a8 100644
> > --- a/sequencer.c
> > +++ b/sequencer.c
> > @@ -3052,17 +3087,29 @@ static int read_populate_todo(struct repository *r,
> > return error(_("no commits parsed."));
> >
> > if (!is_rebase_i(opts)) {
> > - enum todo_command valid =
> > - opts->action == REPLAY_PICK ? TODO_PICK : TODO_REVERT;
> > + enum todo_command valid;
> > int i;
> >
> > - for (i = 0; i < todo_list->nr; i++)
> > + switch (opts->action) {
> > + case REPLAY_PICK:
> > + case REPLAY_HISTORY_EDIT:
> > + valid = TODO_PICK;
> > + break;
> > + default:
> > + valid = TODO_REVERT;
> > + break;
> > + }
>
> I think I see this hunk repeated in a few places—maybe some
> leftoverbits for a refactor?
Fair enough. I don't really see a strong reason why we shouldn't fix
this in the same patch though. We can for example do something like the
below patch.
Patrick
-- >8 --
diff --git a/sequencer.c b/sequencer.c
index 898ac1a2a8..9a66e7d128 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -3063,6 +3063,19 @@ static void todo_list_write_total_nr(struct todo_list *todo_list)
}
}
+static enum todo_command action_to_command(enum replay_action action)
+{
+ switch (action) {
+ case REPLAY_PICK:
+ case REPLAY_HISTORY_EDIT:
+ return TODO_PICK;
+ case REPLAY_REVERT:
+ return TODO_REVERT;
+ default:
+ BUG("unsupported action %d", action);
+ }
+}
+
static int read_populate_todo(struct repository *r,
struct todo_list *todo_list,
struct replay_opts *opts)
@@ -3087,19 +3100,9 @@ static int read_populate_todo(struct repository *r,
return error(_("no commits parsed."));
if (!is_rebase_i(opts)) {
- enum todo_command valid;
+ enum todo_command valid = action_to_command(opts->action);
int i;
- switch (opts->action) {
- case REPLAY_PICK:
- case REPLAY_HISTORY_EDIT:
- valid = TODO_PICK;
- break;
- default:
- valid = TODO_REVERT;
- break;
- }
-
for (i = 0; i < todo_list->nr; i++) {
if (valid == todo_list->items[i].command)
continue;
@@ -3408,16 +3411,7 @@ static int walk_revs_populate_todo(struct todo_list *todo_list,
if (prepare_revs(opts))
return -1;
- switch (opts->action) {
- case REPLAY_PICK:
- case REPLAY_HISTORY_EDIT:
- command = TODO_PICK;
- break;
- default:
- command = TODO_REVERT;
- break;
- }
-
+ command = action_to_command(opts->action);
command_string = todo_command_info[command].str;
encoding = get_log_output_encoding();
@@ -5578,15 +5572,7 @@ static int single_pick(struct repository *r,
int check_todo;
struct todo_item item;
- switch (opts->action) {
- case REPLAY_PICK:
- case REPLAY_HISTORY_EDIT:
- item.command = TODO_PICK;
- break;
- default:
- item.command = TODO_REVERT;
- break;
- }
+ item.command = action_to_command(opts->action);
item.commit = cmit;
return do_pick_commit(r, &item, opts, 0, &check_todo);
^ permalink raw reply related [flat|nested] 278+ messages in thread
* [PATCH RFC v2 04/16] sequencer: stop using `the_repository` in `sequencer_remove_state()`
2025-08-24 17:42 ` [PATCH RFC v2 00/16] " Patrick Steinhardt
` (2 preceding siblings ...)
2025-08-24 17:42 ` [PATCH RFC v2 03/16] sequencer: introduce new history editing mode Patrick Steinhardt
@ 2025-08-24 17:42 ` Patrick Steinhardt
2025-08-24 17:42 ` [PATCH RFC v2 05/16] cache-tree: allow writing in-memory index as tree Patrick Steinhardt
` (12 subsequent siblings)
16 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-24 17:42 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
Refactor `sequencer_remove_state()` to stop using `the_repository` in
favor of a passed-in repository.
A lot of the other code in our sequencer infrastructure still uses
`the_repository`, but this bigger refactoring is left for another day.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
builtin/rebase.c | 4 ++--
builtin/revert.c | 2 +-
sequencer.c | 18 +++++++++---------
sequencer.h | 2 +-
4 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/builtin/rebase.c b/builtin/rebase.c
index 3c85768d29..66824ae136 100644
--- a/builtin/rebase.c
+++ b/builtin/rebase.c
@@ -568,7 +568,7 @@ static int finish_rebase(struct rebase_options *opts)
struct replay_opts replay = REPLAY_OPTS_INIT;
replay.action = REPLAY_INTERACTIVE_REBASE;
- ret = sequencer_remove_state(&replay);
+ ret = sequencer_remove_state(the_repository, &replay);
replay_opts_release(&replay);
} else {
strbuf_addstr(&dir, opts->state_dir);
@@ -1405,7 +1405,7 @@ int cmd_rebase(int argc,
struct replay_opts replay = REPLAY_OPTS_INIT;
replay.action = REPLAY_INTERACTIVE_REBASE;
- ret = sequencer_remove_state(&replay);
+ ret = sequencer_remove_state(the_repository, &replay);
replay_opts_release(&replay);
} else {
strbuf_reset(&buf);
diff --git a/builtin/revert.c b/builtin/revert.c
index c3f92b585d..6456cf2171 100644
--- a/builtin/revert.c
+++ b/builtin/revert.c
@@ -263,7 +263,7 @@ static int run_sequencer(int argc, const char **argv, const char *prefix,
free(options);
if (cmd == 'q') {
- int ret = sequencer_remove_state(opts);
+ int ret = sequencer_remove_state(the_repository, opts);
if (!ret)
remove_branch_state(the_repository, 0);
return ret;
diff --git a/sequencer.c b/sequencer.c
index 898ac1a2a8..749e30c2e6 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -425,7 +425,7 @@ void replay_opts_release(struct replay_opts *opts)
free(opts->ctx);
}
-int sequencer_remove_state(struct replay_opts *opts)
+int sequencer_remove_state(struct repository *repo, struct replay_opts *opts)
{
struct strbuf buf = STRBUF_INIT;
int ret = 0;
@@ -437,7 +437,7 @@ int sequencer_remove_state(struct replay_opts *opts)
char *eol = strchr(p, '\n');
if (eol)
*eol = '\0';
- if (refs_delete_ref(get_main_ref_store(the_repository), "(rebase) cleanup", p, NULL, 0) < 0) {
+ if (refs_delete_ref(get_main_ref_store(repo), "(rebase) cleanup", p, NULL, 0) < 0) {
warning(_("could not delete '%s'"), p);
ret = -1;
}
@@ -3048,7 +3048,7 @@ void sequencer_post_commit_cleanup(struct repository *r, int verbose)
if (!have_finished_the_last_pick())
goto out;
- sequencer_remove_state(&opts);
+ sequencer_remove_state(the_repository, &opts);
out:
replay_opts_release(&opts);
}
@@ -3601,7 +3601,7 @@ int sequencer_rollback(struct repository *r, struct replay_opts *opts)
if (reset_merge(&oid))
goto fail;
strbuf_release(&buf);
- return sequencer_remove_state(opts);
+ return sequencer_remove_state(the_repository, opts);
fail:
strbuf_release(&buf);
return -1;
@@ -4903,7 +4903,7 @@ static int checkout_onto(struct repository *r, struct replay_opts *opts,
};
if (reset_head(r, &ropts)) {
apply_autostash(rebase_path_autostash());
- sequencer_remove_state(opts);
+ sequencer_remove_state(the_repository, opts);
return error(_("could not detach HEAD"));
}
@@ -5268,7 +5268,7 @@ static int pick_commits(struct repository *r,
* Sequence of picks finished successfully; cleanup by
* removing the .git/sequencer directory
*/
- return sequencer_remove_state(opts);
+ return sequencer_remove_state(the_repository, opts);
}
static int continue_single_pick(struct repository *r, struct replay_opts *opts)
@@ -6607,7 +6607,7 @@ int complete_action(struct repository *r, struct replay_opts *opts, unsigned fla
if (count_commands(todo_list) == 0) {
apply_autostash(rebase_path_autostash());
- sequencer_remove_state(opts);
+ sequencer_remove_state(the_repository, opts);
return error(_("nothing to do"));
}
@@ -6618,12 +6618,12 @@ int complete_action(struct repository *r, struct replay_opts *opts, unsigned fla
return -1;
else if (res == -2) {
apply_autostash(rebase_path_autostash());
- sequencer_remove_state(opts);
+ sequencer_remove_state(the_repository, opts);
return -1;
} else if (res == -3) {
apply_autostash(rebase_path_autostash());
- sequencer_remove_state(opts);
+ sequencer_remove_state(the_repository, opts);
todo_list_release(&new_todo);
return error(_("nothing to do"));
diff --git a/sequencer.h b/sequencer.h
index 082fbe3e35..0e0e7301b8 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -170,7 +170,7 @@ int sequencer_continue(struct repository *repo, struct replay_opts *opts);
int sequencer_rollback(struct repository *repo, struct replay_opts *opts);
int sequencer_skip(struct repository *repo, struct replay_opts *opts);
void replay_opts_release(struct replay_opts *opts);
-int sequencer_remove_state(struct replay_opts *opts);
+int sequencer_remove_state(struct repository *repo, struct replay_opts *opts);
#define TODO_LIST_KEEP_EMPTY (1U << 0)
#define TODO_LIST_SHORTEN_IDS (1U << 1)
--
2.51.0.308.g032396e0da.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH RFC v2 05/16] cache-tree: allow writing in-memory index as tree
2025-08-24 17:42 ` [PATCH RFC v2 00/16] " Patrick Steinhardt
` (3 preceding siblings ...)
2025-08-24 17:42 ` [PATCH RFC v2 04/16] sequencer: stop using `the_repository` in `sequencer_remove_state()` Patrick Steinhardt
@ 2025-08-24 17:42 ` Patrick Steinhardt
2025-08-25 16:38 ` Junio C Hamano
2025-08-24 17:42 ` [PATCH RFC v2 06/16] builtin: add new "history" command Patrick Steinhardt
` (11 subsequent siblings)
16 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-24 17:42 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
The function `write_in_core_index_as_tree()` takes a repository and
writes its index into a tree object. What this function cannot do though
is to take an _arbitrary_ in-memory index.
Introduce a new `struct index_state` parameter so that the caller can
pass a different index than the one belonging to the repository. This
will be used in a subsequent commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
builtin/checkout.c | 3 ++-
cache-tree.c | 5 ++---
cache-tree.h | 3 ++-
3 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/builtin/checkout.c b/builtin/checkout.c
index f9453473fe2..43583c8d1be 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -902,7 +902,8 @@ static int merge_working_tree(const struct checkout_opts *opts,
0);
init_ui_merge_options(&o, the_repository);
o.verbosity = 0;
- work = write_in_core_index_as_tree(the_repository);
+ work = write_in_core_index_as_tree(the_repository,
+ the_repository->index);
ret = reset_tree(new_tree,
opts, 1,
diff --git a/cache-tree.c b/cache-tree.c
index 66ef2becbe0..029ec933abe 100644
--- a/cache-tree.c
+++ b/cache-tree.c
@@ -699,11 +699,11 @@ static int write_index_as_tree_internal(struct object_id *oid,
return 0;
}
-struct tree* write_in_core_index_as_tree(struct repository *repo) {
+struct tree *write_in_core_index_as_tree(struct repository *repo,
+ struct index_state *index_state) {
struct object_id o;
int was_valid, ret;
- struct index_state *index_state = repo->index;
was_valid = index_state->cache_tree &&
cache_tree_fully_valid(index_state->cache_tree);
@@ -723,7 +723,6 @@ struct tree* write_in_core_index_as_tree(struct repository *repo) {
return lookup_tree(repo, &index_state->cache_tree->oid);
}
-
int write_index_as_tree(struct object_id *oid, struct index_state *index_state, const char *index_path, int flags, const char *prefix)
{
int entries, was_valid;
diff --git a/cache-tree.h b/cache-tree.h
index b82c4963e7c..f8bddae5235 100644
--- a/cache-tree.h
+++ b/cache-tree.h
@@ -47,7 +47,8 @@ int cache_tree_verify(struct repository *, struct index_state *);
#define WRITE_TREE_UNMERGED_INDEX (-2)
#define WRITE_TREE_PREFIX_ERROR (-3)
-struct tree* write_in_core_index_as_tree(struct repository *repo);
+struct tree *write_in_core_index_as_tree(struct repository *repo,
+ struct index_state *index_state);
int write_index_as_tree(struct object_id *oid, struct index_state *index_state, const char *index_path, int flags, const char *prefix);
void prime_cache_tree(struct repository *, struct index_state *, struct tree *);
--
2.51.0.308.g032396e0da.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH RFC v2 05/16] cache-tree: allow writing in-memory index as tree
2025-08-24 17:42 ` [PATCH RFC v2 05/16] cache-tree: allow writing in-memory index as tree Patrick Steinhardt
@ 2025-08-25 16:38 ` Junio C Hamano
2025-09-03 12:19 ` Patrick Steinhardt
0 siblings, 1 reply; 278+ messages in thread
From: Junio C Hamano @ 2025-08-25 16:38 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, D. Ben Knoble, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk
Patrick Steinhardt <ps@pks.im> writes:
> The function `write_in_core_index_as_tree()` takes a repository and
> writes its index into a tree object. What this function cannot do though
> is to take an _arbitrary_ in-memory index.
>
> Introduce a new `struct index_state` parameter so that the caller can
> pass a different index than the one belonging to the repository. This
> will be used in a subsequent commit.
Nice.
I wonder if this would also allow us to simplify the code paths for
"git commit -o <pathspec>", where we use a separate temporary index
that gets populated afresh from HEAD, grab the new snapshot for the
paths that match the pathspec, and write it out as a tree to be
wrapped in the new commit (and then the real index is also updated
at these same paths).
I guess the code paths need to expose what is in the temporary index
to hooks, which means the index file needs to be written out to an
actual on-disk file, so the picture would be a bit different?
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC v2 05/16] cache-tree: allow writing in-memory index as tree
2025-08-25 16:38 ` Junio C Hamano
@ 2025-09-03 12:19 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-03 12:19 UTC (permalink / raw)
To: Junio C Hamano
Cc: git, D. Ben Knoble, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk
On Mon, Aug 25, 2025 at 09:38:07AM -0700, Junio C Hamano wrote:
> Patrick Steinhardt <ps@pks.im> writes:
>
> > The function `write_in_core_index_as_tree()` takes a repository and
> > writes its index into a tree object. What this function cannot do though
> > is to take an _arbitrary_ in-memory index.
> >
> > Introduce a new `struct index_state` parameter so that the caller can
> > pass a different index than the one belonging to the repository. This
> > will be used in a subsequent commit.
>
> Nice.
>
> I wonder if this would also allow us to simplify the code paths for
> "git commit -o <pathspec>", where we use a separate temporary index
> that gets populated afresh from HEAD, grab the new snapshot for the
> paths that match the pathspec, and write it out as a tree to be
> wrapped in the new commit (and then the real index is also updated
> at these same paths).
>
> I guess the code paths need to expose what is in the temporary index
> to hooks, which means the index file needs to be written out to an
> actual on-disk file, so the picture would be a bit different?
Well, the sequencer itself is also writing out the temporary index to
disk. Took me quite a while to figure out why the index I wrote always
turned out to only contain a subset of the changes I wanted. So I don't
really see a reason why we couldn't use the infra for other commands,
but cannot say whether or not it would end up improving the status quo.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH RFC v2 06/16] builtin: add new "history" command
2025-08-24 17:42 ` [PATCH RFC v2 00/16] " Patrick Steinhardt
` (4 preceding siblings ...)
2025-08-24 17:42 ` [PATCH RFC v2 05/16] cache-tree: allow writing in-memory index as tree Patrick Steinhardt
@ 2025-08-24 17:42 ` Patrick Steinhardt
2025-08-24 17:42 ` [PATCH RFC v2 07/16] builtin/history: introduce subcommands to manage interrupted rewrites Patrick Steinhardt
` (10 subsequent siblings)
16 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-24 17:42 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
When rewriting history via git-rebase(1) there are a couple of very
common use cases:
- The ordering of two commits should be reversed.
- A commit should be split up into two commits.
- A commit should be dropped from the history completely.
- Multiple commits should be squashed into one.
While these operations are all doable, it often feels needlessly cludgy
to do so by doing an interactive rebase, using the editor to say what
one wants, and then perform the actions. Furthermore, some operations
like splitting up a commit into two are way more involved than that and
require a whole series of commands.
Add a new "history" command to plug this gap. This command will have
several different subcommands to imperatively rewrite history for common
use cases like the above. These commands will be implemented in
subsequent commits.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
.gitignore | 1 +
Documentation/git-history.adoc | 45 ++++++++++++++++++++++++++++++++++++++++++
Documentation/meson.build | 1 +
Makefile | 1 +
builtin.h | 1 +
builtin/history.c | 20 +++++++++++++++++++
command-list.txt | 1 +
git.c | 1 +
meson.build | 1 +
9 files changed, 72 insertions(+)
diff --git a/.gitignore b/.gitignore
index 04c444404e..3932d4d618 100644
--- a/.gitignore
+++ b/.gitignore
@@ -77,6 +77,7 @@
/git-grep
/git-hash-object
/git-help
+/git-history
/git-hook
/git-http-backend
/git-http-fetch
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
new file mode 100644
index 0000000000..1537960374
--- /dev/null
+++ b/Documentation/git-history.adoc
@@ -0,0 +1,45 @@
+git-history(1)
+==============
+
+NAME
+----
+git-history - EXPERIMENTAL: Rewrite history of the current branch
+
+SYNOPSIS
+--------
+[synopsis]
+git history [<options>]
+
+DESCRIPTION
+-----------
+
+Rewrite history by rearranging or modifying specific commits in the
+history.
+
+This command is similar to linkgit:git-rebase[1] and uses the same
+underlying machinery. You should use rebases if you either want to
+reapply a range of commits onto a different base, or interactive rebases
+if you want to edit a range of commits.
+
+Note that this command does not (yet) work with histories that contain
+merges. You should use linkgit:git-rebase[1] with the `--rebase-merges`
+flag instead.
+
+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
+
+COMMANDS
+--------
+
+This command requires a subcommand. Several subcommands are available to
+rewrite history in different ways:
+
+CONFIGURATION
+-------------
+
+include::includes/cmd-config-section-all.adoc[]
+
+include::config/sequencer.adoc[]
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Documentation/meson.build b/Documentation/meson.build
index 4404c623f0..a30b5307fd 100644
--- a/Documentation/meson.build
+++ b/Documentation/meson.build
@@ -64,6 +64,7 @@ manpages = {
'git-gui.adoc' : 1,
'git-hash-object.adoc' : 1,
'git-help.adoc' : 1,
+ 'git-history.adoc' : 1,
'git-hook.adoc' : 1,
'git-http-backend.adoc' : 1,
'git-http-fetch.adoc' : 1,
diff --git a/Makefile b/Makefile
index e11340c1ae..bed6eda5e6 100644
--- a/Makefile
+++ b/Makefile
@@ -1261,6 +1261,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o
BUILTIN_OBJS += builtin/grep.o
BUILTIN_OBJS += builtin/hash-object.o
BUILTIN_OBJS += builtin/help.o
+BUILTIN_OBJS += builtin/history.o
BUILTIN_OBJS += builtin/hook.o
BUILTIN_OBJS += builtin/index-pack.o
BUILTIN_OBJS += builtin/init-db.o
diff --git a/builtin.h b/builtin.h
index bff13e3069..2934f4479a 100644
--- a/builtin.h
+++ b/builtin.h
@@ -172,6 +172,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix, struc
int cmd_grep(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_hash_object(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_help(int argc, const char **argv, const char *prefix, struct repository *repo);
+int cmd_history(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_hook(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_index_pack(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_init_db(int argc, const char **argv, const char *prefix, struct repository *repo);
diff --git a/builtin/history.c b/builtin/history.c
new file mode 100644
index 0000000000..d1a40368e0
--- /dev/null
+++ b/builtin/history.c
@@ -0,0 +1,20 @@
+#include "builtin.h"
+#include "gettext.h"
+#include "parse-options.h"
+
+int cmd_history(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo UNUSED)
+{
+ const char * const usage[] = {
+ N_("git history [<options>]"),
+ NULL,
+ };
+ struct option options[] = {
+ OPT_END(),
+ };
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ return 0;
+}
diff --git a/command-list.txt b/command-list.txt
index b7ade3ab9f..f95f0ce926 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -115,6 +115,7 @@ git-grep mainporcelain info
git-gui mainporcelain
git-hash-object plumbingmanipulators
git-help ancillaryinterrogators complete
+git-history mainporcelain history
git-hook purehelpers
git-http-backend synchingrepositories
git-http-fetch synchelpers
diff --git a/git.c b/git.c
index 83eac0aeab..9d2cba2906 100644
--- a/git.c
+++ b/git.c
@@ -560,6 +560,7 @@ static struct cmd_struct commands[] = {
{ "grep", cmd_grep, RUN_SETUP_GENTLY },
{ "hash-object", cmd_hash_object },
{ "help", cmd_help },
+ { "history", cmd_history, RUN_SETUP },
{ "hook", cmd_hook, RUN_SETUP },
{ "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },
{ "init", cmd_init_db },
diff --git a/meson.build b/meson.build
index 5dd299b496..0e40778a23 100644
--- a/meson.build
+++ b/meson.build
@@ -603,6 +603,7 @@ builtin_sources = [
'builtin/grep.c',
'builtin/hash-object.c',
'builtin/help.c',
+ 'builtin/history.c',
'builtin/hook.c',
'builtin/index-pack.c',
'builtin/init-db.c',
--
2.51.0.308.g032396e0da.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH RFC v2 07/16] builtin/history: introduce subcommands to manage interrupted rewrites
2025-08-24 17:42 ` [PATCH RFC v2 00/16] " Patrick Steinhardt
` (5 preceding siblings ...)
2025-08-24 17:42 ` [PATCH RFC v2 06/16] builtin: add new "history" command Patrick Steinhardt
@ 2025-08-24 17:42 ` Patrick Steinhardt
2025-08-24 17:42 ` [PATCH RFC v2 08/16] builtin/history: implement "drop" subcommand Patrick Steinhardt
` (9 subsequent siblings)
16 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-24 17:42 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
Introduce subcommands to manage the sequencer state for git-history(1).
These aren't really useful yet, but will become useful in subsequent
commits where we will introduce git-history(1) subcommands that actually
edit history.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-history.adoc | 20 +++++++-
builtin/history.c | 114 +++++++++++++++++++++++++++++++++++++++--
t/meson.build | 3 +-
t/t3450-history.sh | 32 +++++++++++-
4 files changed, 163 insertions(+), 6 deletions(-)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index 1537960374..3e9a789b83 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -8,7 +8,9 @@ git-history - EXPERIMENTAL: Rewrite history of the current branch
SYNOPSIS
--------
[synopsis]
-git history [<options>]
+git history abort
+git history continue
+git history quit
DESCRIPTION
-----------
@@ -33,6 +35,22 @@ COMMANDS
This command requires a subcommand. Several subcommands are available to
rewrite history in different ways:
+The following commands are used to manage an interrupted history-rewriting
+operation:
+
+`abort`::
+ Abort the history-rewriting operation and reset HEAD to the original
+ branch.
+
+`continue`::
+ Restart the history-rewriting process after having resolved a merge
+ conflict.
+
+`quit`::
+ Abort the history-rewriting operation but `HEAD` is not reset back to
+ the original branch. The index and working tree are also left unchanged
+ as a result.
+
CONFIGURATION
-------------
diff --git a/builtin/history.c b/builtin/history.c
index d1a40368e0..0ad45dbfef 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -1,20 +1,128 @@
#include "builtin.h"
+#include "branch.h"
#include "gettext.h"
#include "parse-options.h"
+#include "sequencer.h"
+
+static int cmd_history_abort(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo)
+{
+ const char * const usage[] = {
+ N_("git history abort"),
+ NULL,
+ };
+ struct option options[] = {
+ OPT_END(),
+ };
+ struct replay_opts opts = REPLAY_OPTS_INIT;
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc) {
+ ret = error(_("command does not take arguments"));
+ goto out;
+ }
+
+ opts.action = REPLAY_HISTORY_EDIT;
+ ret = sequencer_rollback(repo, &opts);
+ if (ret)
+ goto out;
+
+ ret = 0;
+
+out:
+ replay_opts_release(&opts);
+ return ret;
+}
+
+static int cmd_history_continue(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo)
+{
+ const char * const usage[] = {
+ N_("git history continue"),
+ NULL,
+ };
+ struct option options[] = {
+ OPT_END(),
+ };
+ struct replay_opts opts = REPLAY_OPTS_INIT;
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc) {
+ ret = error(_("command does not take arguments"));
+ goto out;
+ }
+
+ opts.action = REPLAY_HISTORY_EDIT;
+ ret = sequencer_continue(repo, &opts);
+ if (ret)
+ goto out;
+
+ ret = 0;
+
+out:
+ replay_opts_release(&opts);
+ return ret;
+}
+
+static int cmd_history_quit(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo)
+{
+ const char * const usage[] = {
+ N_("git history quit"),
+ NULL,
+ };
+ struct option options[] = {
+ OPT_END(),
+ };
+ struct replay_opts opts = REPLAY_OPTS_INIT;
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc) {
+ ret = error(_("command does not take arguments"));
+ goto out;
+ }
+
+ opts.action = REPLAY_HISTORY_EDIT;
+ ret = sequencer_remove_state(repo, &opts);
+ if (ret)
+ goto out;
+ remove_branch_state(repo, 0);
+
+ ret = 0;
+
+out:
+ replay_opts_release(&opts);
+ return ret;
+}
int cmd_history(int argc,
const char **argv,
const char *prefix,
- struct repository *repo UNUSED)
+ struct repository *repo)
{
const char * const usage[] = {
- N_("git history [<options>]"),
+ N_("git history abort"),
+ N_("git history continue"),
+ N_("git history quit"),
NULL,
};
+ parse_opt_subcommand_fn *fn = NULL;
struct option options[] = {
+ OPT_SUBCOMMAND("abort", &fn, cmd_history_abort),
+ OPT_SUBCOMMAND("continue", &fn, cmd_history_continue),
+ OPT_SUBCOMMAND("quit", &fn, cmd_history_quit),
OPT_END(),
};
argc = parse_options(argc, argv, prefix, options, usage, 0);
- return 0;
+ return fn(argc, argv, prefix, repo);
}
diff --git a/t/meson.build b/t/meson.build
index bbeba1a8d5..966d7c14f4 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -376,6 +376,7 @@ integration_tests = [
't3436-rebase-more-options.sh',
't3437-rebase-fixup-options.sh',
't3438-rebase-broken-files.sh',
+ 't3450-history.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
@@ -1214,4 +1215,4 @@ if perl.found() and time.found()
timeout: 0,
)
endforeach
-endif
\ No newline at end of file
+endif
diff --git a/t/t3450-history.sh b/t/t3450-history.sh
index 9eb1ed6749..aa9d44c03b 100755
--- a/t/t3450-history.sh
+++ b/t/t3450-history.sh
@@ -6,7 +6,37 @@ test_description='tests for git-history command'
test_expect_success 'refuses to do anything without subcommand' '
test_must_fail git history 2>err &&
- test_grep foo err
+ test_grep "need a subcommand" err
+'
+
+test_expect_success 'abort complains about arguments' '
+ test_must_fail git history abort foo 2>err &&
+ test_grep "command does not take arguments" err
+'
+
+test_expect_success 'abort complains when no history edit is active' '
+ test_must_fail git history abort 2>err &&
+ test_grep "no history edit in progress" err
+'
+
+test_expect_success 'continue complains about arguments' '
+ test_must_fail git history continue foo 2>err &&
+ test_grep "command does not take arguments" err
+'
+
+test_expect_success 'continue complains when no history edit is active' '
+ test_must_fail git history continue 2>err &&
+ test_grep "no history edit in progress" err
+'
+
+test_expect_success 'quit complains about arguments' '
+ test_must_fail git history quit foo 2>err &&
+ test_grep "command does not take arguments" err
+'
+
+test_expect_success 'quit does not complain when no history edit is active' '
+ git history quit 2>err &&
+ test_must_be_empty err
'
test_done
--
2.51.0.308.g032396e0da.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH RFC v2 08/16] builtin/history: implement "drop" subcommand
2025-08-24 17:42 ` [PATCH RFC v2 00/16] " Patrick Steinhardt
` (6 preceding siblings ...)
2025-08-24 17:42 ` [PATCH RFC v2 07/16] builtin/history: introduce subcommands to manage interrupted rewrites Patrick Steinhardt
@ 2025-08-24 17:42 ` Patrick Steinhardt
2025-08-24 17:42 ` [PATCH RFC v2 09/16] builtin/history: implement "reorder" subcommand Patrick Steinhardt
` (8 subsequent siblings)
16 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-24 17:42 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
It is a fairly common operation to perform an interactive rebase so that
one of the commits can be dropped from history. Doing this is not very
hard in general, but still requires the user to perform multiple steps:
1. Identify the commit in question that is to be dropped.
2. Perform an interactive rebase on top of that commit's parent.
3. Edit the instruction sheet to drop that commit.
This is needlessly complex for such a supposedly-trivial operation.
Furthermore, the second step doesn't account for certain edge cases like
for example dropping the root commit.
Introduce a new "drop" subcommand to make this use case significantly
simpler: all the user needs to do is to say `git history drop $COMMIT`
and they're done.
Note that for now, this command only allows users to drop a single
commit at once. It should be easy enough though to expand the command at
a later point in time to support dropping whole commit ranges.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-history.adoc | 27 ++++
builtin/history.c | 287 +++++++++++++++++++++++++++++++++++++++++
t/meson.build | 1 +
t/t3451-history-drop.sh | 174 +++++++++++++++++++++++++
4 files changed, 489 insertions(+)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index 3e9a789b83..db5b292994 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -11,6 +11,7 @@ SYNOPSIS
git history abort
git history continue
git history quit
+git history drop <commit>
DESCRIPTION
-----------
@@ -35,6 +36,15 @@ COMMANDS
This command requires a subcommand. Several subcommands are available to
rewrite history in different ways:
+`drop <commit>`::
+ Drop a commit from the history and reapply all children of that
+ commit on top of the commit's parent. The commit that is to be
+ dropped must be reachable from the current `HEAD` commit.
++
+Dropping the root commit converts the child of that commit into the new
+root commit. It is invalid to drop a root commit that does not have any
+child commits, as that would lead to an empty branch.
+
The following commands are used to manage an interrupted history-rewriting
operation:
@@ -51,6 +61,23 @@ operation:
the original branch. The index and working tree are also left unchanged
as a result.
+EXAMPLES
+--------
+
+Drop a commit from history
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+----------
+$ git log --oneline
+2d4cd6d third
+125a0f3 second
+e098c27 first
+$ git history drop HEAD~
+$ git log
+b1bc1bd third
+e098c27 first
+----------
+
CONFIGURATION
-------------
diff --git a/builtin/history.c b/builtin/history.c
index 0ad45dbfef..2132b6a441 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -1,7 +1,16 @@
#include "builtin.h"
#include "branch.h"
+#include "commit.h"
+#include "commit-reach.h"
+#include "config.h"
+#include "environment.h"
#include "gettext.h"
+#include "hex.h"
+#include "object-name.h"
#include "parse-options.h"
+#include "refs.h"
+#include "reset.h"
+#include "revision.h"
#include "sequencer.h"
static int cmd_history_abort(int argc,
@@ -104,6 +113,282 @@ static int cmd_history_quit(int argc,
return ret;
}
+static int collect_commits(struct repository *repo,
+ struct commit *old_commit,
+ struct commit *new_commit,
+ struct strvec *out)
+{
+ struct setup_revision_opt revision_opts = {
+ .assume_dashdash = 1,
+ };
+ struct strvec revisions = STRVEC_INIT;
+ struct commit_list *from_list = NULL;
+ struct commit *child;
+ struct rev_info rev = { 0 };
+ int ret;
+
+ /*
+ * Check that the old commit actually is an ancestor of HEAD. If not
+ * the whole request becomes nonsensical.
+ */
+ if (old_commit) {
+ commit_list_insert(old_commit, &from_list);
+ if (!repo_is_descendant_of(repo, new_commit, from_list)) {
+ ret = error(_("commit must be reachable from current HEAD commit"));
+ goto out;
+ }
+ }
+
+ repo_init_revisions(repo, &rev, NULL);
+ strvec_push(&revisions, "");
+ strvec_push(&revisions, oid_to_hex(&new_commit->object.oid));
+ if (old_commit)
+ strvec_pushf(&revisions, "^%s", oid_to_hex(&old_commit->object.oid));
+ if (setup_revisions(revisions.nr, revisions.v, &rev, &revision_opts) != 1 ||
+ prepare_revision_walk(&rev)) {
+ ret = error(_("revision walk setup failed"));
+ goto out;
+ }
+
+ while ((child = get_revision(&rev))) {
+ if (old_commit && !child->parents)
+ BUG("revision walk did not find child commit");
+ if (child->parents && child->parents->next) {
+ ret = error(_("cannot rearrange commit history with merges"));
+ goto out;
+ }
+
+ strvec_push(out, oid_to_hex(&child->object.oid));
+
+ if (child->parents && old_commit &&
+ commit_list_contains(old_commit, child->parents))
+ break;
+ }
+
+ /*
+ * Revisions are in newest-order-first. We have to reverse the
+ * array though so that we pick the oldest commits first.
+ */
+ for (size_t i = 0, j = out->nr - 1; i < j; i++, j--)
+ SWAP(out->v[i], out->v[j]);
+
+ ret = 0;
+
+out:
+ free_commit_list(from_list);
+ strvec_clear(&revisions);
+ release_revisions(&rev);
+ reset_revision_walk();
+ return ret;
+}
+
+static int apply_commits(struct repository *repo,
+ const struct strvec *commits,
+ struct commit *head,
+ struct commit *base,
+ const char *action)
+{
+ struct setup_revision_opt revision_opts = {
+ .assume_dashdash = 1,
+ };
+ struct replay_opts replay_opts = REPLAY_OPTS_INIT;
+ struct reset_head_opts reset_opts = { 0 };
+ struct object_id root_commit;
+ struct strvec args = STRVEC_INIT;
+ struct strbuf buf = STRBUF_INIT;
+ char hex[GIT_MAX_HEXSZ + 1];
+ int ref_flags, ret;
+
+ /*
+ * We have performed all safety checks, so we now prepare
+ * replaying the commits.
+ */
+ replay_opts.action = REPLAY_HISTORY_EDIT;
+ sequencer_init_config(&replay_opts);
+ replay_opts.quiet = 1;
+ replay_opts.skip_commit_summary = 1;
+ if (!replay_opts.strategy && replay_opts.default_strategy) {
+ replay_opts.strategy = replay_opts.default_strategy;
+ replay_opts.default_strategy = NULL;
+ }
+
+ strvec_push(&args, "");
+ strvec_pushv(&args, commits->v);
+
+ replay_opts.revs = xmalloc(sizeof(*replay_opts.revs));
+ repo_init_revisions(repo, replay_opts.revs, NULL);
+ replay_opts.revs->no_walk = 1;
+ replay_opts.revs->unsorted_input = 1;
+ if (setup_revisions(args.nr, args.v, replay_opts.revs,
+ &revision_opts) != 1) {
+ ret = error(_("setting up revisions failed"));
+ goto out;
+ }
+
+ /*
+ * If we're dropping the root commit we first need to create
+ * a new empty root. We then instruct the seqencer machinery to
+ * squash that root commit with the first commit we're picking
+ * onto it.
+ */
+ if (!base->parents) {
+ if (commit_tree("", 0, repo->hash_algo->empty_tree, NULL,
+ &root_commit, NULL, NULL) < 0) {
+ ret = error(_("Could not create new root commit"));
+ goto out;
+ }
+
+ replay_opts.squash_onto = root_commit;
+ replay_opts.have_squash_onto = 1;
+ reset_opts.oid = &root_commit;
+ } else {
+ reset_opts.oid = &base->parents->item->object.oid;
+ }
+
+ replay_opts.restore_head_target =
+ xstrdup_or_null(refs_resolve_ref_unsafe(get_main_ref_store(repo),
+ "HEAD", 0, NULL, &ref_flags));
+ if (!(ref_flags & REF_ISSYMREF))
+ FREE_AND_NULL(replay_opts.restore_head_target);
+
+ /*
+ * Perform a hard-reset to the parent of our commit that is to
+ * be dropped. This is the new base onto which we'll pick all
+ * the descendants.
+ */
+ strbuf_addf(&buf, "%s (start): checkout %s", action,
+ oid_to_hex_r(hex, reset_opts.oid));
+ reset_opts.orig_head = &head->object.oid;
+ reset_opts.flags = RESET_HEAD_DETACH | RESET_ORIG_HEAD;
+ reset_opts.head_msg = buf.buf;
+ reset_opts.default_reflog_action = action;
+ if (reset_head(repo, &reset_opts) < 0) {
+ ret = error(_("could not switch to %s"), oid_to_hex(reset_opts.oid));
+ goto out;
+ }
+
+ ret = sequencer_pick_revisions(repo, &replay_opts);
+ if (ret < 0) {
+ ret = error(_("could not pick commits"));
+ goto out;
+ } else if (ret > 0) {
+ /*
+ * A positive return value indicates we've got a merge
+ * conflict. Bail out, but don't print a message as
+ * `sequencer_pick_revisions()` already printed enough
+ * information.
+ */
+ ret = -1;
+ goto out;
+ }
+
+ ret = 0;
+
+out:
+ replay_opts_release(&replay_opts);
+ strbuf_release(&buf);
+ strvec_clear(&args);
+ return ret;
+}
+
+static int cmd_history_drop(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo)
+{
+ const char * const usage[] = {
+ N_("git history drop <commit>"),
+ NULL,
+ };
+ struct option options[] = {
+ OPT_END(),
+ };
+ struct commit *commit_to_drop, *head;
+ struct strvec commits = STRVEC_INIT;
+ struct strbuf buf = STRBUF_INIT;
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc != 1) {
+ ret = error(_("command expects a single revision"));
+ goto out;
+ }
+ repo_config(repo, git_default_config, NULL);
+
+ commit_to_drop = lookup_commit_reference_by_name(argv[0]);
+ if (!commit_to_drop) {
+ ret = error(_("commit to be dropped cannot be found: %s"), argv[0]);
+ goto out;
+ }
+ if (commit_to_drop->parents && commit_to_drop->parents->next) {
+ ret = error(_("commit to be dropped must not be a merge commit"));
+ goto out;
+ }
+
+ head = lookup_commit_reference_by_name("HEAD");
+ if (!head) {
+ ret = error(_("could not resolve HEAD to a commit"));
+ goto out;
+ }
+
+ if (oideq(&commit_to_drop->object.oid, &head->object.oid)) {
+ /*
+ * If we want to drop the tip of the current branch we don't
+ * have to perform any rebase at all. Instead, we simply
+ * perform a hard reset to the parent commit.
+ */
+ struct reset_head_opts reset_opts = {
+ .orig_head = &head->object.oid,
+ .flags = RESET_ORIG_HEAD,
+ .default_reflog_action = "drop",
+ };
+ char hex[GIT_MAX_HEXSZ + 1];
+
+ if (!commit_to_drop->parents) {
+ ret = error(_("cannot drop the only commit on this branch"));
+ goto out;
+ }
+
+ oid_to_hex_r(hex, &commit_to_drop->parents->item->object.oid);
+ strbuf_addf(&buf, "drop (start): checkout %s", hex);
+ reset_opts.oid = &commit_to_drop->parents->item->object.oid;
+ reset_opts.head_msg = buf.buf;
+
+ if (reset_head(repo, &reset_opts) < 0) {
+ ret = error(_("could not switch to %s"), hex);
+ goto out;
+ }
+ } else {
+ /*
+ * Prepare a revision walk from old commit to the commit that is
+ * about to be dropped. This serves three purposes:
+ *
+ * - We verify that the history doesn't contain any merges.
+ * For now, merges aren't yet handled by us.
+ *
+ * - We need to find the child of the commit-to-be-dropped.
+ * This child is what will be adopted by the parent of the
+ * commit that we are about to drop.
+ *
+ * - We compute the list of commits-to-be-picked.
+ */
+ ret = collect_commits(repo, commit_to_drop, head, &commits);
+ if (ret < 0)
+ goto out;
+
+ ret = apply_commits(repo, &commits, head, commit_to_drop, "drop");
+ if (ret < 0)
+ goto out;
+ }
+
+ ret = 0;
+
+out:
+ strvec_clear(&commits);
+ strbuf_release(&buf);
+ return ret;
+}
+
int cmd_history(int argc,
const char **argv,
const char *prefix,
@@ -113,6 +398,7 @@ int cmd_history(int argc,
N_("git history abort"),
N_("git history continue"),
N_("git history quit"),
+ N_("git history drop <commit>"),
NULL,
};
parse_opt_subcommand_fn *fn = NULL;
@@ -120,6 +406,7 @@ int cmd_history(int argc,
OPT_SUBCOMMAND("abort", &fn, cmd_history_abort),
OPT_SUBCOMMAND("continue", &fn, cmd_history_continue),
OPT_SUBCOMMAND("quit", &fn, cmd_history_quit),
+ OPT_SUBCOMMAND("drop", &fn, cmd_history_drop),
OPT_END(),
};
diff --git a/t/meson.build b/t/meson.build
index 966d7c14f4..8189c6c561 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -377,6 +377,7 @@ integration_tests = [
't3437-rebase-fixup-options.sh',
't3438-rebase-broken-files.sh',
't3450-history.sh',
+ 't3451-history-drop.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
diff --git a/t/t3451-history-drop.sh b/t/t3451-history-drop.sh
new file mode 100755
index 0000000000..325d353d73
--- /dev/null
+++ b/t/t3451-history-drop.sh
@@ -0,0 +1,174 @@
+#!/bin/sh
+
+test_description='tests for git-history drop subcommand'
+
+. ./test-lib.sh
+
+test_expect_success 'refuses to work with merge commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ git branch branch &&
+ test_commit ours &&
+ git switch branch &&
+ test_commit theirs &&
+ git switch - &&
+ git merge theirs &&
+ test_must_fail git history drop HEAD 2>err &&
+ test_grep "commit to be dropped must not be a merge commit" err &&
+ test_must_fail git history drop HEAD~ 2>err &&
+ test_grep "cannot rearrange commit history with merges" err
+ )
+'
+
+test_expect_success 'refuses to work when history becomes empty' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ test_must_fail git history drop HEAD 2>err &&
+ test_grep "cannot drop the only commit on this branch" err
+ )
+'
+
+test_expect_success 'refuses to work with changes in the worktree or index' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit initial &&
+ test_commit file file &&
+ echo foo >file &&
+ test_must_fail git history drop HEAD 2>err &&
+ test_grep "Your local changes to the following files would be overwritten" err &&
+ git add file &&
+ test_must_fail git history drop HEAD 2>err &&
+ test_grep "Your local changes to the following files would be overwritten" err
+ )
+'
+
+test_expect_success 'can drop tip of a branch' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ git symbolic-ref HEAD >expect &&
+ git history drop HEAD &&
+ git symbolic-ref HEAD >actual &&
+ test_cmp expect actual &&
+
+ cat >expect <<-EOF &&
+ second
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'can drop commit in the middle' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ test_commit fourth &&
+ test_commit fifth &&
+
+ git symbolic-ref HEAD >expect &&
+ git history drop HEAD~2 &&
+ git symbolic-ref HEAD >actual &&
+ test_cmp expect actual &&
+
+ cat >expect <<-EOF &&
+ fifth
+ fourth
+ second
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'correct order is retained' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ test_commit fourth &&
+ test_commit fifth &&
+ git history drop HEAD~3 &&
+ cat >expect <<-EOF &&
+ fifth
+ fourth
+ third
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'can drop root commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ git history drop HEAD~2 &&
+ cat >expect <<-EOF &&
+ third
+ second
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'conflicts are detected' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ echo original >file &&
+ git add . &&
+ git commit -m original &&
+ echo modified >file &&
+ git commit -am modified &&
+
+ test_must_fail git history drop HEAD~ >err 2>&1 &&
+ test_grep CONFLICT err &&
+ test_grep "git history continue" err &&
+ echo resolved >file &&
+ git add file &&
+ git history continue &&
+
+ cat >expect <<-EOF &&
+ modified
+ base
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual &&
+ echo resolved >expect &&
+ git cat-file -p HEAD:file >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_done
--
2.51.0.308.g032396e0da.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH RFC v2 09/16] builtin/history: implement "reorder" subcommand
2025-08-24 17:42 ` [PATCH RFC v2 00/16] " Patrick Steinhardt
` (7 preceding siblings ...)
2025-08-24 17:42 ` [PATCH RFC v2 08/16] builtin/history: implement "drop" subcommand Patrick Steinhardt
@ 2025-08-24 17:42 ` Patrick Steinhardt
2025-08-26 13:03 ` D. Ben Knoble
2025-08-24 17:42 ` [PATCH RFC v2 10/16] add-patch: split out header from "add-interactive.h" Patrick Steinhardt
` (7 subsequent siblings)
16 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-24 17:42 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
When working in projects where having nice commits matters it's quite
common that developers end up reordering commits a lot. This is
typically done via interactive rebases, where they can then rearrange
commits in the instruction sheet.
Still, this operation is a frequent-enough operation to provide a more
direct way of doing this imperatively. As such, introduce a new
"reorder" subcommand where users can reorder a commit A to come after or
before another commit B:
$ git log --oneline
a978f73 fifth
57594ee fourth
04eb1c4 third
d535e30 second
bf7438d first
$ git history reorder :/fourth --before=:/second
$ git log --oneline
1610fe0 fifth
444f97d third
2f90797 second
b0ae659 fourth
bf7438d first
$ git history reorder :/fourth --after=:/second
$ git log --oneline
c48729d fifth
f44a46e third
26693b8 fourth
8cb4171 second
bf7438d first
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-history.adoc | 35 +++++-
builtin/history.c | 130 +++++++++++++++++++++++
t/meson.build | 1 +
t/t3452-history-reorder.sh | 234 +++++++++++++++++++++++++++++++++++++++++
4 files changed, 399 insertions(+), 1 deletion(-)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index db5b292994..b36cd925dd 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -12,6 +12,7 @@ git history abort
git history continue
git history quit
git history drop <commit>
+git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)
DESCRIPTION
-----------
@@ -39,12 +40,18 @@ rewrite history in different ways:
`drop <commit>`::
Drop a commit from the history and reapply all children of that
commit on top of the commit's parent. The commit that is to be
- dropped must be reachable from the current `HEAD` commit.
+ dropped must be reachable from the currently checked-out commit.
+
Dropping the root commit converts the child of that commit into the new
root commit. It is invalid to drop a root commit that does not have any
child commits, as that would lead to an empty branch.
+`reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)`::
+ Move the commit so that it becomes either the parent of
+ <following-commit> or the child of <preceding-commit>. The commits must
+ be related to one another and must be reachable from the current `HEAD`
+ commit.
+
The following commands are used to manage an interrupted history-rewriting
operation:
@@ -78,6 +85,32 @@ b1bc1bd third
e098c27 first
----------
+Reorder a commit
+~~~~~~~~~~~~~~~~
+
+----------
+$ git log --oneline
+a978f73 fifth
+57594ee fourth
+04eb1c4 third
+d535e30 second
+bf7438d first
+$ git history reorder :/fourth --before=:/second
+$ git log --oneline
+1610fe0 fifth
+444f97d third
+2f90797 second
+b0ae659 fourth
+bf7438d first
+$ git history reorder :/fourth --after=:/second
+$ git log --oneline
+c48729d fifth
+f44a46e third
+26693b8 fourth
+8cb4171 second
+bf7438d first
+----------
+
CONFIGURATION
-------------
diff --git a/builtin/history.c b/builtin/history.c
index 2132b6a441..16b516856e 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -182,6 +182,33 @@ static int collect_commits(struct repository *repo,
return ret;
}
+static void replace_commits(struct strvec *commits,
+ const struct object_id *commit_to_replace,
+ const struct object_id *replacements,
+ size_t replacements_nr)
+{
+ char commit_to_replace_oid[GIT_MAX_HEXSZ + 1];
+ struct strvec replacement_oids = STRVEC_INIT;
+ bool found = false;
+ size_t i;
+
+ oid_to_hex_r(commit_to_replace_oid, commit_to_replace);
+ for (i = 0; i < replacements_nr; i++)
+ strvec_push(&replacement_oids, oid_to_hex(&replacements[i]));
+
+ for (i = 0; i < commits->nr; i++) {
+ if (strcmp(commits->v[i], commit_to_replace_oid))
+ continue;
+ strvec_splice(commits, i, 1, replacement_oids.v, replacement_oids.nr);
+ found = true;
+ break;
+ }
+ if (!found)
+ BUG("could not find commit to replace");
+
+ strvec_clear(&replacement_oids);
+}
+
static int apply_commits(struct repository *repo,
const struct strvec *commits,
struct commit *head,
@@ -389,6 +416,107 @@ static int cmd_history_drop(int argc,
return ret;
}
+static int cmd_history_reorder(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo)
+{
+ const char * const usage[] = {
+ N_("git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)"),
+ NULL,
+ };
+ const char *before = NULL, *after = NULL;
+ struct option options[] = {
+ OPT_STRING(0, "before", &before, N_("commit"), N_("reorder before this commit")),
+ OPT_STRING(0, "after", &after, N_("commit"), N_("reorder after this commit")),
+ OPT_END(),
+ };
+ struct commit *commit_to_reorder, *head, *anchor, *old;
+ struct strvec commits = STRVEC_INIT;
+ struct object_id replacement[2];
+ struct commit_list *list = NULL;
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc != 1)
+ die(_("command expects a single revision"));
+ if (!before && !after)
+ die(_("exactly one option of 'before' or 'after' must be given"));
+ die_for_incompatible_opt2(!!before, "before", !!after, "after");
+
+ repo_config(repo, git_default_config, NULL);
+
+ commit_to_reorder = lookup_commit_reference_by_name(argv[0]);
+ if (!commit_to_reorder)
+ die(_("commit to be reordered cannot be found: %s"), argv[0]);
+ if (commit_to_reorder->parents && commit_to_reorder->parents->next)
+ die(_("commit to be reordered must not be a merge commit"));
+
+ anchor = lookup_commit_reference_by_name(before ? before : after);
+ if (!commit_to_reorder)
+ die(_("anchor commit cannot be found: %s"), before ? before : after);
+
+ if (oideq(&commit_to_reorder->object.oid, &anchor->object.oid))
+ die(_("commit to reorder and anchor must not be the same"));
+
+ head = lookup_commit_reference_by_name("HEAD");
+ if (!head)
+ die(_("could not resolve HEAD to a commit"));
+
+ commit_list_append(commit_to_reorder, &list);
+ if (!repo_is_descendant_of(repo, commit_to_reorder, list))
+ die(_("reordered commit must be reachable from current HEAD commit"));
+
+ /*
+ * There is no requirement for the user to have either one of the
+ * provided commits be the parent or child. We thus have to figure out
+ * ourselves which one is which.
+ */
+ if (repo_is_descendant_of(repo, anchor, list))
+ old = commit_to_reorder;
+ else
+ old = anchor;
+
+ /*
+ * Select the whole range of commits, including the boundary commit
+ * itself. In case the old commit is the root commit we simply pass no
+ * boundary.
+ */
+ ret = collect_commits(repo, old->parents ? old->parents->item : NULL,
+ head, &commits);
+ if (ret < 0)
+ goto out;
+
+ /*
+ * Perform the reordering of commits in the strvec. This is done by:
+ *
+ * - Deleting the to-be-reordered commit from the range of commits.
+ *
+ * - Replacing the anchor commit with the anchor commit plus the
+ * to-be-reordered commit.
+ */
+ if (before) {
+ replacement[0] = commit_to_reorder->object.oid;
+ replacement[1] = anchor->object.oid;
+ } else {
+ replacement[0] = anchor->object.oid;
+ replacement[1] = commit_to_reorder->object.oid;
+ }
+ replace_commits(&commits, &commit_to_reorder->object.oid, NULL, 0);
+ replace_commits(&commits, &anchor->object.oid, replacement, ARRAY_SIZE(replacement));
+
+ ret = apply_commits(repo, &commits, head, old, "reorder");
+ if (ret < 0)
+ goto out;
+
+ ret = 0;
+
+out:
+ free_commit_list(list);
+ strvec_clear(&commits);
+ return ret;
+}
+
int cmd_history(int argc,
const char **argv,
const char *prefix,
@@ -399,6 +527,7 @@ int cmd_history(int argc,
N_("git history continue"),
N_("git history quit"),
N_("git history drop <commit>"),
+ N_("git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)"),
NULL,
};
parse_opt_subcommand_fn *fn = NULL;
@@ -407,6 +536,7 @@ int cmd_history(int argc,
OPT_SUBCOMMAND("continue", &fn, cmd_history_continue),
OPT_SUBCOMMAND("quit", &fn, cmd_history_quit),
OPT_SUBCOMMAND("drop", &fn, cmd_history_drop),
+ OPT_SUBCOMMAND("reorder", &fn, cmd_history_reorder),
OPT_END(),
};
diff --git a/t/meson.build b/t/meson.build
index 8189c6c561..2bf7bcab5a 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -378,6 +378,7 @@ integration_tests = [
't3438-rebase-broken-files.sh',
't3450-history.sh',
't3451-history-drop.sh',
+ 't3452-history-reorder.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
diff --git a/t/t3452-history-reorder.sh b/t/t3452-history-reorder.sh
new file mode 100755
index 0000000000..49a0784c29
--- /dev/null
+++ b/t/t3452-history-reorder.sh
@@ -0,0 +1,234 @@
+#!/bin/sh
+
+test_description='tests for git-history reorder subcommand'
+
+. ./test-lib.sh
+
+test_expect_success 'refuses to work with merge commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ git branch branch &&
+ test_commit ours &&
+ git switch branch &&
+ test_commit theirs &&
+ git switch - &&
+ git merge theirs &&
+ test_must_fail git history reorder HEAD --before=HEAD~ 2>err &&
+ test_grep "commit to be reordered must not be a merge commit" err &&
+ test_must_fail git history reorder HEAD~ --after=HEAD 2>err &&
+ test_grep "cannot rearrange commit history with merges" err
+ )
+'
+
+test_expect_success 'refuses to work with changes in the worktree or index' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit initial &&
+ test_commit file file &&
+ echo foo >file &&
+ test_must_fail git history reorder HEAD --before=HEAD~ 2>err &&
+ test_grep "Your local changes to the following files would be overwritten" err &&
+ git add file &&
+ test_must_fail git history reorder HEAD --before=HEAD~ 2>err &&
+ test_grep "Your local changes to the following files would be overwritten" err
+ )
+'
+
+test_expect_success 'requires exactly one of --before or --after' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_must_fail git history reorder HEAD 2>err &&
+ test_grep "exactly one option of ${SQ}before${SQ} or ${SQ}after${SQ} must be given" err &&
+ test_must_fail git history reorder HEAD --before=a --after=b 2>err &&
+ test_grep "options ${SQ}before${SQ} and ${SQ}after${SQ} cannot be used together" err
+ )
+'
+
+test_expect_success 'refuses to reorder commit with itself' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_must_fail git history reorder HEAD --after=HEAD 2>err &&
+ test_grep "commit to reorder and anchor must not be the same" err
+ )
+'
+
+test_expect_success '--before can move commit back in history' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ test_commit fourth &&
+ test_commit fifth &&
+ git history reorder :/fourth --before=:/second &&
+ cat >expect <<-EOF &&
+ fifth
+ third
+ second
+ fourth
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success '--before can move commit forward in history' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ test_commit fourth &&
+ test_commit fifth &&
+ git history reorder :/second --before=:/fourth &&
+ cat >expect <<-EOF &&
+ fifth
+ fourth
+ second
+ third
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success '--before can make a commit a root commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ git history reorder :/third --before=:/first &&
+ cat >expect <<-EOF &&
+ second
+ first
+ third
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success '--after can move commit back in history' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ test_commit fourth &&
+ test_commit fifth &&
+ git history reorder :/fourth --after=:/second &&
+ cat >expect <<-EOF &&
+ fifth
+ third
+ fourth
+ second
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success '--after can move commit forward in history' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ test_commit fourth &&
+ test_commit fifth &&
+ git history reorder :/second --after=:/fourth &&
+ cat >expect <<-EOF &&
+ fifth
+ second
+ fourth
+ third
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success '--after can make commit the tip' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ git history reorder :/first --after=:/third &&
+ cat >expect <<-EOF &&
+ first
+ third
+ second
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'conflicts are detected' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ echo base >file &&
+ git add file &&
+ git commit -m base &&
+ echo "first edit" >file &&
+ git commit -am "first edit" &&
+ echo "second edit" >file &&
+ git commit -am "second edit" &&
+
+ git symbolic-ref HEAD >expect-head &&
+ test_must_fail git history reorder HEAD --before=HEAD~ &&
+ test_must_fail git symbolic-ref HEAD &&
+ echo "second edit" >file &&
+ git add file &&
+ test_must_fail git history continue &&
+ echo "first edit" >file &&
+ git add file &&
+ git history continue &&
+
+ cat >expect <<-EOF &&
+ first edit
+ second edit
+ base
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual &&
+
+ git symbolic-ref HEAD >actual-head &&
+ test_cmp expect-head actual-head
+ )
+'
+
+test_done
--
2.51.0.308.g032396e0da.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH RFC v2 09/16] builtin/history: implement "reorder" subcommand
2025-08-24 17:42 ` [PATCH RFC v2 09/16] builtin/history: implement "reorder" subcommand Patrick Steinhardt
@ 2025-08-26 13:03 ` D. Ben Knoble
2025-09-03 12:19 ` Patrick Steinhardt
0 siblings, 1 reply; 278+ messages in thread
From: D. Ben Knoble @ 2025-08-26 13:03 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, Junio C Hamano, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk
On Sun, Aug 24, 2025 at 1:43 PM Patrick Steinhardt <ps@pks.im> wrote:
> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
> index db5b292994..b36cd925dd 100644
> --- a/Documentation/git-history.adoc
> +++ b/Documentation/git-history.adoc
> @@ -12,6 +12,7 @@ git history abort
> git history continue
> git history quit
> git history drop <commit>
> +git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)
>
> DESCRIPTION
> -----------
> @@ -39,12 +40,18 @@ rewrite history in different ways:
> `drop <commit>`::
> Drop a commit from the history and reapply all children of that
> commit on top of the commit's parent. The commit that is to be
> - dropped must be reachable from the current `HEAD` commit.
> + dropped must be reachable from the currently checked-out commit.
> +
> Dropping the root commit converts the child of that commit into the new
> root commit. It is invalid to drop a root commit that does not have any
> child commits, as that would lead to an empty branch.
Fixup in the wrong commit, maybe?
> +`reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)`::
> + Move the commit so that it becomes either the parent of
> + <following-commit> or the child of <preceding-commit>. The commits must
> + be related to one another and must be reachable from the current `HEAD`
> + commit.
> +
> The following commands are used to manage an interrupted history-rewriting
> operation:
>
--
D. Ben Knoble
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC v2 09/16] builtin/history: implement "reorder" subcommand
2025-08-26 13:03 ` D. Ben Knoble
@ 2025-09-03 12:19 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-03 12:19 UTC (permalink / raw)
To: D. Ben Knoble
Cc: git, Junio C Hamano, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk
On Tue, Aug 26, 2025 at 09:03:18AM -0400, D. Ben Knoble wrote:
> On Sun, Aug 24, 2025 at 1:43 PM Patrick Steinhardt <ps@pks.im> wrote:
> > diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
> > index db5b292994..b36cd925dd 100644
> > --- a/Documentation/git-history.adoc
> > +++ b/Documentation/git-history.adoc
> > @@ -39,12 +40,18 @@ rewrite history in different ways:
> > `drop <commit>`::
> > Drop a commit from the history and reapply all children of that
> > commit on top of the commit's parent. The commit that is to be
> > - dropped must be reachable from the current `HEAD` commit.
> > + dropped must be reachable from the currently checked-out commit.
> > +
> > Dropping the root commit converts the child of that commit into the new
> > root commit. It is invalid to drop a root commit that does not have any
> > child commits, as that would lead to an empty branch.
>
> Fixup in the wrong commit, maybe?
Indeed, fixed now.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH RFC v2 10/16] add-patch: split out header from "add-interactive.h"
2025-08-24 17:42 ` [PATCH RFC v2 00/16] " Patrick Steinhardt
` (8 preceding siblings ...)
2025-08-24 17:42 ` [PATCH RFC v2 09/16] builtin/history: implement "reorder" subcommand Patrick Steinhardt
@ 2025-08-24 17:42 ` Patrick Steinhardt
2025-08-25 16:41 ` Junio C Hamano
2025-08-24 17:42 ` [PATCH RFC v2 11/16] add-patch: split out `struct interactive_options` Patrick Steinhardt
` (6 subsequent siblings)
16 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-24 17:42 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
While we have a "add-patch.c" code file, its declarations are part of
"add-interactive.h". This makes it somewhat harder than necessary to
find relevant code and to identify clear boundaries between the two
subsystems.
Split up concerns and move declarations that relate to "add-patch.c"
into a new "add-patch.h" header.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
add-interactive.h | 23 +++--------------------
add-patch.c | 1 +
add-patch.h | 26 ++++++++++++++++++++++++++
3 files changed, 30 insertions(+), 20 deletions(-)
diff --git a/add-interactive.h b/add-interactive.h
index 4213dcd67b..fb95b6ee05 100644
--- a/add-interactive.h
+++ b/add-interactive.h
@@ -1,14 +1,11 @@
#ifndef ADD_INTERACTIVE_H
#define ADD_INTERACTIVE_H
+#include "add-patch.h"
#include "color.h"
-struct add_p_opt {
- int context;
- int interhunkcontext;
-};
-
-#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }
+struct pathspec;
+struct repository;
struct add_i_state {
struct repository *r;
@@ -32,21 +29,7 @@ void init_add_i_state(struct add_i_state *s, struct repository *r,
struct add_p_opt *add_p_opt);
void clear_add_i_state(struct add_i_state *s);
-struct repository;
-struct pathspec;
int run_add_i(struct repository *r, const struct pathspec *ps,
struct add_p_opt *add_p_opt);
-enum add_p_mode {
- ADD_P_ADD,
- ADD_P_STASH,
- ADD_P_RESET,
- ADD_P_CHECKOUT,
- ADD_P_WORKTREE,
-};
-
-int run_add_p(struct repository *r, enum add_p_mode mode,
- struct add_p_opt *o, const char *revision,
- const struct pathspec *ps);
-
#endif
diff --git a/add-patch.c b/add-patch.c
index 302e6ba7d9..e2b002fa73 100644
--- a/add-patch.c
+++ b/add-patch.c
@@ -3,6 +3,7 @@
#include "git-compat-util.h"
#include "add-interactive.h"
+#include "add-patch.h"
#include "advice.h"
#include "editor.h"
#include "environment.h"
diff --git a/add-patch.h b/add-patch.h
new file mode 100644
index 0000000000..4394c74107
--- /dev/null
+++ b/add-patch.h
@@ -0,0 +1,26 @@
+#ifndef ADD_PATCH_H
+#define ADD_PATCH_H
+
+struct pathspec;
+struct repository;
+
+struct add_p_opt {
+ int context;
+ int interhunkcontext;
+};
+
+#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }
+
+enum add_p_mode {
+ ADD_P_ADD,
+ ADD_P_STASH,
+ ADD_P_RESET,
+ ADD_P_CHECKOUT,
+ ADD_P_WORKTREE,
+};
+
+int run_add_p(struct repository *r, enum add_p_mode mode,
+ struct add_p_opt *o, const char *revision,
+ const struct pathspec *ps);
+
+#endif
--
2.51.0.308.g032396e0da.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH RFC v2 10/16] add-patch: split out header from "add-interactive.h"
2025-08-24 17:42 ` [PATCH RFC v2 10/16] add-patch: split out header from "add-interactive.h" Patrick Steinhardt
@ 2025-08-25 16:41 ` Junio C Hamano
0 siblings, 0 replies; 278+ messages in thread
From: Junio C Hamano @ 2025-08-25 16:41 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, D. Ben Knoble, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk
Patrick Steinhardt <ps@pks.im> writes:
> While we have a "add-patch.c" code file, its declarations are part of
> "add-interactive.h". This makes it somewhat harder than necessary to
> find relevant code and to identify clear boundaries between the two
> subsystems.
>
> Split up concerns and move declarations that relate to "add-patch.c"
> into a new "add-patch.h" header.
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> add-interactive.h | 23 +++--------------------
> add-patch.c | 1 +
> add-patch.h | 26 ++++++++++++++++++++++++++
> 3 files changed, 30 insertions(+), 20 deletions(-)
What is left in the interactive side is the things "add -i" can do
other than "add -p" (aka "add -i" plus "5: patch"), which makes
sense.
It is surprising that this step does not touch any of the clients of
the "-p" machinery (like "git reset -p"), though. They surely do
not need the rest of "add-interactive.h", do they?
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH RFC v2 11/16] add-patch: split out `struct interactive_options`
2025-08-24 17:42 ` [PATCH RFC v2 00/16] " Patrick Steinhardt
` (9 preceding siblings ...)
2025-08-24 17:42 ` [PATCH RFC v2 10/16] add-patch: split out header from "add-interactive.h" Patrick Steinhardt
@ 2025-08-24 17:42 ` Patrick Steinhardt
2025-08-24 17:42 ` [PATCH RFC v2 12/16] add-patch: remove dependency on "add-interactive" subsystem Patrick Steinhardt
` (5 subsequent siblings)
16 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-24 17:42 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
The `struct add_p_opt` is reused both by our the infra for "git add -p"
and "git add -i". Users of `run_add_i()` for example are expected to
pass `struct add_p_opt`. This is somewhat confusing and raises the
question which options apply to what part of the stack.
But things are even more confusing than that: while callers are expected
to pass in `struct add_p_opt`, these options ultimately get used to
initialize a `struct add_i_state` that is used by both subsystems. So we
are basically going full circle here.
Refactor the code and split out a new `struct interactive_options` that
hosts common options used by both. These options are then applied to a
`struct interactive_config` that hosts common configuration.
This refactoring doesn't yet fully detangle the two subsystems from one
another, as we still end up calling `init_add_i_state()` in the "git add
-p" subsystem. This will be fixed in a subsequent commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
add-interactive.c | 151 +++++++++++++----------------------------------------
add-interactive.h | 20 ++-----
add-patch.c | 145 +++++++++++++++++++++++++++++++++++++++++---------
add-patch.h | 33 ++++++++++--
builtin/add.c | 22 ++++----
builtin/checkout.c | 4 +-
builtin/commit.c | 16 +++---
builtin/reset.c | 16 +++---
builtin/stash.c | 46 ++++++++--------
commit.h | 2 +-
10 files changed, 243 insertions(+), 212 deletions(-)
diff --git a/add-interactive.c b/add-interactive.c
index 3e692b47ec..3babc3e013 100644
--- a/add-interactive.c
+++ b/add-interactive.c
@@ -3,7 +3,6 @@
#include "git-compat-util.h"
#include "add-interactive.h"
#include "color.h"
-#include "config.h"
#include "diffcore.h"
#include "gettext.h"
#include "hash.h"
@@ -20,96 +19,18 @@
#include "prompt.h"
#include "tree.h"
-static void init_color(struct repository *r, struct add_i_state *s,
- const char *section_and_slot, char *dst,
- const char *default_color)
-{
- char *key = xstrfmt("color.%s", section_and_slot);
- const char *value;
-
- if (!s->use_color)
- dst[0] = '\0';
- else if (repo_config_get_value(r, key, &value) ||
- color_parse(value, dst))
- strlcpy(dst, default_color, COLOR_MAXLEN);
-
- free(key);
-}
-
void init_add_i_state(struct add_i_state *s, struct repository *r,
- struct add_p_opt *add_p_opt)
+ struct interactive_options *opts)
{
- const char *value;
-
s->r = r;
- s->context = -1;
- s->interhunkcontext = -1;
-
- if (repo_config_get_value(r, "color.interactive", &value))
- s->use_color = -1;
- else
- s->use_color =
- git_config_colorbool("color.interactive", value);
- s->use_color = want_color(s->use_color);
-
- init_color(r, s, "interactive.header", s->header_color, GIT_COLOR_BOLD);
- init_color(r, s, "interactive.help", s->help_color, GIT_COLOR_BOLD_RED);
- init_color(r, s, "interactive.prompt", s->prompt_color,
- GIT_COLOR_BOLD_BLUE);
- init_color(r, s, "interactive.error", s->error_color,
- GIT_COLOR_BOLD_RED);
-
- init_color(r, s, "diff.frag", s->fraginfo_color,
- diff_get_color(s->use_color, DIFF_FRAGINFO));
- init_color(r, s, "diff.context", s->context_color, "fall back");
- if (!strcmp(s->context_color, "fall back"))
- init_color(r, s, "diff.plain", s->context_color,
- diff_get_color(s->use_color, DIFF_CONTEXT));
- init_color(r, s, "diff.old", s->file_old_color,
- diff_get_color(s->use_color, DIFF_FILE_OLD));
- init_color(r, s, "diff.new", s->file_new_color,
- diff_get_color(s->use_color, DIFF_FILE_NEW));
-
- strlcpy(s->reset_color,
- s->use_color ? GIT_COLOR_RESET : "", COLOR_MAXLEN);
-
- FREE_AND_NULL(s->interactive_diff_filter);
- repo_config_get_string(r, "interactive.difffilter",
- &s->interactive_diff_filter);
-
- FREE_AND_NULL(s->interactive_diff_algorithm);
- repo_config_get_string(r, "diff.algorithm",
- &s->interactive_diff_algorithm);
-
- if (!repo_config_get_int(r, "diff.context", &s->context))
- if (s->context < 0)
- die(_("%s cannot be negative"), "diff.context");
- if (!repo_config_get_int(r, "diff.interHunkContext", &s->interhunkcontext))
- if (s->interhunkcontext < 0)
- die(_("%s cannot be negative"), "diff.interHunkContext");
-
- repo_config_get_bool(r, "interactive.singlekey", &s->use_single_key);
- if (s->use_single_key)
- setbuf(stdin, NULL);
-
- if (add_p_opt->context != -1) {
- if (add_p_opt->context < 0)
- die(_("%s cannot be negative"), "--unified");
- s->context = add_p_opt->context;
- }
- if (add_p_opt->interhunkcontext != -1) {
- if (add_p_opt->interhunkcontext < 0)
- die(_("%s cannot be negative"), "--inter-hunk-context");
- s->interhunkcontext = add_p_opt->interhunkcontext;
- }
+ interactive_config_init(&s->cfg, r, opts);
}
void clear_add_i_state(struct add_i_state *s)
{
- FREE_AND_NULL(s->interactive_diff_filter);
- FREE_AND_NULL(s->interactive_diff_algorithm);
+ interactive_config_clear(&s->cfg);
memset(s, 0, sizeof(*s));
- s->use_color = -1;
+ interactive_config_clear(&s->cfg);
}
/*
@@ -262,7 +183,7 @@ static void list(struct add_i_state *s, struct string_list *list, int *selected,
return;
if (opts->header)
- color_fprintf_ln(stdout, s->header_color,
+ color_fprintf_ln(stdout, s->cfg.header_color,
"%s", opts->header);
for (i = 0; i < list->nr; i++) {
@@ -330,7 +251,7 @@ static ssize_t list_and_choose(struct add_i_state *s,
list(s, &items->items, items->selected, &opts->list_opts);
- color_fprintf(stdout, s->prompt_color, "%s", opts->prompt);
+ color_fprintf(stdout, s->cfg.prompt_color, "%s", opts->prompt);
fputs(singleton ? "> " : ">> ", stdout);
fflush(stdout);
@@ -408,7 +329,7 @@ static ssize_t list_and_choose(struct add_i_state *s,
if (from < 0 || from >= items->items.nr ||
(singleton && from + 1 != to)) {
- color_fprintf_ln(stderr, s->error_color,
+ color_fprintf_ln(stderr, s->cfg.error_color,
_("Huh (%s)?"), p);
break;
} else if (singleton) {
@@ -968,7 +889,7 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps,
free(files->items.items[i].string);
} else if (item->index.unmerged ||
item->worktree.unmerged) {
- color_fprintf_ln(stderr, s->error_color,
+ color_fprintf_ln(stderr, s->cfg.error_color,
_("ignoring unmerged: %s"),
files->items.items[i].string);
free(item);
@@ -990,9 +911,9 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps,
opts->prompt = N_("Patch update");
count = list_and_choose(s, files, opts);
if (count > 0) {
- struct add_p_opt add_p_opt = {
- .context = s->context,
- .interhunkcontext = s->interhunkcontext,
+ struct interactive_options opts = {
+ .context = s->cfg.context,
+ .interhunkcontext = s->cfg.interhunkcontext,
};
struct strvec args = STRVEC_INIT;
struct pathspec ps_selected = { 0 };
@@ -1004,7 +925,7 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps,
parse_pathspec(&ps_selected,
PATHSPEC_ALL_MAGIC & ~PATHSPEC_LITERAL,
PATHSPEC_LITERAL_PATH, "", args.v);
- res = run_add_p(s->r, ADD_P_ADD, &add_p_opt, NULL, &ps_selected);
+ res = run_add_p(s->r, ADD_P_ADD, &opts, NULL, &ps_selected);
strvec_clear(&args);
clear_pathspec(&ps_selected);
}
@@ -1040,10 +961,10 @@ static int run_diff(struct add_i_state *s, const struct pathspec *ps,
struct child_process cmd = CHILD_PROCESS_INIT;
strvec_pushl(&cmd.args, "git", "diff", "-p", "--cached", NULL);
- if (s->context != -1)
- strvec_pushf(&cmd.args, "--unified=%i", s->context);
- if (s->interhunkcontext != -1)
- strvec_pushf(&cmd.args, "--inter-hunk-context=%i", s->interhunkcontext);
+ if (s->cfg.context != -1)
+ strvec_pushf(&cmd.args, "--unified=%i", s->cfg.context);
+ if (s->cfg.interhunkcontext != -1)
+ strvec_pushf(&cmd.args, "--inter-hunk-context=%i", s->cfg.interhunkcontext);
strvec_pushl(&cmd.args, oid_to_hex(!is_initial ? &oid :
s->r->hash_algo->empty_tree), "--", NULL);
for (i = 0; i < files->items.nr; i++)
@@ -1061,17 +982,17 @@ static int run_help(struct add_i_state *s, const struct pathspec *ps UNUSED,
struct prefix_item_list *files UNUSED,
struct list_and_choose_options *opts UNUSED)
{
- color_fprintf_ln(stdout, s->help_color, "status - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "status - %s",
_("show paths with changes"));
- color_fprintf_ln(stdout, s->help_color, "update - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "update - %s",
_("add working tree state to the staged set of changes"));
- color_fprintf_ln(stdout, s->help_color, "revert - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "revert - %s",
_("revert staged set of changes back to the HEAD version"));
- color_fprintf_ln(stdout, s->help_color, "patch - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "patch - %s",
_("pick hunks and update selectively"));
- color_fprintf_ln(stdout, s->help_color, "diff - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "diff - %s",
_("view diff between HEAD and index"));
- color_fprintf_ln(stdout, s->help_color, "add untracked - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "add untracked - %s",
_("add contents of untracked files to the staged set of changes"));
return 0;
@@ -1079,21 +1000,21 @@ static int run_help(struct add_i_state *s, const struct pathspec *ps UNUSED,
static void choose_prompt_help(struct add_i_state *s)
{
- color_fprintf_ln(stdout, s->help_color, "%s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "%s",
_("Prompt help:"));
- color_fprintf_ln(stdout, s->help_color, "1 - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "1 - %s",
_("select a single item"));
- color_fprintf_ln(stdout, s->help_color, "3-5 - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "3-5 - %s",
_("select a range of items"));
- color_fprintf_ln(stdout, s->help_color, "2-3,6-9 - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "2-3,6-9 - %s",
_("select multiple ranges"));
- color_fprintf_ln(stdout, s->help_color, "foo - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "foo - %s",
_("select item based on unique prefix"));
- color_fprintf_ln(stdout, s->help_color, "-... - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "-... - %s",
_("unselect specified items"));
- color_fprintf_ln(stdout, s->help_color, "* - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "* - %s",
_("choose all items"));
- color_fprintf_ln(stdout, s->help_color, " - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, " - %s",
_("(empty) finish selecting"));
}
@@ -1128,7 +1049,7 @@ static void print_command_item(int i, int selected UNUSED,
static void command_prompt_help(struct add_i_state *s)
{
- const char *help_color = s->help_color;
+ const char *help_color = s->cfg.help_color;
color_fprintf_ln(stdout, help_color, "%s", _("Prompt help:"));
color_fprintf_ln(stdout, help_color, "1 - %s",
_("select a numbered item"));
@@ -1139,7 +1060,7 @@ static void command_prompt_help(struct add_i_state *s)
}
int run_add_i(struct repository *r, const struct pathspec *ps,
- struct add_p_opt *add_p_opt)
+ struct interactive_options *interactive_opts)
{
struct add_i_state s = { NULL };
struct print_command_item_data data = { "[", "]" };
@@ -1182,15 +1103,15 @@ int run_add_i(struct repository *r, const struct pathspec *ps,
->util = util;
}
- init_add_i_state(&s, r, add_p_opt);
+ init_add_i_state(&s, r, interactive_opts);
/*
* When color was asked for, use the prompt color for
* highlighting, otherwise use square brackets.
*/
- if (s.use_color) {
- data.color = s.prompt_color;
- data.reset = s.reset_color;
+ if (s.cfg.use_color) {
+ data.color = s.cfg.prompt_color;
+ data.reset = s.cfg.reset_color;
}
print_file_item_data.color = data.color;
print_file_item_data.reset = data.reset;
diff --git a/add-interactive.h b/add-interactive.h
index fb95b6ee05..eefa2edc7c 100644
--- a/add-interactive.h
+++ b/add-interactive.h
@@ -2,34 +2,20 @@
#define ADD_INTERACTIVE_H
#include "add-patch.h"
-#include "color.h"
struct pathspec;
struct repository;
struct add_i_state {
struct repository *r;
- int use_color;
- char header_color[COLOR_MAXLEN];
- char help_color[COLOR_MAXLEN];
- char prompt_color[COLOR_MAXLEN];
- char error_color[COLOR_MAXLEN];
- char reset_color[COLOR_MAXLEN];
- char fraginfo_color[COLOR_MAXLEN];
- char context_color[COLOR_MAXLEN];
- char file_old_color[COLOR_MAXLEN];
- char file_new_color[COLOR_MAXLEN];
-
- int use_single_key;
- char *interactive_diff_filter, *interactive_diff_algorithm;
- int context, interhunkcontext;
+ struct interactive_config cfg;
};
void init_add_i_state(struct add_i_state *s, struct repository *r,
- struct add_p_opt *add_p_opt);
+ struct interactive_options *opts);
void clear_add_i_state(struct add_i_state *s);
int run_add_i(struct repository *r, const struct pathspec *ps,
- struct add_p_opt *add_p_opt);
+ struct interactive_options *opts);
#endif
diff --git a/add-patch.c b/add-patch.c
index e2b002fa73..45bc254e0c 100644
--- a/add-patch.c
+++ b/add-patch.c
@@ -5,6 +5,8 @@
#include "add-interactive.h"
#include "add-patch.h"
#include "advice.h"
+#include "config.h"
+#include "diff.h"
#include "editor.h"
#include "environment.h"
#include "gettext.h"
@@ -279,6 +281,99 @@ struct add_p_state {
const char *revision;
};
+static void init_color(struct repository *r,
+ struct interactive_config *cfg,
+ const char *section_and_slot, char *dst,
+ const char *default_color)
+{
+ char *key = xstrfmt("color.%s", section_and_slot);
+ const char *value;
+
+ if (!cfg->use_color)
+ dst[0] = '\0';
+ else if (repo_config_get_value(r, key, &value) ||
+ color_parse(value, dst))
+ strlcpy(dst, default_color, COLOR_MAXLEN);
+
+ free(key);
+}
+
+void interactive_config_init(struct interactive_config *cfg,
+ struct repository *r,
+ struct interactive_options *opts)
+{
+ const char *value;
+
+ cfg->context = -1;
+ cfg->interhunkcontext = -1;
+
+ if (repo_config_get_value(r, "color.interactive", &value))
+ cfg->use_color = -1;
+ else
+ cfg->use_color =
+ git_config_colorbool("color.interactive", value);
+ cfg->use_color = want_color(cfg->use_color);
+
+ init_color(r, cfg, "interactive.header", cfg->header_color, GIT_COLOR_BOLD);
+ init_color(r, cfg, "interactive.help", cfg->help_color, GIT_COLOR_BOLD_RED);
+ init_color(r, cfg, "interactive.prompt", cfg->prompt_color,
+ GIT_COLOR_BOLD_BLUE);
+ init_color(r, cfg, "interactive.error", cfg->error_color,
+ GIT_COLOR_BOLD_RED);
+
+ init_color(r, cfg, "diff.frag", cfg->fraginfo_color,
+ diff_get_color(cfg->use_color, DIFF_FRAGINFO));
+ init_color(r, cfg, "diff.context", cfg->context_color, "fall back");
+ if (!strcmp(cfg->context_color, "fall back"))
+ init_color(r, cfg, "diff.plain", cfg->context_color,
+ diff_get_color(cfg->use_color, DIFF_CONTEXT));
+ init_color(r, cfg, "diff.old", cfg->file_old_color,
+ diff_get_color(cfg->use_color, DIFF_FILE_OLD));
+ init_color(r, cfg, "diff.new", cfg->file_new_color,
+ diff_get_color(cfg->use_color, DIFF_FILE_NEW));
+
+ strlcpy(cfg->reset_color,
+ cfg->use_color ? GIT_COLOR_RESET : "", COLOR_MAXLEN);
+
+ FREE_AND_NULL(cfg->interactive_diff_filter);
+ repo_config_get_string(r, "interactive.difffilter",
+ &cfg->interactive_diff_filter);
+
+ FREE_AND_NULL(cfg->interactive_diff_algorithm);
+ repo_config_get_string(r, "diff.algorithm",
+ &cfg->interactive_diff_algorithm);
+
+ if (!repo_config_get_int(r, "diff.context", &cfg->context))
+ if (cfg->context < 0)
+ die(_("%s cannot be negative"), "diff.context");
+ if (!repo_config_get_int(r, "diff.interHunkContext", &cfg->interhunkcontext))
+ if (cfg->interhunkcontext < 0)
+ die(_("%s cannot be negative"), "diff.interHunkContext");
+
+ repo_config_get_bool(r, "interactive.singlekey", &cfg->use_single_key);
+ if (cfg->use_single_key)
+ setbuf(stdin, NULL);
+
+ if (opts->context != -1) {
+ if (opts->context < 0)
+ die(_("%s cannot be negative"), "--unified");
+ cfg->context = opts->context;
+ }
+ if (opts->interhunkcontext != -1) {
+ if (opts->interhunkcontext < 0)
+ die(_("%s cannot be negative"), "--inter-hunk-context");
+ cfg->interhunkcontext = opts->interhunkcontext;
+ }
+}
+
+void interactive_config_clear(struct interactive_config *cfg)
+{
+ FREE_AND_NULL(cfg->interactive_diff_filter);
+ FREE_AND_NULL(cfg->interactive_diff_algorithm);
+ memset(cfg, 0, sizeof(*cfg));
+ cfg->use_color = -1;
+}
+
static void add_p_state_clear(struct add_p_state *s)
{
size_t i;
@@ -299,9 +394,9 @@ static void err(struct add_p_state *s, const char *fmt, ...)
va_list args;
va_start(args, fmt);
- fputs(s->s.error_color, stdout);
+ fputs(s->s.cfg.error_color, stdout);
vprintf(fmt, args);
- puts(s->s.reset_color);
+ puts(s->s.cfg.reset_color);
va_end(args);
}
@@ -424,12 +519,12 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
int res;
strvec_pushv(&args, s->mode->diff_cmd);
- if (s->s.context != -1)
- strvec_pushf(&args, "--unified=%i", s->s.context);
- if (s->s.interhunkcontext != -1)
- strvec_pushf(&args, "--inter-hunk-context=%i", s->s.interhunkcontext);
- if (s->s.interactive_diff_algorithm)
- strvec_pushf(&args, "--diff-algorithm=%s", s->s.interactive_diff_algorithm);
+ if (s->s.cfg.context != -1)
+ strvec_pushf(&args, "--unified=%i", s->s.cfg.context);
+ if (s->s.cfg.interhunkcontext != -1)
+ strvec_pushf(&args, "--inter-hunk-context=%i", s->s.cfg.interhunkcontext);
+ if (s->s.cfg.interactive_diff_algorithm)
+ strvec_pushf(&args, "--diff-algorithm=%s", s->s.cfg.interactive_diff_algorithm);
if (s->revision) {
struct object_id oid;
strvec_push(&args,
@@ -460,7 +555,7 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
if (want_color_fd(1, -1)) {
struct child_process colored_cp = CHILD_PROCESS_INIT;
- const char *diff_filter = s->s.interactive_diff_filter;
+ const char *diff_filter = s->s.cfg.interactive_diff_filter;
setup_child_process(s, &colored_cp, NULL);
xsnprintf((char *)args.v[color_arg_index], 8, "--color");
@@ -693,7 +788,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
hunk->colored_end - hunk->colored_start);
return;
} else {
- strbuf_addstr(out, s->s.fraginfo_color);
+ strbuf_addstr(out, s->s.cfg.fraginfo_color);
p = s->colored.buf + header->colored_extra_start;
len = header->colored_extra_end
- header->colored_extra_start;
@@ -715,7 +810,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
if (len)
strbuf_add(out, p, len);
else if (colored)
- strbuf_addf(out, "%s\n", s->s.reset_color);
+ strbuf_addf(out, "%s\n", s->s.cfg.reset_color);
else
strbuf_addch(out, '\n');
}
@@ -1103,12 +1198,12 @@ static void recolor_hunk(struct add_p_state *s, struct hunk *hunk)
strbuf_addstr(&s->colored,
plain[current] == '-' ?
- s->s.file_old_color :
+ s->s.cfg.file_old_color :
plain[current] == '+' ?
- s->s.file_new_color :
- s->s.context_color);
+ s->s.cfg.file_new_color :
+ s->s.cfg.context_color);
strbuf_add(&s->colored, plain + current, eol - current);
- strbuf_addstr(&s->colored, s->s.reset_color);
+ strbuf_addstr(&s->colored, s->s.cfg.reset_color);
if (next > eol)
strbuf_add(&s->colored, plain + eol, next - eol);
current = next;
@@ -1227,7 +1322,7 @@ static int run_apply_check(struct add_p_state *s,
static int read_single_character(struct add_p_state *s)
{
- if (s->s.use_single_key) {
+ if (s->s.cfg.use_single_key) {
int res = read_key_without_echo(&s->answer);
printf("%s\n", res == EOF ? "" : s->answer.buf);
return res;
@@ -1241,7 +1336,7 @@ static int read_single_character(struct add_p_state *s)
static int prompt_yesno(struct add_p_state *s, const char *prompt)
{
for (;;) {
- color_fprintf(stdout, s->s.prompt_color, "%s", _(prompt));
+ color_fprintf(stdout, s->s.cfg.prompt_color, "%s", _(prompt));
fflush(stdout);
if (read_single_character(s) == EOF)
return -1;
@@ -1522,15 +1617,15 @@ static int patch_update_file(struct add_p_state *s,
else
prompt_mode_type = PROMPT_HUNK;
- printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->s.prompt_color,
+ printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->s.cfg.prompt_color,
(uintmax_t)hunk_index + 1,
(uintmax_t)(file_diff->hunk_nr
? file_diff->hunk_nr
: 1));
printf(_(s->mode->prompt_mode[prompt_mode_type]),
s->buf.buf);
- if (*s->s.reset_color)
- fputs(s->s.reset_color, stdout);
+ if (*s->s.cfg.reset_color)
+ fputs(s->s.cfg.reset_color, stdout);
fflush(stdout);
if (read_single_character(s) == EOF)
break;
@@ -1687,7 +1782,7 @@ static int patch_update_file(struct add_p_state *s,
err(s, _("Sorry, cannot split this hunk"));
} else if (!split_hunk(s, file_diff,
hunk - file_diff->hunk)) {
- color_fprintf_ln(stdout, s->s.header_color,
+ color_fprintf_ln(stdout, s->s.cfg.header_color,
_("Split into %d hunks."),
(int)splittable_into);
rendered_hunk_index = -1;
@@ -1705,7 +1800,7 @@ static int patch_update_file(struct add_p_state *s,
} else if (s->answer.buf[0] == '?') {
const char *p = _(help_patch_remainder), *eol = p;
- color_fprintf(stdout, s->s.help_color, "%s",
+ color_fprintf(stdout, s->s.cfg.help_color, "%s",
_(s->mode->help_patch_text));
/*
@@ -1723,7 +1818,7 @@ static int patch_update_file(struct add_p_state *s,
if (*p != '?' && !strchr(s->buf.buf, *p))
continue;
- color_fprintf_ln(stdout, s->s.help_color,
+ color_fprintf_ln(stdout, s->s.cfg.help_color,
"%.*s", (int)(eol - p), p);
}
} else {
@@ -1764,7 +1859,7 @@ static int patch_update_file(struct add_p_state *s,
}
int run_add_p(struct repository *r, enum add_p_mode mode,
- struct add_p_opt *o, const char *revision,
+ struct interactive_options *opts, const char *revision,
const struct pathspec *ps)
{
struct add_p_state s = {
@@ -1772,7 +1867,7 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
};
size_t i, binary_count = 0;
- init_add_i_state(&s.s, r, o);
+ init_add_i_state(&s.s, r, opts);
if (mode == ADD_P_STASH)
s.mode = &patch_mode_stash;
diff --git a/add-patch.h b/add-patch.h
index 4394c74107..51c0d7bce9 100644
--- a/add-patch.h
+++ b/add-patch.h
@@ -1,15 +1,42 @@
#ifndef ADD_PATCH_H
#define ADD_PATCH_H
+#include "color.h"
+
struct pathspec;
struct repository;
-struct add_p_opt {
+struct interactive_options {
int context;
int interhunkcontext;
};
-#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }
+#define INTERACTIVE_OPTIONS_INIT { \
+ .context = -1, \
+ .interhunkcontext = -1, \
+}
+
+struct interactive_config {
+ int use_color;
+ char header_color[COLOR_MAXLEN];
+ char help_color[COLOR_MAXLEN];
+ char prompt_color[COLOR_MAXLEN];
+ char error_color[COLOR_MAXLEN];
+ char reset_color[COLOR_MAXLEN];
+ char fraginfo_color[COLOR_MAXLEN];
+ char context_color[COLOR_MAXLEN];
+ char file_old_color[COLOR_MAXLEN];
+ char file_new_color[COLOR_MAXLEN];
+
+ int use_single_key;
+ char *interactive_diff_filter, *interactive_diff_algorithm;
+ int context, interhunkcontext;
+};
+
+void interactive_config_init(struct interactive_config *cfg,
+ struct repository *r,
+ struct interactive_options *opts);
+void interactive_config_clear(struct interactive_config *cfg);
enum add_p_mode {
ADD_P_ADD,
@@ -20,7 +47,7 @@ enum add_p_mode {
};
int run_add_p(struct repository *r, enum add_p_mode mode,
- struct add_p_opt *o, const char *revision,
+ struct interactive_options *opts, const char *revision,
const struct pathspec *ps);
#endif
diff --git a/builtin/add.c b/builtin/add.c
index 0235854f80..a94c826c14 100644
--- a/builtin/add.c
+++ b/builtin/add.c
@@ -30,7 +30,7 @@ static const char * const builtin_add_usage[] = {
NULL
};
static int patch_interactive, add_interactive, edit_interactive;
-static struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+static struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
static int take_worktree_changes;
static int add_renormalize;
static int pathspec_file_nul;
@@ -159,7 +159,7 @@ static int refresh(struct repository *repo, int verbose, const struct pathspec *
int interactive_add(struct repository *repo,
const char **argv,
const char *prefix,
- int patch, struct add_p_opt *add_p_opt)
+ int patch, struct interactive_options *interactive_opts)
{
struct pathspec pathspec;
int ret;
@@ -171,9 +171,9 @@ int interactive_add(struct repository *repo,
prefix, argv);
if (patch)
- ret = !!run_add_p(repo, ADD_P_ADD, add_p_opt, NULL, &pathspec);
+ ret = !!run_add_p(repo, ADD_P_ADD, interactive_opts, NULL, &pathspec);
else
- ret = !!run_add_i(repo, &pathspec, add_p_opt);
+ ret = !!run_add_i(repo, &pathspec, interactive_opts);
clear_pathspec(&pathspec);
return ret;
@@ -255,8 +255,8 @@ static struct option builtin_add_options[] = {
OPT_GROUP(""),
OPT_BOOL('i', "interactive", &add_interactive, N_("interactive picking")),
OPT_BOOL('p', "patch", &patch_interactive, N_("select hunks interactively")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT_BOOL('e', "edit", &edit_interactive, N_("edit current diff and apply")),
OPT__FORCE(&ignored_too, N_("allow adding otherwise ignored files"), 0),
OPT_BOOL('u', "update", &take_worktree_changes, N_("update tracked files")),
@@ -398,9 +398,9 @@ int cmd_add(int argc,
prepare_repo_settings(repo);
repo->settings.command_requires_full_index = 0;
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
if (patch_interactive)
@@ -410,11 +410,11 @@ int cmd_add(int argc,
die(_("options '%s' and '%s' cannot be used together"), "--dry-run", "--interactive/--patch");
if (pathspec_from_file)
die(_("options '%s' and '%s' cannot be used together"), "--pathspec-from-file", "--interactive/--patch");
- exit(interactive_add(repo, argv + 1, prefix, patch_interactive, &add_p_opt));
+ exit(interactive_add(repo, argv + 1, prefix, patch_interactive, &interactive_opts));
} else {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--interactive/--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--interactive/--patch");
}
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 43583c8d1b..0b90f398fe 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -546,7 +546,7 @@ static int checkout_paths(const struct checkout_opts *opts,
if (opts->patch_mode) {
enum add_p_mode patch_mode;
- struct add_p_opt add_p_opt = {
+ struct interactive_options interactive_opts = {
.context = opts->patch_context,
.interhunkcontext = opts->patch_interhunk_context,
};
@@ -575,7 +575,7 @@ static int checkout_paths(const struct checkout_opts *opts,
else
BUG("either flag must have been set, worktree=%d, index=%d",
opts->checkout_worktree, opts->checkout_index);
- return !!run_add_p(the_repository, patch_mode, &add_p_opt,
+ return !!run_add_p(the_repository, patch_mode, &interactive_opts,
rev, &opts->pathspec);
}
diff --git a/builtin/commit.c b/builtin/commit.c
index b5b9608813..767351fd87 100644
--- a/builtin/commit.c
+++ b/builtin/commit.c
@@ -123,7 +123,7 @@ static const char *edit_message, *use_message;
static char *fixup_message, *fixup_commit, *squash_message;
static const char *fixup_prefix;
static int all, also, interactive, patch_interactive, only, amend, signoff;
-static struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+static struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
static int edit_flag = -1; /* unspecified */
static int quiet, verbose, no_verify, allow_empty, dry_run, renew_authorship;
static int config_commit_verbose = -1; /* unspecified */
@@ -356,9 +356,9 @@ static const char *prepare_index(const char **argv, const char *prefix,
const char *ret;
char *path = NULL;
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
if (is_status)
@@ -407,7 +407,7 @@ static const char *prepare_index(const char **argv, const char *prefix,
old_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT));
setenv(INDEX_ENVIRONMENT, the_repository->index_file, 1);
- if (interactive_add(the_repository, argv, prefix, patch_interactive, &add_p_opt) != 0)
+ if (interactive_add(the_repository, argv, prefix, patch_interactive, &interactive_opts) != 0)
die(_("interactive add failed"));
the_repository->index_file = old_repo_index_file;
@@ -432,9 +432,9 @@ static const char *prepare_index(const char **argv, const char *prefix,
ret = get_lock_file_path(&index_lock);
goto out;
} else {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--interactive/--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--interactive/--patch");
}
@@ -1738,8 +1738,8 @@ int cmd_commit(int argc,
OPT_BOOL('i', "include", &also, N_("add specified files to index for commit")),
OPT_BOOL(0, "interactive", &interactive, N_("interactively add files")),
OPT_BOOL('p', "patch", &patch_interactive, N_("interactively add changes")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT_BOOL('o', "only", &only, N_("commit only specified files")),
OPT_BOOL('n', "no-verify", &no_verify, N_("bypass pre-commit and commit-msg hooks")),
OPT_BOOL(0, "dry-run", &dry_run, N_("show what would be committed")),
diff --git a/builtin/reset.c b/builtin/reset.c
index ed35802af1..088449e120 100644
--- a/builtin/reset.c
+++ b/builtin/reset.c
@@ -346,7 +346,7 @@ int cmd_reset(int argc,
struct object_id oid;
struct pathspec pathspec;
int intent_to_add = 0;
- struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
const struct option options[] = {
OPT__QUIET(&quiet, N_("be quiet, only report errors")),
OPT_BOOL(0, "no-refresh", &no_refresh,
@@ -371,8 +371,8 @@ int cmd_reset(int argc,
PARSE_OPT_OPTARG,
option_parse_recurse_submodules_worktree_updater),
OPT_BOOL('p', "patch", &patch_mode, N_("select hunks interactively")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT_BOOL('N', "intent-to-add", &intent_to_add,
N_("record only the fact that removed paths will be added later")),
OPT_PATHSPEC_FROM_FILE(&pathspec_from_file),
@@ -423,9 +423,9 @@ int cmd_reset(int argc,
oidcpy(&oid, &tree->object.oid);
}
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
prepare_repo_settings(the_repository);
@@ -436,12 +436,12 @@ int cmd_reset(int argc,
die(_("options '%s' and '%s' cannot be used together"), "--patch", "--{hard,mixed,soft}");
trace2_cmd_mode("patch-interactive");
update_ref_status = !!run_add_p(the_repository, ADD_P_RESET,
- &add_p_opt, rev, &pathspec);
+ &interactive_opts, rev, &pathspec);
goto cleanup;
} else {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--patch");
}
diff --git a/builtin/stash.c b/builtin/stash.c
index 1977e50df2..5070b8a88f 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -1302,7 +1302,7 @@ static int stash_staged(struct stash_info *info, struct strbuf *out_patch,
static int stash_patch(struct stash_info *info, const struct pathspec *ps,
struct strbuf *out_patch, int quiet,
- struct add_p_opt *add_p_opt)
+ struct interactive_options *interactive_opts)
{
int ret = 0;
struct child_process cp_read_tree = CHILD_PROCESS_INIT;
@@ -1327,7 +1327,7 @@ static int stash_patch(struct stash_info *info, const struct pathspec *ps,
old_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT));
setenv(INDEX_ENVIRONMENT, the_repository->index_file, 1);
- ret = !!run_add_p(the_repository, ADD_P_STASH, add_p_opt, NULL, ps);
+ ret = !!run_add_p(the_repository, ADD_P_STASH, interactive_opts, NULL, ps);
the_repository->index_file = old_repo_index_file;
if (old_index_env && *old_index_env)
@@ -1422,7 +1422,8 @@ static int stash_working_tree(struct stash_info *info, const struct pathspec *ps
}
static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_buf,
- int include_untracked, int patch_mode, struct add_p_opt *add_p_opt,
+ int include_untracked, int patch_mode,
+ struct interactive_options *interactive_opts,
int only_staged, struct stash_info *info, struct strbuf *patch,
int quiet)
{
@@ -1504,7 +1505,7 @@ static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_b
untracked_commit_option = 1;
}
if (patch_mode) {
- ret = stash_patch(info, ps, patch, quiet, add_p_opt);
+ ret = stash_patch(info, ps, patch, quiet, interactive_opts);
if (ret < 0) {
if (!quiet)
fprintf_ln(stderr, _("Cannot save the current "
@@ -1590,7 +1591,8 @@ static int create_stash(int argc, const char **argv, const char *prefix UNUSED,
}
static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int quiet,
- int keep_index, int patch_mode, struct add_p_opt *add_p_opt,
+ int keep_index, int patch_mode,
+ struct interactive_options *interactive_opts,
int include_untracked, int only_staged)
{
int ret = 0;
@@ -1662,7 +1664,7 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q
if (stash_msg)
strbuf_addstr(&stash_msg_buf, stash_msg);
if (do_create_stash(ps, &stash_msg_buf, include_untracked, patch_mode,
- add_p_opt, only_staged, &info, &patch, quiet)) {
+ interactive_opts, only_staged, &info, &patch, quiet)) {
ret = -1;
goto done;
}
@@ -1835,7 +1837,7 @@ static int push_stash(int argc, const char **argv, const char *prefix,
const char *stash_msg = NULL;
char *pathspec_from_file = NULL;
struct pathspec ps;
- struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
struct option options[] = {
OPT_BOOL('k', "keep-index", &keep_index,
N_("keep index")),
@@ -1843,8 +1845,8 @@ static int push_stash(int argc, const char **argv, const char *prefix,
N_("stash staged changes only")),
OPT_BOOL('p', "patch", &patch_mode,
N_("stash in patch mode")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT__QUIET(&quiet, N_("quiet mode")),
OPT_BOOL('u', "include-untracked", &include_untracked,
N_("include untracked files in stash")),
@@ -1901,19 +1903,19 @@ static int push_stash(int argc, const char **argv, const char *prefix,
}
if (!patch_mode) {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--patch");
}
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
ret = do_push_stash(&ps, stash_msg, quiet, keep_index, patch_mode,
- &add_p_opt, include_untracked, only_staged);
+ &interactive_opts, include_untracked, only_staged);
clear_pathspec(&ps);
free(pathspec_from_file);
@@ -1938,7 +1940,7 @@ static int save_stash(int argc, const char **argv, const char *prefix,
const char *stash_msg = NULL;
struct pathspec ps;
struct strbuf stash_msg_buf = STRBUF_INIT;
- struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
struct option options[] = {
OPT_BOOL('k', "keep-index", &keep_index,
N_("keep index")),
@@ -1946,8 +1948,8 @@ static int save_stash(int argc, const char **argv, const char *prefix,
N_("stash staged changes only")),
OPT_BOOL('p', "patch", &patch_mode,
N_("stash in patch mode")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT__QUIET(&quiet, N_("quiet mode")),
OPT_BOOL('u', "include-untracked", &include_untracked,
N_("include untracked files in stash")),
@@ -1967,20 +1969,20 @@ static int save_stash(int argc, const char **argv, const char *prefix,
memset(&ps, 0, sizeof(ps));
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
if (!patch_mode) {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--patch");
}
ret = do_push_stash(&ps, stash_msg, quiet, keep_index,
- patch_mode, &add_p_opt, include_untracked,
+ patch_mode, &interactive_opts, include_untracked,
only_staged);
strbuf_release(&stash_msg_buf);
diff --git a/commit.h b/commit.h
index 1d6e0c7518..7b6e59d6c1 100644
--- a/commit.h
+++ b/commit.h
@@ -258,7 +258,7 @@ int for_each_commit_graft(each_commit_graft_fn, void *);
int interactive_add(struct repository *repo,
const char **argv,
const char *prefix,
- int patch, struct add_p_opt *add_p_opt);
+ int patch, struct interactive_options *opts);
struct commit_extra_header {
struct commit_extra_header *next;
--
2.51.0.308.g032396e0da.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH RFC v2 12/16] add-patch: remove dependency on "add-interactive" subsystem
2025-08-24 17:42 ` [PATCH RFC v2 00/16] " Patrick Steinhardt
` (10 preceding siblings ...)
2025-08-24 17:42 ` [PATCH RFC v2 11/16] add-patch: split out `struct interactive_options` Patrick Steinhardt
@ 2025-08-24 17:42 ` Patrick Steinhardt
2025-08-25 16:43 ` Junio C Hamano
2025-08-24 17:42 ` [PATCH RFC v2 13/16] add-patch: add support for in-memory index patching Patrick Steinhardt
` (4 subsequent siblings)
16 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-24 17:42 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
With the preceding commit we have split out interactive configuration
that is used by both "git add -p" and "git add -i". But we still
initialize that configuration in the "add -p" subsystem by calling
`init_add_i_state()`, even though we only do so to initialize the
interactive configuration as well as a repository pointer.
Stop doing so and instead store and initialize the interactive
configuration in `struct add_p_state` directly.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
add-patch.c | 68 ++++++++++++++++++++++++++++++++-----------------------------
1 file changed, 36 insertions(+), 32 deletions(-)
diff --git a/add-patch.c b/add-patch.c
index 45bc254e0c..1bcbc91de9 100644
--- a/add-patch.c
+++ b/add-patch.c
@@ -2,7 +2,6 @@
#define DISABLE_SIGN_COMPARE_WARNINGS
#include "git-compat-util.h"
-#include "add-interactive.h"
#include "add-patch.h"
#include "advice.h"
#include "config.h"
@@ -263,7 +262,8 @@ struct hunk {
};
struct add_p_state {
- struct add_i_state s;
+ struct repository *r;
+ struct interactive_config cfg;
struct strbuf answer, buf;
/* parsed diff */
@@ -385,7 +385,7 @@ static void add_p_state_clear(struct add_p_state *s)
for (i = 0; i < s->file_diff_nr; i++)
free(s->file_diff[i].hunk);
free(s->file_diff);
- clear_add_i_state(&s->s);
+ interactive_config_clear(&s->cfg);
}
__attribute__((format (printf, 2, 3)))
@@ -394,9 +394,9 @@ static void err(struct add_p_state *s, const char *fmt, ...)
va_list args;
va_start(args, fmt);
- fputs(s->s.cfg.error_color, stdout);
+ fputs(s->cfg.error_color, stdout);
vprintf(fmt, args);
- puts(s->s.cfg.reset_color);
+ puts(s->cfg.reset_color);
va_end(args);
}
@@ -414,7 +414,7 @@ static void setup_child_process(struct add_p_state *s,
cp->git_cmd = 1;
strvec_pushf(&cp->env,
- INDEX_ENVIRONMENT "=%s", s->s.r->index_file);
+ INDEX_ENVIRONMENT "=%s", s->r->index_file);
}
static int parse_range(const char **p,
@@ -519,12 +519,12 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
int res;
strvec_pushv(&args, s->mode->diff_cmd);
- if (s->s.cfg.context != -1)
- strvec_pushf(&args, "--unified=%i", s->s.cfg.context);
- if (s->s.cfg.interhunkcontext != -1)
- strvec_pushf(&args, "--inter-hunk-context=%i", s->s.cfg.interhunkcontext);
- if (s->s.cfg.interactive_diff_algorithm)
- strvec_pushf(&args, "--diff-algorithm=%s", s->s.cfg.interactive_diff_algorithm);
+ if (s->cfg.context != -1)
+ strvec_pushf(&args, "--unified=%i", s->cfg.context);
+ if (s->cfg.interhunkcontext != -1)
+ strvec_pushf(&args, "--inter-hunk-context=%i", s->cfg.interhunkcontext);
+ if (s->cfg.interactive_diff_algorithm)
+ strvec_pushf(&args, "--diff-algorithm=%s", s->cfg.interactive_diff_algorithm);
if (s->revision) {
struct object_id oid;
strvec_push(&args,
@@ -555,7 +555,7 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
if (want_color_fd(1, -1)) {
struct child_process colored_cp = CHILD_PROCESS_INIT;
- const char *diff_filter = s->s.cfg.interactive_diff_filter;
+ const char *diff_filter = s->cfg.interactive_diff_filter;
setup_child_process(s, &colored_cp, NULL);
xsnprintf((char *)args.v[color_arg_index], 8, "--color");
@@ -788,7 +788,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
hunk->colored_end - hunk->colored_start);
return;
} else {
- strbuf_addstr(out, s->s.cfg.fraginfo_color);
+ strbuf_addstr(out, s->cfg.fraginfo_color);
p = s->colored.buf + header->colored_extra_start;
len = header->colored_extra_end
- header->colored_extra_start;
@@ -810,7 +810,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
if (len)
strbuf_add(out, p, len);
else if (colored)
- strbuf_addf(out, "%s\n", s->s.cfg.reset_color);
+ strbuf_addf(out, "%s\n", s->cfg.reset_color);
else
strbuf_addch(out, '\n');
}
@@ -1198,12 +1198,12 @@ static void recolor_hunk(struct add_p_state *s, struct hunk *hunk)
strbuf_addstr(&s->colored,
plain[current] == '-' ?
- s->s.cfg.file_old_color :
+ s->cfg.file_old_color :
plain[current] == '+' ?
- s->s.cfg.file_new_color :
- s->s.cfg.context_color);
+ s->cfg.file_new_color :
+ s->cfg.context_color);
strbuf_add(&s->colored, plain + current, eol - current);
- strbuf_addstr(&s->colored, s->s.cfg.reset_color);
+ strbuf_addstr(&s->colored, s->cfg.reset_color);
if (next > eol)
strbuf_add(&s->colored, plain + eol, next - eol);
current = next;
@@ -1322,7 +1322,7 @@ static int run_apply_check(struct add_p_state *s,
static int read_single_character(struct add_p_state *s)
{
- if (s->s.cfg.use_single_key) {
+ if (s->cfg.use_single_key) {
int res = read_key_without_echo(&s->answer);
printf("%s\n", res == EOF ? "" : s->answer.buf);
return res;
@@ -1336,7 +1336,7 @@ static int read_single_character(struct add_p_state *s)
static int prompt_yesno(struct add_p_state *s, const char *prompt)
{
for (;;) {
- color_fprintf(stdout, s->s.cfg.prompt_color, "%s", _(prompt));
+ color_fprintf(stdout, s->cfg.prompt_color, "%s", _(prompt));
fflush(stdout);
if (read_single_character(s) == EOF)
return -1;
@@ -1617,15 +1617,15 @@ static int patch_update_file(struct add_p_state *s,
else
prompt_mode_type = PROMPT_HUNK;
- printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->s.cfg.prompt_color,
+ printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->cfg.prompt_color,
(uintmax_t)hunk_index + 1,
(uintmax_t)(file_diff->hunk_nr
? file_diff->hunk_nr
: 1));
printf(_(s->mode->prompt_mode[prompt_mode_type]),
s->buf.buf);
- if (*s->s.cfg.reset_color)
- fputs(s->s.cfg.reset_color, stdout);
+ if (*s->cfg.reset_color)
+ fputs(s->cfg.reset_color, stdout);
fflush(stdout);
if (read_single_character(s) == EOF)
break;
@@ -1782,7 +1782,7 @@ static int patch_update_file(struct add_p_state *s,
err(s, _("Sorry, cannot split this hunk"));
} else if (!split_hunk(s, file_diff,
hunk - file_diff->hunk)) {
- color_fprintf_ln(stdout, s->s.cfg.header_color,
+ color_fprintf_ln(stdout, s->cfg.header_color,
_("Split into %d hunks."),
(int)splittable_into);
rendered_hunk_index = -1;
@@ -1800,7 +1800,7 @@ static int patch_update_file(struct add_p_state *s,
} else if (s->answer.buf[0] == '?') {
const char *p = _(help_patch_remainder), *eol = p;
- color_fprintf(stdout, s->s.cfg.help_color, "%s",
+ color_fprintf(stdout, s->cfg.help_color, "%s",
_(s->mode->help_patch_text));
/*
@@ -1818,7 +1818,7 @@ static int patch_update_file(struct add_p_state *s,
if (*p != '?' && !strchr(s->buf.buf, *p))
continue;
- color_fprintf_ln(stdout, s->s.cfg.help_color,
+ color_fprintf_ln(stdout, s->cfg.help_color,
"%.*s", (int)(eol - p), p);
}
} else {
@@ -1838,7 +1838,7 @@ static int patch_update_file(struct add_p_state *s,
strbuf_reset(&s->buf);
reassemble_patch(s, file_diff, 0, &s->buf);
- discard_index(s->s.r->index);
+ discard_index(s->r->index);
if (s->mode->apply_for_checkout)
apply_for_checkout(s, &s->buf,
s->mode->is_reverse);
@@ -1849,8 +1849,8 @@ static int patch_update_file(struct add_p_state *s,
NULL, 0, NULL, 0))
error(_("'git apply' failed"));
}
- if (repo_read_index(s->s.r) >= 0)
- repo_refresh_and_write_index(s->s.r, REFRESH_QUIET, 0,
+ if (repo_read_index(s->r) >= 0)
+ repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,
1, NULL, NULL, NULL);
}
@@ -1863,11 +1863,15 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
const struct pathspec *ps)
{
struct add_p_state s = {
- { r }, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
+ .r = r,
+ .answer = STRBUF_INIT,
+ .buf = STRBUF_INIT,
+ .plain = STRBUF_INIT,
+ .colored = STRBUF_INIT,
};
size_t i, binary_count = 0;
- init_add_i_state(&s.s, r, opts);
+ interactive_config_init(&s.cfg, r, opts);
if (mode == ADD_P_STASH)
s.mode = &patch_mode_stash;
--
2.51.0.308.g032396e0da.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH RFC v2 12/16] add-patch: remove dependency on "add-interactive" subsystem
2025-08-24 17:42 ` [PATCH RFC v2 12/16] add-patch: remove dependency on "add-interactive" subsystem Patrick Steinhardt
@ 2025-08-25 16:43 ` Junio C Hamano
0 siblings, 0 replies; 278+ messages in thread
From: Junio C Hamano @ 2025-08-25 16:43 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, D. Ben Knoble, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk
Patrick Steinhardt <ps@pks.im> writes:
> With the preceding commit we have split out interactive configuration
> that is used by both "git add -p" and "git add -i". But we still
> initialize that configuration in the "add -p" subsystem by calling
> `init_add_i_state()`, even though we only do so to initialize the
> interactive configuration as well as a repository pointer.
>
> Stop doing so and instead store and initialize the interactive
> configuration in `struct add_p_state` directly.
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> add-patch.c | 68 ++++++++++++++++++++++++++++++++-----------------------------
> 1 file changed, 36 insertions(+), 32 deletions(-)
Ahh, with the two steps, this and the previous one, my question is
answered. I like the shape of the code base at this step very much.
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH RFC v2 13/16] add-patch: add support for in-memory index patching
2025-08-24 17:42 ` [PATCH RFC v2 00/16] " Patrick Steinhardt
` (11 preceding siblings ...)
2025-08-24 17:42 ` [PATCH RFC v2 12/16] add-patch: remove dependency on "add-interactive" subsystem Patrick Steinhardt
@ 2025-08-24 17:42 ` Patrick Steinhardt
2025-08-24 17:42 ` [PATCH RFC v2 14/16] wt-status: provide function to expose status for trees Patrick Steinhardt
` (3 subsequent siblings)
16 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-24 17:42 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
With `run_add_p()` callers have the ability to apply changes from a
specific revision to a repository's index. This infra supports several
different modes, like for example applying changes to the index,
worktree or both.
One feature that is missing though is the ability to apply changes to an
in-memory index different from the repository's index. Add a new
function `run_add_p_index()` to plug this gap.
This new function will be used in a subsequent commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
add-patch.c | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
add-patch.h | 8 +++++
2 files changed, 115 insertions(+), 3 deletions(-)
diff --git a/add-patch.c b/add-patch.c
index 1bcbc91de9..2a72c7b931 100644
--- a/add-patch.c
+++ b/add-patch.c
@@ -4,11 +4,13 @@
#include "git-compat-util.h"
#include "add-patch.h"
#include "advice.h"
+#include "commit.h"
#include "config.h"
#include "diff.h"
#include "editor.h"
#include "environment.h"
#include "gettext.h"
+#include "hex.h"
#include "object-name.h"
#include "pager.h"
#include "read-cache-ll.h"
@@ -263,6 +265,8 @@ struct hunk {
struct add_p_state {
struct repository *r;
+ struct index_state *index;
+ const char *index_file;
struct interactive_config cfg;
struct strbuf answer, buf;
@@ -414,7 +418,7 @@ static void setup_child_process(struct add_p_state *s,
cp->git_cmd = 1;
strvec_pushf(&cp->env,
- INDEX_ENVIRONMENT "=%s", s->r->index_file);
+ INDEX_ENVIRONMENT "=%s", s->index_file);
}
static int parse_range(const char **p,
@@ -1838,7 +1842,7 @@ static int patch_update_file(struct add_p_state *s,
strbuf_reset(&s->buf);
reassemble_patch(s, file_diff, 0, &s->buf);
- discard_index(s->r->index);
+ discard_index(s->index);
if (s->mode->apply_for_checkout)
apply_for_checkout(s, &s->buf,
s->mode->is_reverse);
@@ -1849,9 +1853,11 @@ static int patch_update_file(struct add_p_state *s,
NULL, 0, NULL, 0))
error(_("'git apply' failed"));
}
- if (repo_read_index(s->r) >= 0)
+ if (read_index_from(s->index, s->index_file, s->r->gitdir) >= 0 &&
+ s->index == s->r->index) {
repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,
1, NULL, NULL, NULL);
+ }
}
putchar('\n');
@@ -1864,6 +1870,8 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
{
struct add_p_state s = {
.r = r,
+ .index = r->index,
+ .index_file = r->index_file,
.answer = STRBUF_INIT,
.buf = STRBUF_INIT,
.plain = STRBUF_INIT,
@@ -1922,3 +1930,99 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
add_p_state_clear(&s);
return 0;
}
+
+int run_add_p_index(struct repository *r,
+ struct index_state *index,
+ const char *index_file,
+ struct interactive_options *opts,
+ const char *revision,
+ const struct pathspec *ps)
+{
+ struct patch_mode mode = {
+ .apply_args = { "--cached", NULL },
+ .apply_check_args = { "--cached", NULL },
+ .prompt_mode = {
+ N_("Stage mode change [y,n,q,a,d%s,?]? "),
+ N_("Stage deletion [y,n,q,a,d%s,?]? "),
+ N_("Stage addition [y,n,q,a,d%s,?]? "),
+ N_("Stage this hunk [y,n,q,a,d%s,?]? ")
+ },
+ .edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk "
+ "will immediately be marked for staging."),
+ .help_patch_text =
+ N_("y - stage this hunk\n"
+ "n - do not stage this hunk\n"
+ "q - quit; do not stage this hunk or any of the remaining "
+ "ones\n"
+ "a - stage this hunk and all later hunks in the file\n"
+ "d - do not stage this hunk or any of the later hunks in "
+ "the file\n"),
+ .index_only = 1,
+ };
+ struct add_p_state s = {
+ .r = r,
+ .index = index,
+ .index_file = index_file,
+ .answer = STRBUF_INIT,
+ .buf = STRBUF_INIT,
+ .plain = STRBUF_INIT,
+ .colored = STRBUF_INIT,
+ .mode = &mode,
+ .revision = revision,
+ };
+ struct strbuf parent_revision = STRBUF_INIT;
+ char parent_tree_oid[GIT_MAX_HEXSZ + 1];
+ size_t binary_count = 0;
+ struct commit *commit;
+ int ret;
+
+ commit = lookup_commit_reference_by_name(revision);
+ if (!commit) {
+ err(&s, _("Revision does not refer to a commit"));
+ ret = -1;
+ goto out;
+ }
+
+ if (commit->parents)
+ oid_to_hex_r(parent_tree_oid, get_commit_tree_oid(commit->parents->item));
+ else
+ oid_to_hex_r(parent_tree_oid, r->hash_algo->empty_tree);
+
+ strbuf_addf(&parent_revision, "%s~", revision);
+ mode.diff_cmd[0] = "diff-tree";
+ mode.diff_cmd[1] = "-r";
+ mode.diff_cmd[2] = parent_tree_oid;
+
+ interactive_config_init(&s.cfg, r, opts);
+
+ if (parse_diff(&s, ps) < 0) {
+ ret = -1;
+ goto out;
+ }
+
+ for (size_t i = 0; i < s.file_diff_nr; i++) {
+ if (s.file_diff[i].binary && !s.file_diff[i].hunk_nr)
+ binary_count++;
+ else if (patch_update_file(&s, s.file_diff + i))
+ break;
+ }
+
+ if (s.file_diff_nr == 0) {
+ err(&s, _("No changes."));
+ ret = -1;
+ goto out;
+ }
+
+ if (binary_count == s.file_diff_nr) {
+ err(&s, _("Only binary files changed."));
+ ret = -1;
+ goto out;
+ }
+
+ ret = 0;
+
+out:
+ strbuf_release(&parent_revision);
+ add_p_state_clear(&s);
+ return ret;
+}
diff --git a/add-patch.h b/add-patch.h
index 51c0d7bce9..d0edfec936 100644
--- a/add-patch.h
+++ b/add-patch.h
@@ -3,6 +3,7 @@
#include "color.h"
+struct index_state;
struct pathspec;
struct repository;
@@ -50,4 +51,11 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
struct interactive_options *opts, const char *revision,
const struct pathspec *ps);
+int run_add_p_index(struct repository *r,
+ struct index_state *index,
+ const char *index_file,
+ struct interactive_options *opts,
+ const char *revision,
+ const struct pathspec *ps);
+
#endif
--
2.51.0.308.g032396e0da.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH RFC v2 14/16] wt-status: provide function to expose status for trees
2025-08-24 17:42 ` [PATCH RFC v2 00/16] " Patrick Steinhardt
` (12 preceding siblings ...)
2025-08-24 17:42 ` [PATCH RFC v2 13/16] add-patch: add support for in-memory index patching Patrick Steinhardt
@ 2025-08-24 17:42 ` Patrick Steinhardt
2025-08-24 17:42 ` [PATCH RFC v2 15/16] builtin/history: implement "split" subcommand Patrick Steinhardt
` (2 subsequent siblings)
16 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-24 17:42 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
The "wt-status" subsystem is responsible for printing status information
around the current state of the working tree. This most importantly
includes information around whether the working tree or the index have
any changes.
We're about to introduce a new command though where the changes in
neither of them are actually relevant to us. Instead, what we want is to
format the changes between two different trees. While it is a little bit
of a stretch to add this as functionality to _working tree_ status, it
doesn't make any sense to open-code this functionality, either.
Implement a new function `wt_status_collect_changes_trees()` that diffs
two trees and formats the status accordingly. This function is not yet
used, but will be in a subsequent commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
wt-status.c | 24 ++++++++++++++++++++++++
wt-status.h | 3 +++
2 files changed, 27 insertions(+)
diff --git a/wt-status.c b/wt-status.c
index 454601afa15..f09309d12e3 100644
--- a/wt-status.c
+++ b/wt-status.c
@@ -612,6 +612,30 @@ static void wt_status_collect_updated_cb(struct diff_queue_struct *q,
}
}
+void wt_status_collect_changes_trees(struct wt_status *s,
+ const struct object_id *old_treeish,
+ const struct object_id *new_treeish)
+{
+ struct diff_options opts = { 0 };
+
+ repo_diff_setup(s->repo, &opts);
+ opts.output_format = DIFF_FORMAT_CALLBACK;
+ opts.format_callback = wt_status_collect_updated_cb;
+ opts.format_callback_data = s;
+ opts.detect_rename = s->detect_rename >= 0 ? s->detect_rename : opts.detect_rename;
+ opts.rename_limit = s->rename_limit >= 0 ? s->rename_limit : opts.rename_limit;
+ opts.rename_score = s->rename_score >= 0 ? s->rename_score : opts.rename_score;
+ opts.flags.recursive = 1;
+ diff_setup_done(&opts);
+
+ diff_tree_oid(old_treeish, new_treeish, "", &opts);
+ diffcore_std(&opts);
+ diff_flush(&opts);
+ wt_status_get_state(s->repo, &s->state, 0);
+
+ diff_free(&opts);
+}
+
static void wt_status_collect_changes_worktree(struct wt_status *s)
{
struct rev_info rev;
diff --git a/wt-status.h b/wt-status.h
index 4e377ce62b8..b262e345f79 100644
--- a/wt-status.h
+++ b/wt-status.h
@@ -153,6 +153,9 @@ void wt_status_add_cut_line(struct wt_status *s);
void wt_status_prepare(struct repository *r, struct wt_status *s);
void wt_status_print(struct wt_status *s);
void wt_status_collect(struct wt_status *s);
+void wt_status_collect_changes_trees(struct wt_status *s,
+ const struct object_id *old_treeish,
+ const struct object_id *new_treeish);
/*
* Frees the buffers allocated by wt_status_collect.
*/
--
2.51.0.308.g032396e0da.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH RFC v2 15/16] builtin/history: implement "split" subcommand
2025-08-24 17:42 ` [PATCH RFC v2 00/16] " Patrick Steinhardt
` (13 preceding siblings ...)
2025-08-24 17:42 ` [PATCH RFC v2 14/16] wt-status: provide function to expose status for trees Patrick Steinhardt
@ 2025-08-24 17:42 ` Patrick Steinhardt
2025-08-24 18:03 ` Kristoffer Haugsbakk
2025-08-26 13:14 ` D. Ben Knoble
2025-08-24 17:42 ` [PATCH RFC v2 16/16] builtin/history: implement "reword" subcommand Patrick Steinhardt
2025-09-03 23:39 ` [PATCH RFC v2 00/16] Introduce git-history(1) command for easy history editing D. Ben Knoble
16 siblings, 2 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-24 17:42 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
It is quite a common use case that one wants to split up one commit into
multiple commits by moving parts of the changes of the original commit
out of it into a separate commit. This is quite an involved operation
though:
1. Identify the commit in question that is to be dropped.
2. Perform an interactive rebase on top of that commit's parent.
3. Modify the instruction sheet to "edit" the commit that is to be
split up.
4. Drop the commit via "git reset HEAD~".
5. Stage changes that should go into the first commit and commit it.
6. Stage changes that should go into the second commit and commit it.
7. Finalize the rebase.
This is quite complex, and overall I would claim that most people who
are not experts in Git would struggle with this flow.
Introduce a new "split" subcommand for git-history(1) to make this way
easier. All the user needs to do is to say `git history split $COMMIT`.
From hereon, Git asks the user which parts of the commit shall be moved
out into a separate commit and, once done, asks the user for the commit
message. Git then creates that split-out commit and applies the original
commit on top of it.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-history.adoc | 60 +++++++
builtin/history.c | 291 +++++++++++++++++++++++++++++++
t/meson.build | 1 +
t/t3453-history-split.sh | 387 +++++++++++++++++++++++++++++++++++++++++
4 files changed, 739 insertions(+)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index b36cd925dd..6f0c64b90e 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -13,6 +13,7 @@ git history continue
git history quit
git history drop <commit>
git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)
+git history split [<options>] <commit> [--] [<pathspec>...]
DESCRIPTION
-----------
@@ -52,6 +53,26 @@ child commits, as that would lead to an empty branch.
be related to one another and must be reachable from the current `HEAD`
commit.
+`split [--message=<message>] <commit> [--] [<pathspec>...]`::
+ Interactively split up <commit> into two commits by choosing
+ hunks introduced by it that will be moved into the new split-out
+ commit. These hunks will then be written into a new commit that
+ becomes the parent of the previous commit. The original commit
+ stays intact, except that its parent will be the newly split-out
+ commit.
++
+The commit message of the new commit will be asked for by launching the
+configured editor, unless it has been specified with the `-m` option.
+Authorship of the commit will be the same as for the original commit.
++
+If passed, _<pathspec>_ can be used to limit which changes shall be split out
+of the original commit. Files not matching any of the pathspecs will remain
+part of the original commit. For more details, see the 'pathspec' entry in
+linkgit:gitglossary[7].
++
+It is invalid to select either all or no hunks, as that would lead to
+one of the commits becoming empty.
+
The following commands are used to manage an interrupted history-rewriting
operation:
@@ -111,6 +132,45 @@ f44a46e third
bf7438d first
----------
+Split a commit
+~~~~~~~~~~~~~~
+
+----------
+$ git log --stat --oneline
+3f81232 (HEAD -> main) original
+ bar | 1 +
+ foo | 1 +
+ 2 files changed, 2 insertions(+)
+
+$ git history split HEAD --message="split-out commit"
+diff --git a/bar b/bar
+new file mode 100644
+index 0000000..5716ca5
+--- /dev/null
++++ b/bar
+@@ -0,0 +1 @@
++bar
+(1/1) Stage addition [y,n,q,a,d,e,p,?]? y
+
+diff --git a/foo b/foo
+new file mode 100644
+index 0000000..257cc56
+--- /dev/null
++++ b/foo
+@@ -0,0 +1 @@
++foo
+(1/1) Stage addition [y,n,q,a,d,e,p,?]? n
+
+$ git log --stat --oneline
+7cebe64 (HEAD -> main) original
+ foo | 1 +
+ 1 file changed, 1 insertion(+)
+d1582f3 split-out commit
+ bar | 1 +
+ 1 file changed, 1 insertion(+)
+----------
+
+
CONFIGURATION
-------------
diff --git a/builtin/history.c b/builtin/history.c
index 16b516856e..6d3f44152c 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -1,17 +1,27 @@
+/* Required for `comment_line_str`. */
+#define USE_THE_REPOSITORY_VARIABLE
+
#include "builtin.h"
#include "branch.h"
+#include "cache-tree.h"
#include "commit.h"
#include "commit-reach.h"
#include "config.h"
+#include "editor.h"
#include "environment.h"
#include "gettext.h"
#include "hex.h"
#include "object-name.h"
#include "parse-options.h"
+#include "path.h"
+#include "pathspec.h"
+#include "read-cache-ll.h"
#include "refs.h"
#include "reset.h"
#include "revision.h"
+#include "run-command.h"
#include "sequencer.h"
+#include "sparse-index.h"
static int cmd_history_abort(int argc,
const char **argv,
@@ -517,6 +527,285 @@ static int cmd_history_reorder(int argc,
return ret;
}
+static void change_data_free(void *util, const char *str UNUSED)
+{
+ struct wt_status_change_data *d = util;
+ free(d->rename_source);
+ free(d);
+}
+
+static int fill_commit_message(struct repository *repo,
+ const struct object_id *old_tree,
+ const struct object_id *new_tree,
+ const char *default_message,
+ const char *provided_message,
+ const char *action,
+ struct strbuf *out)
+{
+ if (!provided_message) {
+ struct wt_status s;
+ const char *path = git_path_commit_editmsg();
+ const char *hint =
+ _("Please enter the commit message for the %s changes. Lines starting\n"
+ "with '%s' will be kept; you may remove them yourself if you want to.\n");
+
+ strbuf_addstr(out, default_message);
+ strbuf_addch(out, '\n');
+ strbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);
+ write_file_buf(path, out->buf, out->len);
+
+ wt_status_prepare(repo, &s);
+ FREE_AND_NULL(s.branch);
+ s.ahead_behind_flags = AHEAD_BEHIND_QUICK;
+ s.commit_template = 1;
+ s.colopts = 0;
+ s.display_comment_prefix = 1;
+ s.hints = 0;
+ s.use_color = 0;
+ s.whence = FROM_COMMIT;
+ s.committable = 1;
+
+ s.fp = fopen(git_path_commit_editmsg(), "a");
+ if (!s.fp)
+ return error_errno(_("could not open '%s'"), git_path_commit_editmsg());
+
+ wt_status_collect_changes_trees(&s, old_tree, new_tree);
+ wt_status_print(&s);
+ wt_status_collect_free_buffers(&s);
+ string_list_clear_func(&s.change, change_data_free);
+
+ strbuf_reset(out);
+ if (launch_editor(path, out, NULL)) {
+ fprintf(stderr, _("Please supply the message using either -m or -F option.\n"));
+ return -1;
+ }
+ strbuf_stripspace(out, comment_line_str);
+
+ } else {
+ strbuf_addstr(out, provided_message);
+ }
+
+ cleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);
+
+ if (!out->len) {
+ fprintf(stderr, _("Aborting commit due to empty commit message.\n"));
+ return -1;
+ }
+
+ return 0;
+}
+
+static int split_commit(struct repository *repo,
+ struct commit *original_commit,
+ struct pathspec *pathspec,
+ const char *commit_message,
+ struct object_id *out)
+{
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
+ struct strbuf index_file = STRBUF_INIT, split_message = STRBUF_INIT;
+ struct child_process read_tree_cmd = CHILD_PROCESS_INIT;
+ struct index_state index = INDEX_STATE_INIT(repo);
+ struct object_id original_commit_tree_oid, parent_tree_oid;
+ const char *original_message, *original_body, *ptr;
+ char original_commit_oid[GIT_MAX_HEXSZ + 1];
+ char *original_author = NULL;
+ struct commit_list *parents = NULL;
+ struct commit *first_commit;
+ struct tree *split_tree;
+ size_t len;
+ int ret;
+
+ if (original_commit->parents)
+ parent_tree_oid = *get_commit_tree_oid(original_commit->parents->item);
+ else
+ oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
+ original_commit_tree_oid = *get_commit_tree_oid(original_commit);
+
+ /*
+ * Construct the first commit. This is done by taking the original
+ * commit parent's tree and selectively patching changes from the diff
+ * between that parent and its child.
+ */
+ repo_git_path_replace(repo, &index_file, "%s", "history-split.index");
+
+ read_tree_cmd.git_cmd = 1;
+ strvec_pushf(&read_tree_cmd.env, "GIT_INDEX_FILE=%s", index_file.buf);
+ strvec_push(&read_tree_cmd.args, "read-tree");
+ strvec_push(&read_tree_cmd.args, oid_to_hex(&parent_tree_oid));
+ ret = run_command(&read_tree_cmd);
+ if (ret < 0)
+ goto out;
+
+ ret = read_index_from(&index, index_file.buf, repo->gitdir);
+ if (ret < 0) {
+ ret = error(_("failed reading temporary index"));
+ goto out;
+ }
+
+ oid_to_hex_r(original_commit_oid, &original_commit->object.oid);
+ ret = run_add_p_index(repo, &index, index_file.buf, &interactive_opts,
+ original_commit_oid, pathspec);
+ if (ret < 0)
+ goto out;
+
+ split_tree = write_in_core_index_as_tree(repo, &index);
+ if (!split_tree) {
+ ret = error(_("failed split tree"));
+ goto out;
+ }
+
+ unlink(index_file.buf);
+
+ /*
+ * We disallow the cases where either the split-out commit or the
+ * original commit would become empty. Consequently, if we see that the
+ * new tree ID matches either of those trees we abort.
+ */
+ if (oideq(&split_tree->object.oid, &parent_tree_oid)) {
+ ret = error(_("split commit is empty"));
+ goto out;
+ } else if (oideq(&split_tree->object.oid, &original_commit_tree_oid)) {
+ ret = error(_("split commit tree matches original commit"));
+ goto out;
+ }
+
+ /* We retain authorship of the original commit. */
+ original_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);
+ ptr = find_commit_header(original_message, "author", &len);
+ if (ptr)
+ original_author = xmemdupz(ptr, len);
+
+ ret = fill_commit_message(repo, &parent_tree_oid, &split_tree->object.oid,
+ "", commit_message, "split-out", &split_message);
+ if (ret < 0)
+ goto out;
+
+ ret = commit_tree(split_message.buf, split_message.len, &split_tree->object.oid,
+ original_commit->parents, &out[0], original_author, NULL);
+ if (ret < 0) {
+ ret = error(_("failed writing split-out commit"));
+ goto out;
+ }
+
+ /*
+ * The second commit is much simpler to construct, as we can simply use
+ * the original commit details, except that we adjust its parent to be
+ * the newly split-out commit.
+ */
+ find_commit_subject(original_message, &original_body);
+ first_commit = lookup_commit_reference(repo, &out[0]);
+ commit_list_append(first_commit, &parents);
+
+ ret = commit_tree(original_body, strlen(original_body), &original_commit_tree_oid,
+ parents, &out[1], original_author, NULL);
+ if (ret < 0) {
+ ret = error(_("failed writing second commit"));
+ goto out;
+ }
+
+ ret = 0;
+
+out:
+ if (index_file.len)
+ unlink(index_file.buf);
+ strbuf_release(&split_message);
+ strbuf_release(&index_file);
+ free_commit_list(parents);
+ free(original_author);
+ release_index(&index);
+ return ret;
+}
+
+static int cmd_history_split(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo)
+{
+ const char * const usage[] = {
+ N_("git history split [<options>] <commit>"),
+ NULL,
+ };
+ const char *commit_message = NULL;
+ struct option options[] = {
+ OPT_STRING('m', "message", &commit_message, N_("message"), N_("commit message")),
+ OPT_END(),
+ };
+ struct commit *original_commit, *head;
+ struct strvec commits = STRVEC_INIT;
+ struct commit_list *list = NULL;
+ struct object_id split_commits[2];
+ struct pathspec pathspec = { 0 };
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc < 1) {
+ ret = error(_("command expects a revision"));
+ goto out;
+ }
+ repo_config(repo, git_default_config, NULL);
+
+ original_commit = lookup_commit_reference_by_name(argv[0]);
+ if (!original_commit) {
+ ret = error(_("commit to be split cannot be found: %s"), argv[0]);
+ goto out;
+ }
+
+ if (original_commit->parents && original_commit->parents->next) {
+ ret = error(_("commit to be split must not be a merge commit"));
+ goto out;
+ }
+
+ head = lookup_commit_reference_by_name("HEAD");
+ if (!head) {
+ ret = error(_("could not resolve HEAD to a commit"));
+ goto out;
+ }
+
+ commit_list_append(original_commit, &list);
+ if (!repo_is_descendant_of(repo, original_commit, list)) {
+ ret = error (_("split commit must be reachable from current HEAD commit"));
+ goto out;
+ }
+
+ parse_pathspec(&pathspec, 0,
+ PATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,
+ prefix, argv + 1);
+
+ /*
+ * Collect the list of commits that we'll have to reapply now already.
+ * This ensures that we'll abort early on in case the range of commits
+ * contains merges, which we do not yet handle.
+ */
+ ret = collect_commits(repo, original_commit->parents ? original_commit->parents->item : NULL,
+ head, &commits);
+ if (ret < 0)
+ goto out;
+
+ /*
+ * Then we split up the commit and replace the original commit with the
+ * new new ones.
+ */
+ ret = split_commit(repo, original_commit, &pathspec,
+ commit_message, split_commits);
+ if (ret < 0)
+ goto out;
+
+ replace_commits(&commits, &original_commit->object.oid,
+ split_commits, ARRAY_SIZE(split_commits));
+
+ ret = apply_commits(repo, &commits, head, original_commit, "split");
+ if (ret < 0)
+ goto out;
+
+ ret = 0;
+
+out:
+ clear_pathspec(&pathspec);
+ strvec_clear(&commits);
+ free_commit_list(list);
+ return ret;
+}
+
int cmd_history(int argc,
const char **argv,
const char *prefix,
@@ -528,6 +817,7 @@ int cmd_history(int argc,
N_("git history quit"),
N_("git history drop <commit>"),
N_("git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)"),
+ N_("git history split [<options>] <commit> [--] [<pathspec>...]"),
NULL,
};
parse_opt_subcommand_fn *fn = NULL;
@@ -537,6 +827,7 @@ int cmd_history(int argc,
OPT_SUBCOMMAND("quit", &fn, cmd_history_quit),
OPT_SUBCOMMAND("drop", &fn, cmd_history_drop),
OPT_SUBCOMMAND("reorder", &fn, cmd_history_reorder),
+ OPT_SUBCOMMAND("split", &fn, cmd_history_split),
OPT_END(),
};
diff --git a/t/meson.build b/t/meson.build
index 2bf7bcab5a..b3d33c8588 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -379,6 +379,7 @@ integration_tests = [
't3450-history.sh',
't3451-history-drop.sh',
't3452-history-reorder.sh',
+ 't3453-history-split.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
diff --git a/t/t3453-history-split.sh b/t/t3453-history-split.sh
new file mode 100755
index 0000000000..b053fc2f29
--- /dev/null
+++ b/t/t3453-history-split.sh
@@ -0,0 +1,387 @@
+#!/bin/sh
+
+test_description='tests for git-history split subcommand'
+
+. ./test-lib.sh
+
+set_fake_editor () {
+ write_script fake-editor.sh <<-\EOF &&
+ echo "split-out commit" >"$1"
+ EOF
+ test_set_editor "$(pwd)"/fake-editor.sh
+}
+
+expect_log () {
+ git log --format="%s" >actual &&
+ cat >expect &&
+ test_cmp expect actual
+}
+
+expect_tree_entries () {
+ git ls-tree --name-only "$1" >actual &&
+ cat >expect &&
+ test_cmp expect actual
+}
+
+test_expect_success 'refuses to work with merge commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ git branch branch &&
+ test_commit ours &&
+ git switch branch &&
+ test_commit theirs &&
+ git switch - &&
+ git merge theirs &&
+ test_must_fail git history split HEAD 2>err &&
+ test_grep "commit to be split must not be a merge commit" err &&
+ test_must_fail git history split HEAD~ 2>err &&
+ test_grep "cannot rearrange commit history with merges" err
+ )
+'
+
+test_expect_success 'refuses to work with changes in the worktree or index' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit initial &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ echo changed >bar &&
+ test_must_fail git history split -m message HEAD 2>err <<-EOF &&
+ y
+ n
+ EOF
+ test_grep "Your local changes to the following files would be overwritten" err &&
+
+ git add bar &&
+ test_must_fail git history split -m message HEAD 2>err <<-EOF &&
+ y
+ n
+ EOF
+ test_grep "Your local changes to the following files would be overwritten" err
+ )
+'
+
+test_expect_success 'can split up tip commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit initial &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ git symbolic-ref HEAD >expect &&
+ set_fake_editor &&
+ git history split HEAD <<-EOF &&
+ y
+ n
+ EOF
+ git symbolic-ref HEAD >actual &&
+ test_cmp expect actual &&
+
+ expect_log <<-EOF &&
+ split-me
+ split-out commit
+ initial
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ bar
+ initial.t
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ initial.t
+ EOF
+ )
+'
+
+test_expect_success 'can split up root commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m root &&
+ test_commit tip &&
+
+ set_fake_editor &&
+ git history split HEAD~ <<-EOF &&
+ y
+ n
+ EOF
+
+ expect_log <<-EOF &&
+ tip
+ root
+ split-out commit
+ EOF
+
+ expect_tree_entries HEAD~2 <<-EOF &&
+ bar
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ bar
+ foo
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ tip.t
+ EOF
+ )
+'
+
+test_expect_success 'can split up in-between commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit initial &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+ test_commit tip &&
+
+ set_fake_editor &&
+ git history split HEAD~ <<-EOF &&
+ y
+ n
+ EOF
+
+ expect_log <<-EOF &&
+ tip
+ split-me
+ split-out commit
+ initial
+ EOF
+
+ expect_tree_entries HEAD~2 <<-EOF &&
+ bar
+ initial.t
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ bar
+ foo
+ initial.t
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ initial.t
+ tip.t
+ EOF
+ )
+'
+
+test_expect_success 'can pick multiple hunks' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar baz foo qux &&
+ git add . &&
+ git commit -m split-me &&
+
+ git history split HEAD -m "split-out commit" <<-EOF &&
+ y
+ n
+ y
+ n
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ bar
+ foo
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ baz
+ foo
+ qux
+ EOF
+ )
+'
+
+
+test_expect_success 'can use only last hunk' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ git history split HEAD -m "split-out commit" <<-EOF &&
+ n
+ y
+ EOF
+
+ expect_log <<-EOF &&
+ split-me
+ split-out commit
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ foo
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ EOF
+ )
+'
+
+test_expect_success 'aborts with empty commit message' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ test_must_fail git history split HEAD -m "" <<-EOF 2>err &&
+ y
+ n
+ EOF
+ test_grep "Aborting commit due to empty commit message." err
+ )
+'
+
+test_expect_success 'can specify message via option' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ git history split HEAD -m "message option" <<-EOF &&
+ y
+ n
+ EOF
+
+ expect_log <<-EOF
+ split-me
+ message option
+ EOF
+ )
+'
+
+test_expect_success 'commit message editor sees split-out changes' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ write_script fake-editor.sh <<-\EOF &&
+ cp "$1" . &&
+ echo "some commit message" >>"$1"
+ EOF
+ test_set_editor "$(pwd)"/fake-editor.sh &&
+
+ git history split HEAD <<-EOF &&
+ y
+ n
+ EOF
+
+ cat >expect <<-EOF &&
+
+ # Please enter the commit message for the split-out changes. Lines starting
+ # with ${SQ}#${SQ} will be kept; you may remove them yourself if you want to.
+ # Changes to be committed:
+ # new file: bar
+ #
+ EOF
+ test_cmp expect COMMIT_EDITMSG &&
+
+ expect_log <<-EOF
+ split-me
+ some commit message
+ EOF
+ )
+'
+
+test_expect_success 'can use pathspec to limit what gets split' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ git history split HEAD -m "message option" -- foo <<-EOF &&
+ y
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ foo
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ EOF
+ )
+'
+
+test_expect_success 'refuses to create empty split-out commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ test_must_fail git history split HEAD 2>err <<-EOF &&
+ n
+ n
+ EOF
+ test_grep "split commit is empty" err
+ )
+'
+
+test_expect_success 'refuses to create empty original commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ test_must_fail git history split HEAD 2>err <<-EOF &&
+ y
+ y
+ EOF
+ test_grep "split commit tree matches original commit" err
+ )
+'
+
+test_done
--
2.51.0.308.g032396e0da.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH RFC v2 15/16] builtin/history: implement "split" subcommand
2025-08-24 17:42 ` [PATCH RFC v2 15/16] builtin/history: implement "split" subcommand Patrick Steinhardt
@ 2025-08-24 18:03 ` Kristoffer Haugsbakk
2025-09-03 12:20 ` Patrick Steinhardt
2025-08-26 13:14 ` D. Ben Knoble
1 sibling, 1 reply; 278+ messages in thread
From: Kristoffer Haugsbakk @ 2025-08-24 18:03 UTC (permalink / raw)
To: Patrick Steinhardt, git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk
On Sun, Aug 24, 2025, at 19:42, Patrick Steinhardt wrote:
> It is quite a common use case that one wants to split up one commit into
> multiple commits by moving parts of the changes of the original commit
> out of it into a separate commit. This is quite an involved operation
s/out of it into/out into/
> though:
>
> 1. Identify the commit in question that is to be dropped.
>
> 2. Perform an interactive rebase on top of that commit's parent.
>
> 3. Modify the instruction sheet to "edit" the commit that is to be
> split up.
>
> 4. Drop the commit via "git reset HEAD~".
>
> 5. Stage changes that should go into the first commit and commit it.
>
> 6. Stage changes that should go into the second commit and commit it.
>
> 7. Finalize the rebase.
Exactly right and this fills a conspicuous hole (in the rewriting parts
of git(1)).
>
> This is quite complex, and overall I would claim that most people who
> are not experts in Git would struggle with this flow.
>
> Introduce a new "split" subcommand for git-history(1) to make this way
> easier. All the user needs to do is to say `git history split $COMMIT`.
> From hereon, Git asks the user which parts of the commit shall be moved
> out into a separate commit and, once done, asks the user for the commit
> message. Git then creates that split-out commit and applies the original
> commit on top of it.
The interactive mode here seems just-right.
• Split in two, give the commit message for the new one
• I can use `git history split :/'The second batch'` if I want to split
a single commit multiple times
• I can use `git history reword :/'The second batch'` if I want
to change the original commit message as well
But it’s interactive-only, correct? Would it make sense for a stateful
split session so that other tools could be used to split the patch?
One nice thing about “the staging area” is that people can use whatever
tools they want for things like selectively including changes in a
commit. Likewise for a rebase session.
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC v2 15/16] builtin/history: implement "split" subcommand
2025-08-24 18:03 ` Kristoffer Haugsbakk
@ 2025-09-03 12:20 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-03 12:20 UTC (permalink / raw)
To: Kristoffer Haugsbakk
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk
On Sun, Aug 24, 2025 at 08:03:18PM +0200, Kristoffer Haugsbakk wrote:
> On Sun, Aug 24, 2025, at 19:42, Patrick Steinhardt wrote:
> > This is quite complex, and overall I would claim that most people who
> > are not experts in Git would struggle with this flow.
> >
> > Introduce a new "split" subcommand for git-history(1) to make this way
> > easier. All the user needs to do is to say `git history split $COMMIT`.
> > From hereon, Git asks the user which parts of the commit shall be moved
> > out into a separate commit and, once done, asks the user for the commit
> > message. Git then creates that split-out commit and applies the original
> > commit on top of it.
>
> The interactive mode here seems just-right.
>
> • Split in two, give the commit message for the new one
> • I can use `git history split :/'The second batch'` if I want to split
> a single commit multiple times
> • I can use `git history reword :/'The second batch'` if I want
> to change the original commit message as well
>
> But it’s interactive-only, correct? Would it make sense for a stateful
> split session so that other tools could be used to split the patch?
Yeah, it's interactive-only. I see git-history(1) as part of porcelain
rather than plumbing, so this is kind of intentional. I also wouldn't
quite know how this would look like to have a split session.
But if anyone has an idea how such a stateful split session might look
like I don't see a strong reason to not add this in the future. For now
though my focus is to get a baseline going that helps the user to
perform common tasks easier as opposed to before.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC v2 15/16] builtin/history: implement "split" subcommand
2025-08-24 17:42 ` [PATCH RFC v2 15/16] builtin/history: implement "split" subcommand Patrick Steinhardt
2025-08-24 18:03 ` Kristoffer Haugsbakk
@ 2025-08-26 13:14 ` D. Ben Knoble
2025-09-03 12:20 ` Patrick Steinhardt
1 sibling, 1 reply; 278+ messages in thread
From: D. Ben Knoble @ 2025-08-26 13:14 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, Junio C Hamano, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk
On Sun, Aug 24, 2025 at 1:44 PM Patrick Steinhardt <ps@pks.im> wrote:
> diff --git a/builtin/history.c b/builtin/history.c
> index 16b516856e..6d3f44152c 100644
> --- a/builtin/history.c
> +++ b/builtin/history.c
> @@ -517,6 +527,285 @@ static int cmd_history_reorder(int argc,
> return ret;
> }
>
> +static void change_data_free(void *util, const char *str UNUSED)
> +{
> + struct wt_status_change_data *d = util;
> + free(d->rename_source);
> + free(d);
> +}
> +
> +static int fill_commit_message(struct repository *repo,
> + const struct object_id *old_tree,
> + const struct object_id *new_tree,
> + const char *default_message,
> + const char *provided_message,
> + const char *action,
> + struct strbuf *out)
> +{
> + if (!provided_message) {
> + struct wt_status s;
> + const char *path = git_path_commit_editmsg();
> + const char *hint =
> + _("Please enter the commit message for the %s changes. Lines starting\n"
> + "with '%s' will be kept; you may remove them yourself if you want to.\n");
> +
> + strbuf_addstr(out, default_message);
> + strbuf_addch(out, '\n');
> + strbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);
> + write_file_buf(path, out->buf, out->len);
> +
> + wt_status_prepare(repo, &s);
> + FREE_AND_NULL(s.branch);
> + s.ahead_behind_flags = AHEAD_BEHIND_QUICK;
> + s.commit_template = 1;
> + s.colopts = 0;
> + s.display_comment_prefix = 1;
> + s.hints = 0;
> + s.use_color = 0;
> + s.whence = FROM_COMMIT;
> + s.committable = 1;
> +
> + s.fp = fopen(git_path_commit_editmsg(), "a");
> + if (!s.fp)
> + return error_errno(_("could not open '%s'"), git_path_commit_editmsg());
> +
> + wt_status_collect_changes_trees(&s, old_tree, new_tree);
> + wt_status_print(&s);
> + wt_status_collect_free_buffers(&s);
> + string_list_clear_func(&s.change, change_data_free);
I think I'm supposed to see the changes between the old and new trees,
right? Does this only happen if I use the interactive machinery to
edit a hunk? When I try accepting some changes and leaving others for
the next commit I get no diff in the template.
I did try to add new diff lines to a hunk, and nothing showed up…
maybe I'm holding it wrong? I'm pretty sure I compiled this version.
It doesn't look like it's triggered only on commit.verbose config, either.
> +
> + strbuf_reset(out);
> + if (launch_editor(path, out, NULL)) {
> + fprintf(stderr, _("Please supply the message using either -m or -F option.\n"));
According to the usage, git history split only supports -m, not -F ;)
> + return -1;
> + }
> + strbuf_stripspace(out, comment_line_str);
> +
> + } else {
> + strbuf_addstr(out, provided_message);
> + }
> +
> + cleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);
> +
> + if (!out->len) {
> + fprintf(stderr, _("Aborting commit due to empty commit message.\n"));
It _would_ be nice if this and similar errors left me able to "try
again" without losing staged changes—I think I mentioned this before,
though. And with the in-memory indices vs. actual working state,
presenting a UI here could be very difficult. So it's an
understandable choice.
--
D. Ben Knoble
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC v2 15/16] builtin/history: implement "split" subcommand
2025-08-26 13:14 ` D. Ben Knoble
@ 2025-09-03 12:20 ` Patrick Steinhardt
2025-09-03 21:55 ` D. Ben Knoble
0 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-03 12:20 UTC (permalink / raw)
To: D. Ben Knoble
Cc: git, Junio C Hamano, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk
On Tue, Aug 26, 2025 at 09:14:49AM -0400, D. Ben Knoble wrote:
> On Sun, Aug 24, 2025 at 1:44 PM Patrick Steinhardt <ps@pks.im> wrote:
> > diff --git a/builtin/history.c b/builtin/history.c
> > index 16b516856e..6d3f44152c 100644
> > --- a/builtin/history.c
> > +++ b/builtin/history.c
> > @@ -517,6 +527,285 @@ static int cmd_history_reorder(int argc,
[snip]
> > + wt_status_collect_changes_trees(&s, old_tree, new_tree);
> > + wt_status_print(&s);
> > + wt_status_collect_free_buffers(&s);
> > + string_list_clear_func(&s.change, change_data_free);
>
> I think I'm supposed to see the changes between the old and new trees,
> right? Does this only happen if I use the interactive machinery to
> edit a hunk? When I try accepting some changes and leaving others for
> the next commit I get no diff in the template.
Yeah, it's supposed to show the diff between old and new tree indeed. So
in theory you should see something.
> I did try to add new diff lines to a hunk, and nothing showed up…
> maybe I'm holding it wrong? I'm pretty sure I compiled this version.
Do you maybe have a reproducer for this? It seems to work alright for
me, but I wouldn't be surprised if there was a bug here. The wt-status
interfaces are quite something and I was tearing my hair while trying to
figure them out.
> It doesn't look like it's triggered only on commit.verbose config, either.
Fixed now.
> > +
> > + strbuf_reset(out);
> > + if (launch_editor(path, out, NULL)) {
> > + fprintf(stderr, _("Please supply the message using either -m or -F option.\n"));
>
> According to the usage, git history split only supports -m, not -F ;)
True. I didn't want to add too many options right from the start to keep
the series somewhat simple. We should eventually add it though.
> > + return -1;
> > + }
> > + strbuf_stripspace(out, comment_line_str);
> > +
> > + } else {
> > + strbuf_addstr(out, provided_message);
> > + }
> > +
> > + cleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);
> > +
> > + if (!out->len) {
> > + fprintf(stderr, _("Aborting commit due to empty commit message.\n"));
>
> It _would_ be nice if this and similar errors left me able to "try
> again" without losing staged changes—I think I mentioned this before,
> though. And with the in-memory indices vs. actual working state,
> presenting a UI here could be very difficult. So it's an
> understandable choice.
Yeah, I don't dare touching this yet, but certainly see that this might
be a worthwhile addition as we iterate on this command.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC v2 15/16] builtin/history: implement "split" subcommand
2025-09-03 12:20 ` Patrick Steinhardt
@ 2025-09-03 21:55 ` D. Ben Knoble
2025-09-04 12:57 ` Patrick Steinhardt
0 siblings, 1 reply; 278+ messages in thread
From: D. Ben Knoble @ 2025-09-03 21:55 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, Junio C Hamano, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk
On Wed, Sep 3, 2025 at 8:20 AM Patrick Steinhardt <ps@pks.im> wrote:
>
> On Tue, Aug 26, 2025 at 09:14:49AM -0400, D. Ben Knoble wrote:
> > On Sun, Aug 24, 2025 at 1:44 PM Patrick Steinhardt <ps@pks.im> wrote:
> > > diff --git a/builtin/history.c b/builtin/history.c
> > > index 16b516856e..6d3f44152c 100644
> > > --- a/builtin/history.c
> > > +++ b/builtin/history.c
> > > @@ -517,6 +527,285 @@ static int cmd_history_reorder(int argc,
> [snip]
> > > + wt_status_collect_changes_trees(&s, old_tree, new_tree);
> > > + wt_status_print(&s);
> > > + wt_status_collect_free_buffers(&s);
> > > + string_list_clear_func(&s.change, change_data_free);
> >
> > I think I'm supposed to see the changes between the old and new trees,
> > right? Does this only happen if I use the interactive machinery to
> > edit a hunk? When I try accepting some changes and leaving others for
> > the next commit I get no diff in the template.
>
> Yeah, it's supposed to show the diff between old and new tree indeed. So
> in theory you should see something.
>
> > I did try to add new diff lines to a hunk, and nothing showed up…
> > maybe I'm holding it wrong? I'm pretty sure I compiled this version.
>
> Do you maybe have a reproducer for this? It seems to work alright for
> me, but I wouldn't be surprised if there was a bug here. The wt-status
> interfaces are quite something and I was tearing my hair while trying to
> figure them out.
Hm. I have a copy of these patches at
https://github.com/benknoble/git/tree/ps-jj. After "make DEVELOPER=1
-j $(nproc)" on that branch, I did
bin-wrappers/git history split @~3
<input y,q> # once I even used "e" and added new diffs to the patch
<type commit message> # no status info
<exit editor>
Then it looks like the 2nd commit gets created automatically. Maybe
I'm just missing how this should work? Thanks for looking at it.
--
D. Ben Knoble
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC v2 15/16] builtin/history: implement "split" subcommand
2025-09-03 21:55 ` D. Ben Knoble
@ 2025-09-04 12:57 ` Patrick Steinhardt
2025-09-12 18:26 ` D. Ben Knoble
0 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-04 12:57 UTC (permalink / raw)
To: D. Ben Knoble
Cc: git, Junio C Hamano, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk
On Wed, Sep 03, 2025 at 05:55:28PM -0400, D. Ben Knoble wrote:
> On Wed, Sep 3, 2025 at 8:20 AM Patrick Steinhardt <ps@pks.im> wrote:
> >
> > On Tue, Aug 26, 2025 at 09:14:49AM -0400, D. Ben Knoble wrote:
> > > On Sun, Aug 24, 2025 at 1:44 PM Patrick Steinhardt <ps@pks.im> wrote:
> > > > diff --git a/builtin/history.c b/builtin/history.c
> > > > index 16b516856e..6d3f44152c 100644
> > > > --- a/builtin/history.c
> > > > +++ b/builtin/history.c
> > > > @@ -517,6 +527,285 @@ static int cmd_history_reorder(int argc,
> > [snip]
> > > > + wt_status_collect_changes_trees(&s, old_tree, new_tree);
> > > > + wt_status_print(&s);
> > > > + wt_status_collect_free_buffers(&s);
> > > > + string_list_clear_func(&s.change, change_data_free);
> > >
> > > I think I'm supposed to see the changes between the old and new trees,
> > > right? Does this only happen if I use the interactive machinery to
> > > edit a hunk? When I try accepting some changes and leaving others for
> > > the next commit I get no diff in the template.
> >
> > Yeah, it's supposed to show the diff between old and new tree indeed. So
> > in theory you should see something.
> >
> > > I did try to add new diff lines to a hunk, and nothing showed up…
> > > maybe I'm holding it wrong? I'm pretty sure I compiled this version.
> >
> > Do you maybe have a reproducer for this? It seems to work alright for
> > me, but I wouldn't be surprised if there was a bug here. The wt-status
> > interfaces are quite something and I was tearing my hair while trying to
> > figure them out.
>
> Hm. I have a copy of these patches at
> https://github.com/benknoble/git/tree/ps-jj. After "make DEVELOPER=1
> -j $(nproc)" on that branch, I did
>
> bin-wrappers/git history split @~3
> <input y,q> # once I even used "e" and added new diffs to the patch
> <type commit message> # no status info
> <exit editor>
>
> Then it looks like the 2nd commit gets created automatically. Maybe
> I'm just missing how this should work? Thanks for looking at it.
Weird. I used the exact same branch, command and input and did have
status information in my editor. Note that for now the editor only asks
for the commit message of the first commit. The second commit is
basically retained from the original: both tree and commit message are
the exact same, only difference is its parent.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC v2 15/16] builtin/history: implement "split" subcommand
2025-09-04 12:57 ` Patrick Steinhardt
@ 2025-09-12 18:26 ` D. Ben Knoble
2025-09-15 9:32 ` Patrick Steinhardt
0 siblings, 1 reply; 278+ messages in thread
From: D. Ben Knoble @ 2025-09-12 18:26 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, Junio C Hamano, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk
On Thu, Sep 4, 2025 at 8:57 AM Patrick Steinhardt <ps@pks.im> wrote:
>
> On Wed, Sep 03, 2025 at 05:55:28PM -0400, D. Ben Knoble wrote:
> > On Wed, Sep 3, 2025 at 8:20 AM Patrick Steinhardt <ps@pks.im> wrote:
> > >
> > > On Tue, Aug 26, 2025 at 09:14:49AM -0400, D. Ben Knoble wrote:
> > > > On Sun, Aug 24, 2025 at 1:44 PM Patrick Steinhardt <ps@pks.im> wrote:
> > > > > diff --git a/builtin/history.c b/builtin/history.c
> > > > > index 16b516856e..6d3f44152c 100644
> > > > > --- a/builtin/history.c
> > > > > +++ b/builtin/history.c
> > > > > @@ -517,6 +527,285 @@ static int cmd_history_reorder(int argc,
> > > [snip]
> > > > > + wt_status_collect_changes_trees(&s, old_tree, new_tree);
> > > > > + wt_status_print(&s);
> > > > > + wt_status_collect_free_buffers(&s);
> > > > > + string_list_clear_func(&s.change, change_data_free);
> > > >
> > > > I think I'm supposed to see the changes between the old and new trees,
> > > > right? Does this only happen if I use the interactive machinery to
> > > > edit a hunk? When I try accepting some changes and leaving others for
> > > > the next commit I get no diff in the template.
> > >
> > > Yeah, it's supposed to show the diff between old and new tree indeed. So
> > > in theory you should see something.
> > >
> > > > I did try to add new diff lines to a hunk, and nothing showed up…
> > > > maybe I'm holding it wrong? I'm pretty sure I compiled this version.
> > >
> > > Do you maybe have a reproducer for this? It seems to work alright for
> > > me, but I wouldn't be surprised if there was a bug here. The wt-status
> > > interfaces are quite something and I was tearing my hair while trying to
> > > figure them out.
> >
> > Hm. I have a copy of these patches at
> > https://github.com/benknoble/git/tree/ps-jj. After "make DEVELOPER=1
> > -j $(nproc)" on that branch, I did
> >
> > bin-wrappers/git history split @~3
> > <input y,q> # once I even used "e" and added new diffs to the patch
> > <type commit message> # no status info
> > <exit editor>
> >
> > Then it looks like the 2nd commit gets created automatically. Maybe
> > I'm just missing how this should work? Thanks for looking at it.
>
> Weird. I used the exact same branch, command and input and did have
> status information in my editor.
Hm. I've pulled down v3, built it, and pushed to the same branch. The
tip is e91e23546b (builtin/history: implement "reword" subcommand,
2025-09-04).
Now, a heavier-handed recipe:
GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null bin-wrappers/git
-c commit.verbose=true history split @~5
<input y,q> # once I even used "e" and added new diffs to the patch
I see the usual instructions:
# Please enter the commit message for the split-out changes. Lines starting
# with '#' will be kept; you may remove them yourself if you want to.
# Modifications qui seront validées :
# modifié : add-patch.c
#
And finally I might know what happened, ha… if the "modified files" is
the status information, then it has been there all along! Meanwhile, I
was expecting a _diff_.
I'm actually _still_ expecting a diff with v3 and commit.verbose set,
but I apologize if I've led you down a wild goose chase for the rest
:)
> Note that for now the editor only asks
> for the commit message of the first commit. The second commit is
> basically retained from the original: both tree and commit message are
> the exact same, only difference is its parent.
Sensible, though I might agree that the 2nd message should be edited
(mentioned elsewhere).
--
D. Ben Knoble
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC v2 15/16] builtin/history: implement "split" subcommand
2025-09-12 18:26 ` D. Ben Knoble
@ 2025-09-15 9:32 ` Patrick Steinhardt
2025-09-15 13:04 ` Ben Knoble
0 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-15 9:32 UTC (permalink / raw)
To: D. Ben Knoble
Cc: git, Junio C Hamano, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk
On Fri, Sep 12, 2025 at 02:26:04PM -0400, D. Ben Knoble wrote:
> Hm. I've pulled down v3, built it, and pushed to the same branch. The
> tip is e91e23546b (builtin/history: implement "reword" subcommand,
> 2025-09-04).
>
> Now, a heavier-handed recipe:
>
> GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null bin-wrappers/git
> -c commit.verbose=true history split @~5
> <input y,q> # once I even used "e" and added new diffs to the patch
>
> I see the usual instructions:
>
> # Please enter the commit message for the split-out changes. Lines starting
> # with '#' will be kept; you may remove them yourself if you want to.
> # Modifications qui seront validées :
> # modifié : add-patch.c
> #
>
> And finally I might know what happened, ha… if the "modified files" is
> the status information, then it has been there all along! Meanwhile, I
> was expecting a _diff_.
>
> I'm actually _still_ expecting a diff with v3 and commit.verbose set,
> but I apologize if I've led you down a wild goose chase for the rest
> :)
Oh! I didn't even know this was a thing Git supports, I've never seen it
before. For the sake of simplicity I'd propose to keep this as-is for
now, but to amend that mode once the initial couple of patches have
landed.
Would that be fine with you?
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC v2 15/16] builtin/history: implement "split" subcommand
2025-09-15 9:32 ` Patrick Steinhardt
@ 2025-09-15 13:04 ` Ben Knoble
0 siblings, 0 replies; 278+ messages in thread
From: Ben Knoble @ 2025-09-15 13:04 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, Junio C Hamano, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk
> Le 15 sept. 2025 à 05:32, Patrick Steinhardt <ps@pks.im> a écrit :
>
> On Fri, Sep 12, 2025 at 02:26:04PM -0400, D. Ben Knoble wrote:
>> Hm. I've pulled down v3, built it, and pushed to the same branch. The
>> tip is e91e23546b (builtin/history: implement "reword" subcommand,
>> 2025-09-04).
>>
>> Now, a heavier-handed recipe:
>>
>> GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null bin-wrappers/git
>> -c commit.verbose=true history split @~5
>> <input y,q> # once I even used "e" and added new diffs to the patch
>>
>> I see the usual instructions:
>>
>> # Please enter the commit message for the split-out changes. Lines starting
>> # with '#' will be kept; you may remove them yourself if you want to.
>> # Modifications qui seront validées :
>> # modifié : add-patch.c
>> #
>>
>> And finally I might know what happened, ha… if the "modified files" is
>> the status information, then it has been there all along! Meanwhile, I
>> was expecting a _diff_.
>>
>> I'm actually _still_ expecting a diff with v3 and commit.verbose set,
>> but I apologize if I've led you down a wild goose chase for the rest
>> :)
>
> Oh! I didn't even know this was a thing Git supports, I've never seen it
> before. For the sake of simplicity I'd propose to keep this as-is for
> now, but to amend that mode once the initial couple of patches have
> landed.
>
> Would that be fine with you?
Sure! My apologies again for the « commit.verbose » red herring.
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH RFC v2 16/16] builtin/history: implement "reword" subcommand
2025-08-24 17:42 ` [PATCH RFC v2 00/16] " Patrick Steinhardt
` (14 preceding siblings ...)
2025-08-24 17:42 ` [PATCH RFC v2 15/16] builtin/history: implement "split" subcommand Patrick Steinhardt
@ 2025-08-24 17:42 ` Patrick Steinhardt
2025-08-24 18:08 ` Kristoffer Haugsbakk
2025-09-03 23:39 ` [PATCH RFC v2 00/16] Introduce git-history(1) command for easy history editing D. Ben Knoble
16 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-08-24 17:42 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
Implement a new "reword" subcommand for git-history(1). This subcommand
is essentially the same as if a user performed an interactive rebase
with a single commit changed to use the "reword" verb.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-history.adoc | 5 ++
builtin/history.c | 96 +++++++++++++++++++++++++
t/meson.build | 1 +
t/t3454-history-reword.sh | 158 +++++++++++++++++++++++++++++++++++++++++
4 files changed, 260 insertions(+)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index 6f0c64b90e..cbbcef3582 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -13,6 +13,7 @@ git history continue
git history quit
git history drop <commit>
git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)
+git history reword [<options>] <commit>
git history split [<options>] <commit> [--] [<pathspec>...]
DESCRIPTION
@@ -53,6 +54,10 @@ child commits, as that would lead to an empty branch.
be related to one another and must be reachable from the current `HEAD`
commit.
+`reword <commit> [--message=<message>]`::
+ Rewrite the commit message of the specified commit. All the other
+ details of this commit remain unchanged.
+
`split [--message=<message>] <commit> [--] [<pathspec>...]`::
Interactively split up <commit> into two commits by choosing
hunks introduced by it that will be moved into the new split-out
diff --git a/builtin/history.c b/builtin/history.c
index 6d3f44152c..cdc93a1cbd 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -716,6 +716,100 @@ static int split_commit(struct repository *repo,
return ret;
}
+static int cmd_history_reword(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo)
+{
+ const char * const usage[] = {
+ N_("git history reword [<options>] <commit>"),
+ NULL,
+ };
+ const char *commit_message = NULL;
+ struct option options[] = {
+ OPT_STRING('m', "message", &commit_message, N_("message"), N_("commit message")),
+ OPT_END(),
+ };
+ struct strbuf final_message = STRBUF_INIT;
+ struct commit *original_commit, *head;
+ struct strvec commits = STRVEC_INIT;
+ struct object_id parent_tree_oid, original_commit_tree_oid;
+ struct object_id rewritten_commit;
+ const char *original_message, *original_body, *ptr;
+ char *original_author = NULL;
+ size_t len;
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc != 1) {
+ ret = error(_("command expects a single revision"));
+ goto out;
+ }
+ repo_config(repo, git_default_config, NULL);
+
+ original_commit = lookup_commit_reference_by_name(argv[0]);
+ if (!original_commit) {
+ ret = error(_("commit to be split cannot be found: %s"), argv[0]);
+ goto out;
+ }
+
+ head = lookup_commit_reference_by_name("HEAD");
+ if (!head) {
+ ret = error(_("could not resolve HEAD to a commit"));
+ goto out;
+ }
+
+ /*
+ * Collect the list of commits that we'll have to reapply now already.
+ * This ensures that we'll abort early on in case the range of commits
+ * contains merges, which we do not yet handle.
+ */
+ ret = collect_commits(repo, original_commit->parents ? original_commit->parents->item : NULL,
+ head, &commits);
+ if (ret < 0)
+ goto out;
+
+ /* We retain authorship of the original commit. */
+ original_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);
+ ptr = find_commit_header(original_message, "author", &len);
+ if (ptr)
+ original_author = xmemdupz(ptr, len);
+ find_commit_subject(original_message, &original_body);
+
+ if (original_commit->parents)
+ parent_tree_oid = *get_commit_tree_oid(original_commit->parents->item);
+ else
+ oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
+ original_commit_tree_oid = *get_commit_tree_oid(original_commit);
+
+ ret = fill_commit_message(repo, &parent_tree_oid, &original_commit_tree_oid,
+ original_body, commit_message, "reworded", &final_message);
+ if (ret < 0)
+ goto out;
+
+ ret = commit_tree(final_message.buf, final_message.len,
+ &repo_get_commit_tree(repo, original_commit)->object.oid,
+ original_commit->parents, &rewritten_commit, original_author, NULL);
+ if (ret < 0) {
+ ret = error(_("failed writing reworded commit"));
+ goto out;
+ }
+
+ replace_commits(&commits, &original_commit->object.oid, &rewritten_commit, 1);
+
+ ret = apply_commits(repo, &commits, head, original_commit, "reword");
+ if (ret < 0)
+ goto out;
+
+ ret = 0;
+
+out:
+ strbuf_release(&final_message);
+ strvec_clear(&commits);
+ free(original_author);
+ return ret;
+}
+
static int cmd_history_split(int argc,
const char **argv,
const char *prefix,
@@ -817,6 +911,7 @@ int cmd_history(int argc,
N_("git history quit"),
N_("git history drop <commit>"),
N_("git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)"),
+ N_("git history reword [<options>] <commit>"),
N_("git history split [<options>] <commit> [--] [<pathspec>...]"),
NULL,
};
@@ -827,6 +922,7 @@ int cmd_history(int argc,
OPT_SUBCOMMAND("quit", &fn, cmd_history_quit),
OPT_SUBCOMMAND("drop", &fn, cmd_history_drop),
OPT_SUBCOMMAND("reorder", &fn, cmd_history_reorder),
+ OPT_SUBCOMMAND("reword", &fn, cmd_history_reword),
OPT_SUBCOMMAND("split", &fn, cmd_history_split),
OPT_END(),
};
diff --git a/t/meson.build b/t/meson.build
index b3d33c8588..948223f453 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -380,6 +380,7 @@ integration_tests = [
't3451-history-drop.sh',
't3452-history-reorder.sh',
't3453-history-split.sh',
+ 't3454-history-reword.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
diff --git a/t/t3454-history-reword.sh b/t/t3454-history-reword.sh
new file mode 100755
index 0000000000..9822c0336a
--- /dev/null
+++ b/t/t3454-history-reword.sh
@@ -0,0 +1,158 @@
+#!/bin/sh
+
+test_description='tests for git-history reword subcommand'
+
+. ./test-lib.sh
+
+test_expect_success 'refuses to work with merge commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ git branch branch &&
+ test_commit ours &&
+ git switch branch &&
+ test_commit theirs &&
+ git switch - &&
+ git merge theirs &&
+ test_must_fail git history reword HEAD~ 2>err &&
+ test_grep "cannot rearrange commit history with merges" err &&
+ test_must_fail git history reword HEAD 2>err &&
+ test_grep "cannot rearrange commit history with merges" err
+ )
+'
+
+test_expect_success 'refuses to work with changes in the worktree or index' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base file &&
+ echo foo >file &&
+ test_must_fail git history reword HEAD 2>err &&
+ test_grep "Your local changes to the following files would be overwritten" err &&
+ git add file &&
+ test_must_fail git history reword HEAD 2>err &&
+ test_grep "Your local changes to the following files would be overwritten" err
+ )
+'
+
+test_expect_success 'can reword tip of a branch' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ git symbolic-ref HEAD >expect &&
+ git history reword -m "third reworded" HEAD &&
+ git symbolic-ref HEAD >actual &&
+ test_cmp expect actual &&
+
+ cat >expect <<-EOF &&
+ third reworded
+ second
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'can reword commit in the middle' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ git symbolic-ref HEAD >expect &&
+ git history reword -m "second reworded" HEAD~ &&
+ git symbolic-ref HEAD >actual &&
+ test_cmp expect actual &&
+
+ cat >expect <<-EOF &&
+ third
+ second reworded
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'can reword root commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ git history reword -m "first reworded" HEAD~2 &&
+
+ cat >expect <<-EOF &&
+ third
+ second
+ first reworded
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'can use editor to rewrite commit message' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+
+ write_script fake-editor.sh <<-\EOF &&
+ cp "$1" . &&
+ printf "\namend a comment\n" >>"$1"
+ EOF
+ test_set_editor "$(pwd)"/fake-editor.sh &&
+ git history reword HEAD &&
+
+ cat >expect <<-EOF &&
+ first
+
+ # Please enter the commit message for the reworded changes. Lines starting
+ # with ${SQ}#${SQ} will be kept; you may remove them yourself if you want to.
+ # Changes to be committed:
+ # new file: first.t
+ #
+ EOF
+ test_cmp expect COMMIT_EDITMSG &&
+
+ cat >expect <<-EOF &&
+ first
+
+ amend a comment
+
+ EOF
+ git log --format=%B >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'aborts with empty commit message' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+
+ test_must_fail git history reword -m "" HEAD 2>err &&
+ test_grep "Aborting commit due to empty commit message." err
+ )
+'
+
+
+test_done
--
2.51.0.308.g032396e0da.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH RFC v2 16/16] builtin/history: implement "reword" subcommand
2025-08-24 17:42 ` [PATCH RFC v2 16/16] builtin/history: implement "reword" subcommand Patrick Steinhardt
@ 2025-08-24 18:08 ` Kristoffer Haugsbakk
2025-09-03 12:20 ` Patrick Steinhardt
0 siblings, 1 reply; 278+ messages in thread
From: Kristoffer Haugsbakk @ 2025-08-24 18:08 UTC (permalink / raw)
To: Patrick Steinhardt, git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk
On Sun, Aug 24, 2025, at 19:42, Patrick Steinhardt wrote:
> Implement a new "reword" subcommand for git-history(1). This subcommand
> is essentially the same as if a user performed an interactive rebase
> with a single commit changed to use the "reword" verb.
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
I get a “split” error when I typo the commit to reword:
$ ./git history reword ./s2
error: commit to be split cannot be found: ./s2
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC v2 16/16] builtin/history: implement "reword" subcommand
2025-08-24 18:08 ` Kristoffer Haugsbakk
@ 2025-09-03 12:20 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-03 12:20 UTC (permalink / raw)
To: Kristoffer Haugsbakk
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk
On Sun, Aug 24, 2025 at 08:08:07PM +0200, Kristoffer Haugsbakk wrote:
> On Sun, Aug 24, 2025, at 19:42, Patrick Steinhardt wrote:
> > Implement a new "reword" subcommand for git-history(1). This subcommand
> > is essentially the same as if a user performed an interactive rebase
> > with a single commit changed to use the "reword" verb.
> >
> > Signed-off-by: Patrick Steinhardt <ps@pks.im>
>
> I get a “split” error when I typo the commit to reword:
>
> $ ./git history reword ./s2
> error: commit to be split cannot be found: ./s2
Oh, indeed, this is a boring copy-paste error:
diff --git a/builtin/history.c b/builtin/history.c
index cdc93a1cbd..f03272bddd 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -749,7 +749,7 @@ static int cmd_history_reword(int argc,
original_commit = lookup_commit_reference_by_name(argv[0]);
if (!original_commit) {
- ret = error(_("commit to be split cannot be found: %s"), argv[0]);
+ ret = error(_("commit to be reworded cannot be found: %s"), argv[0]);
goto out;
}
Thanks for noticing!
Patrick
^ permalink raw reply related [flat|nested] 278+ messages in thread
* Re: [PATCH RFC v2 00/16] Introduce git-history(1) command for easy history editing
2025-08-24 17:42 ` [PATCH RFC v2 00/16] " Patrick Steinhardt
` (15 preceding siblings ...)
2025-08-24 17:42 ` [PATCH RFC v2 16/16] builtin/history: implement "reword" subcommand Patrick Steinhardt
@ 2025-09-03 23:39 ` D. Ben Knoble
2025-09-04 13:05 ` Patrick Steinhardt
16 siblings, 1 reply; 278+ messages in thread
From: D. Ben Knoble @ 2025-09-03 23:39 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, Junio C Hamano, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk
On Sun, Aug 24, 2025 at 1:42 PM Patrick Steinhardt <ps@pks.im> wrote:
> Changes in v2:
> - Add a new "reword" subcommand.
> - List git-history(1) in "command-list.txt".
> - Add some missing error handling.
> - Simplify calling convention of `apply_commits()` to handle root
> commits internally instead of requiring every caller to do so.
> - Add tests to verify that git-history(1) refuses to work with changes
> in the worktree or index.
> - Mark git-history(1) as experimental.
> - Introduce commands to manage interrupted history edits.
> - A bunch of improvements to the manpage.
> - Link to v1: https://lore.kernel.org/r/20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im
Having test-driven this a bit, I wanted to mention a feature from my
editor that I use frequently and that "reword" reminded me of [*]. I
wonder if this would be a good fit for git-history, but certainly
wouldn't be _necessary_ for a v1.
With fugitive.vim [1], I can get a list of {staged,unstaged} changes
and commits. It looks something like this
Head: ps-jj
Rebase: origin/master
Push: benknoble/ps-jj
Help: g?
Unstaged (1)
M README.md
Unpushed to origin/master (16)
5c65c405e6 builtin/history: implement "reword" subcommand
b215e38d49 builtin/history: implement "split" subcommand
93977ef8d2 wt-status: provide function to expose status for trees
b09b4b9d48 add-patch: add support for in-memory index patching
[…]
If I stage changes, then the following mappings are available when the
cursor is on any commit line:
cF Create a `fixup!` commit for the commit under the
cursor and immediately rebase it.
cS Create a `squash!` commit for the commit under the
cursor and immediately rebase it.
What they do is populate my command line with something like
:Git commit --fixup=<commit>|Git -c sequence.editor=true rebase
--interactive --autosquash <commit>^
(The pipe is Vim's command separator, a bit like ";" or "&&" in shell.)
If git-history had a squash or fixup mode, I imagine it would function
similarly (and could be used as the backend for fugitive's cF/cS with
new enough Git).
[*] The reason I thought of this from "reword" is the obvious tie-in
to interactive rebase commands. Fugitive does have "cw" to reword the
last commit, "ca" to amend, and "cW" to "amend!" an arbitrary commit,
but no mappings that "amend!" + rebase immediately. It doesn't need
them because "rw" starts an interactive rebase with the commit under
cursor set to "reword" ;) likewise "rd" for drop.
[1]: https://github.com/tpope/vim-fugitive
--
D. Ben Knoble
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC v2 00/16] Introduce git-history(1) command for easy history editing
2025-09-03 23:39 ` [PATCH RFC v2 00/16] Introduce git-history(1) command for easy history editing D. Ben Knoble
@ 2025-09-04 13:05 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-04 13:05 UTC (permalink / raw)
To: D. Ben Knoble
Cc: git, Junio C Hamano, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk
On Wed, Sep 03, 2025 at 07:39:58PM -0400, D. Ben Knoble wrote:
> If git-history had a squash or fixup mode, I imagine it would function
> similarly (and could be used as the backend for fugitive's cF/cS with
> new enough Git).
Yup! I definitely do want to introduce a squash command eventually.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH RFC v3 00/18] Introduce git-history(1) command for easy history editing
2025-08-19 10:55 [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing Patrick Steinhardt
` (15 preceding siblings ...)
2025-08-24 17:42 ` [PATCH RFC v2 00/16] " Patrick Steinhardt
@ 2025-09-04 14:27 ` Patrick Steinhardt
2025-09-04 14:27 ` [PATCH RFC v3 01/18] sequencer: optionally skip printing commit summary Patrick Steinhardt
` (20 more replies)
2025-10-01 15:57 ` [PATCH v4 00/12] " Patrick Steinhardt
` (3 subsequent siblings)
20 siblings, 21 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-04 14:27 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
Hi,
over recent months I've been playing around with Jujutsu quite
frequently. While I still prefer using Git, there's been a couple
features in it that I really like and that I'd like to have in Git, as
well.
A copule of these features relate to history editing. Most importantly,
I really dig the following commands:
- jj-abandon(1) to drop a specific commit from your history.
- jj-absorb(1) to take some changes and automatically apply them to
commits in your history that last modified the respective hunks.
- jj-split(1) to split a commit into two.
- jj-new(1) to insert a new commit after or before a specific other
commit.
Not all of these commands can be ported directly into Git. jj-new(1) for
example doesn't really make a ton of sense for us, I'd claim. But some
of these commands _do_ make sense.
I thus had a look at implementing some of these commands in Git itself,
where the result is this patch series. Specifically, the following
commands are introduced by this patch series:
- `git history drop` to drop a specific commit. This is basically the
same as jj-abandon(1).
- `git history reorder` to reorder a specific commit before or after
another commit. This is inspired by jj-new(1).
- `git history split` takes a commit and splits it into two. This is
basically the same as jj-split(1).
If this is something we want to have I think it'd be just a starting
point. There's other commands that I think are quite common and that
might make sense to introduce eventually:
- An equivalent to jj-absorb(1) would be awesome to have.
- `git history reword` to change only the commit message of a specific
commit.
- `git history squash` to squash together multiple commits into one.
In the end, I'd like us to learn from what people like about Jujutsu and
apply those learnings to Git. We won't be able to apply all learnings
from Jujutsu, as the workflow is quite different there due to the lack
of the index. But other things we certainly can apply to Git directly.
Changes in v2:
- Add a new "reword" subcommand.
- List git-history(1) in "command-list.txt".
- Add some missing error handling.
- Simplify calling convention of `apply_commits()` to handle root
commits internally instead of requiring every caller to do so.
- Add tests to verify that git-history(1) refuses to work with changes
in the worktree or index.
- Mark git-history(1) as experimental.
- Introduce commands to manage interrupted history edits.
- A bunch of improvements to the manpage.
- Link to v1: https://lore.kernel.org/r/20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im
Changes in v3:
- Add logic to drive the "post-rewrite" hook and add tests to verify
that all hooks are executed as expected.
- Deduplicate logic to turn a replay action into a todo command.
- Move the addition of tests for the top-level git-history(1) command
to the correct commit.
- Some smaller commit message fixes.
- Honor "commit.verbose".
- Fix copy-paste error with an error message.
- Link to v2: https://lore.kernel.org/r/20250824-b4-pks-history-builtin-v2-0-964ac12f65bd@pks.im
Note: this patch series is growing quite large overall. I'll send one
last version of the complete series with the RFC tag, but after that
I'll probably split the series into two and stop after introducing the
"reorder" command.
Thanks!
Patrick
---
Patrick Steinhardt (18):
sequencer: optionally skip printing commit summary
sequencer: add option to rewind HEAD after picking commits
sequencer: introduce new history editing mode
sequencer: stop using `the_repository` in `sequencer_remove_state()`
sequencer: wire up "rewritten-hook" for REPLAY_HISTORY_EDIT
cache-tree: allow writing in-memory index as tree
builtin: add new "history" command
builtin/history: introduce subcommands to manage interrupted rewrites
builtin/history: implement "drop" subcommand
builtin/history: implement "reorder" subcommand
add-patch: split out header from "add-interactive.h"
add-patch: split out `struct interactive_options`
add-patch: remove dependency on "add-interactive" subsystem
add-patch: add support for in-memory index patching
wt-status: provide function to expose status for trees
sequencer: allow callers to provide mappings for the old commit
builtin/history: implement "split" subcommand
builtin/history: implement "reword" subcommand
.gitignore | 1 +
Documentation/git-history.adoc | 188 ++++++++
Documentation/meson.build | 1 +
Makefile | 1 +
add-interactive.c | 151 ++-----
add-interactive.h | 43 +-
add-patch.c | 270 ++++++++++--
add-patch.h | 61 +++
builtin.h | 1 +
builtin/add.c | 22 +-
builtin/checkout.c | 7 +-
builtin/commit.c | 16 +-
builtin/history.c | 958 +++++++++++++++++++++++++++++++++++++++++
builtin/rebase.c | 4 +-
builtin/reset.c | 16 +-
builtin/revert.c | 2 +-
builtin/stash.c | 46 +-
cache-tree.c | 5 +-
cache-tree.h | 3 +-
command-list.txt | 1 +
commit.h | 2 +-
git.c | 1 +
meson.build | 1 +
sequencer.c | 259 ++++++++---
sequencer.h | 23 +-
t/meson.build | 7 +-
t/t3450-history.sh | 42 ++
t/t3451-history-drop.sh | 207 +++++++++
t/t3452-history-reorder.sh | 278 ++++++++++++
t/t3453-history-split.sh | 468 ++++++++++++++++++++
t/t3454-history-reword.sh | 202 +++++++++
wt-status.c | 24 ++
wt-status.h | 3 +
33 files changed, 3005 insertions(+), 309 deletions(-)
Range-diff versus v2:
1: ac3e60d11f = 1: d397c84460 sequencer: optionally skip printing commit summary
2: 411212b581 = 2: 9f6444ab79 sequencer: add option to rewind HEAD after picking commits
3: 670a1879ea ! 3: 6918f3fc1b sequencer: introduce new history editing mode
@@ sequencer.c: void sequencer_post_commit_cleanup(struct repository *r, int verbos
if (refs_ref_exists(get_main_ref_store(r), "REVERT_HEAD")) {
if (!refs_delete_ref(get_main_ref_store(r), "", "REVERT_HEAD",
NULL, REF_NO_DEREF) &&
+@@ sequencer.c: static void todo_list_write_total_nr(struct todo_list *todo_list)
+ }
+ }
+
++static enum todo_command action_to_command(enum replay_action action)
++{
++ switch (action) {
++ case REPLAY_PICK:
++ case REPLAY_HISTORY_EDIT:
++ return TODO_PICK;
++ case REPLAY_REVERT:
++ return TODO_REVERT;
++ default:
++ BUG("unsupported action %d", action);
++ }
++}
++
+ static int read_populate_todo(struct repository *r,
+ struct todo_list *todo_list,
+ struct replay_opts *opts)
@@ sequencer.c: static int read_populate_todo(struct repository *r,
return error(_("no commits parsed."));
if (!is_rebase_i(opts)) {
- enum todo_command valid =
- opts->action == REPLAY_PICK ? TODO_PICK : TODO_REVERT;
-+ enum todo_command valid;
++ enum todo_command valid = action_to_command(opts->action);
int i;
- for (i = 0; i < todo_list->nr; i++)
-+ switch (opts->action) {
-+ case REPLAY_PICK:
-+ case REPLAY_HISTORY_EDIT:
-+ valid = TODO_PICK;
-+ break;
-+ default:
-+ valid = TODO_REVERT;
-+ break;
-+ }
-+
+ for (i = 0; i < todo_list->nr; i++) {
if (valid == todo_list->items[i].command)
continue;
@@ sequencer.c: int write_basic_state(struct replay_opts *opts, const char *head_na
if (prepare_revs(opts))
return -1;
-+ switch (opts->action) {
-+ case REPLAY_PICK:
-+ case REPLAY_HISTORY_EDIT:
-+ command = TODO_PICK;
-+ break;
-+ default:
-+ command = TODO_REVERT;
-+ break;
-+ }
-+
++ command = action_to_command(opts->action);
+ command_string = todo_command_info[command].str;
encoding = get_log_output_encoding();
@@ sequencer.c: static int single_pick(struct repository *r,
- item.command = opts->action == REPLAY_PICK ?
- TODO_PICK : TODO_REVERT;
-+ switch (opts->action) {
-+ case REPLAY_PICK:
-+ case REPLAY_HISTORY_EDIT:
-+ item.command = TODO_PICK;
-+ break;
-+ default:
-+ item.command = TODO_REVERT;
-+ break;
-+ }
++ item.command = action_to_command(opts->action);
item.commit = cmit;
return do_pick_commit(r, &item, opts, 0, &check_todo);
@@ sequencer.h: extern const char *rebase_resolvemsg;
};
enum commit_msg_cleanup_mode {
-
- ## t/t3450-history.sh (new) ##
-@@
-+#!/bin/sh
-+
-+test_description='tests for git-history command'
-+
-+. ./test-lib.sh
-+
-+test_expect_success 'refuses to do anything without subcommand' '
-+ test_must_fail git history 2>err &&
-+ test_grep foo err
-+'
-+
-+test_done
4: 6f2605f63d = 4: e5317d44a3 sequencer: stop using `the_repository` in `sequencer_remove_state()`
-: ---------- > 5: cc5108af10 sequencer: wire up "rewritten-hook" for REPLAY_HISTORY_EDIT
5: 727ec4955d = 6: 457042c591 cache-tree: allow writing in-memory index as tree
6: eec4f74fd8 ! 7: 1acf4de28a builtin: add new "history" command
@@ meson.build: builtin_sources = [
'builtin/hook.c',
'builtin/index-pack.c',
'builtin/init-db.c',
+
+ ## t/meson.build ##
+@@ t/meson.build: integration_tests = [
+ 't3436-rebase-more-options.sh',
+ 't3437-rebase-fixup-options.sh',
+ 't3438-rebase-broken-files.sh',
++ 't3450-history.sh',
+ 't3500-cherry.sh',
+ 't3501-revert-cherry-pick.sh',
+ 't3502-cherry-pick-merge.sh',
+@@ t/meson.build: if perl.found() and time.found()
+ timeout: 0,
+ )
+ endforeach
+-endif
+ \ No newline at end of file
++endif
+
+ ## t/t3450-history.sh (new) ##
+@@
++#!/bin/sh
++
++test_description='tests for git-history command'
++
++. ./test-lib.sh
++
++test_expect_success 'refuses to do anything without subcommand' '
++ test_must_fail git history 2>err &&
++ test_grep foo err
++'
++
++test_done
7: 2820521f46 ! 8: 07fff4c157 builtin/history: introduce subcommands to manage interrupted rewrites
@@ builtin/history.c
+ return fn(argc, argv, prefix, repo);
}
- ## t/meson.build ##
-@@ t/meson.build: integration_tests = [
- 't3436-rebase-more-options.sh',
- 't3437-rebase-fixup-options.sh',
- 't3438-rebase-broken-files.sh',
-+ 't3450-history.sh',
- 't3500-cherry.sh',
- 't3501-revert-cherry-pick.sh',
- 't3502-cherry-pick-merge.sh',
-@@ t/meson.build: if perl.found() and time.found()
- timeout: 0,
- )
- endforeach
--endif
- \ No newline at end of file
-+endif
-
## t/t3450-history.sh ##
@@ t/t3450-history.sh: test_description='tests for git-history command'
8: 3438f23f1b ! 9: a68cd72864 builtin/history: implement "drop" subcommand
@@ Documentation/git-history.adoc: COMMANDS
+`drop <commit>`::
+ Drop a commit from the history and reapply all children of that
+ commit on top of the commit's parent. The commit that is to be
-+ dropped must be reachable from the current `HEAD` commit.
++ dropped must be reachable from the currently checked-out commit.
++
+Dropping the root commit converts the child of that commit into the new
+root commit. It is invalid to drop a root commit that does not have any
@@ t/t3451-history-drop.sh (new)
+ )
+'
+
++test_expect_success 'hooks are executed for rewritten commits' '
++ test_when_finished "rm -rf repo" &&
++ git init repo &&
++ (
++ cd repo &&
++ test_commit first &&
++ test_commit second &&
++ test_commit third &&
++
++ write_script .git/hooks/prepare-commit-msg <<-EOF &&
++ echo "prepare-commit-msg: \$@" >>"$(pwd)/hooks.log"
++ EOF
++ write_script .git/hooks/post-commit <<-EOF &&
++ echo "post-commit" >>"$(pwd)/hooks.log"
++ EOF
++ write_script .git/hooks/post-rewrite <<-EOF &&
++ {
++ echo "post-rewrite: \$@"
++ cat
++ } >>"$(pwd)/hooks.log"
++ EOF
++
++ git history drop HEAD~ &&
++ cat >expect <<-EOF &&
++ prepare-commit-msg: .git/COMMIT_EDITMSG message
++ post-commit
++ post-rewrite: history
++ $(git rev-parse third) $(git rev-parse HEAD)
++ EOF
++ test_cmp expect hooks.log
++ )
++'
++
+test_expect_success 'can drop root commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
9: 1af60a15fe ! 10: 3caf8f8e23 builtin/history: implement "reorder" subcommand
@@ Documentation/git-history.adoc: git history abort
DESCRIPTION
-----------
-@@ Documentation/git-history.adoc: rewrite history in different ways:
- `drop <commit>`::
- Drop a commit from the history and reapply all children of that
- commit on top of the commit's parent. The commit that is to be
-- dropped must be reachable from the current `HEAD` commit.
-+ dropped must be reachable from the currently checked-out commit.
- +
- Dropping the root commit converts the child of that commit into the new
+@@ Documentation/git-history.adoc: Dropping the root commit converts the child of that commit into the new
root commit. It is invalid to drop a root commit that does not have any
child commits, as that would lead to an empty branch.
@@ t/t3452-history-reorder.sh (new)
+ )
+'
+
++test_expect_success 'hooks are executed for rewritten commits' '
++ test_when_finished "rm -rf repo" &&
++ git init repo &&
++ (
++ cd repo &&
++ test_commit first &&
++ test_commit second &&
++ test_commit third &&
++
++ write_script .git/hooks/prepare-commit-msg <<-EOF &&
++ echo "prepare-commit-msg: \$@" >>"$(pwd)/hooks.log"
++ EOF
++ write_script .git/hooks/post-commit <<-EOF &&
++ echo "post-commit" >>"$(pwd)/hooks.log"
++ EOF
++ write_script .git/hooks/post-rewrite <<-EOF &&
++ {
++ echo "post-rewrite: \$@"
++ cat
++ } >>"$(pwd)/hooks.log"
++ EOF
++
++ git history reorder :/third --before=:/second &&
++ cat >expect <<-EOF &&
++ second
++ third
++ first
++ EOF
++ git log --format=%s >actual &&
++ test_cmp expect actual &&
++
++ cat >expect <<-EOF &&
++ prepare-commit-msg: .git/COMMIT_EDITMSG message
++ post-commit
++ prepare-commit-msg: .git/COMMIT_EDITMSG message
++ post-commit
++ post-rewrite: history
++ $(git rev-parse third) $(git rev-parse HEAD~)
++ $(git rev-parse second) $(git rev-parse HEAD)
++ EOF
++ test_cmp expect hooks.log
++ )
++'
++
+test_expect_success 'conflicts are detected' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
10: a5e90d5b2f = 11: 5f509b2edc add-patch: split out header from "add-interactive.h"
11: 927f2d226b = 12: cba60462f5 add-patch: split out `struct interactive_options`
12: 1b232881f2 = 13: 316b830d2c add-patch: remove dependency on "add-interactive" subsystem
13: 4f1fcc0f85 = 14: 4e964cdcf8 add-patch: add support for in-memory index patching
14: b88ac830f1 = 15: 9b33852290 wt-status: provide function to expose status for trees
-: ---------- > 16: 2a7fc74da1 sequencer: allow callers to provide mappings for the old commit
15: 163e8d4d6e ! 17: 9b68e75de2 builtin/history: implement "split" subcommand
@@ Commit message
It is quite a common use case that one wants to split up one commit into
multiple commits by moving parts of the changes of the original commit
- out of it into a separate commit. This is quite an involved operation
- though:
+ out into a separate commit. This is quite an involved operation though:
1. Identify the commit in question that is to be dropped.
@@ builtin/history.c
static int cmd_history_abort(int argc,
const char **argv,
+@@ builtin/history.c: static int apply_commits(struct repository *repo,
+ const struct strvec *commits,
+ struct commit *head,
+ struct commit *base,
++ const struct oidmap *rewritten_commits,
+ const char *action)
+ {
+ struct setup_revision_opt revision_opts = {
+@@ builtin/history.c: static int apply_commits(struct repository *repo,
+ replay_opts.strategy = replay_opts.default_strategy;
+ replay_opts.default_strategy = NULL;
+ }
++ replay_opts.old_oid_mappings = rewritten_commits;
+
+ strvec_push(&args, "");
+ strvec_pushv(&args, commits->v);
+@@ builtin/history.c: static int cmd_history_drop(int argc,
+ if (ret < 0)
+ goto out;
+
+- ret = apply_commits(repo, &commits, head, commit_to_drop, "drop");
++ ret = apply_commits(repo, &commits, head, commit_to_drop,
++ NULL, "drop");
+ if (ret < 0)
+ goto out;
+ }
+@@ builtin/history.c: static int cmd_history_reorder(int argc,
+ replace_commits(&commits, &commit_to_reorder->object.oid, NULL, 0);
+ replace_commits(&commits, &anchor->object.oid, replacement, ARRAY_SIZE(replacement));
+
+- ret = apply_commits(repo, &commits, head, old, "reorder");
++ ret = apply_commits(repo, &commits, head, old, NULL, "reorder");
+ if (ret < 0)
+ goto out;
+
@@ builtin/history.c: static int cmd_history_reorder(int argc,
return ret;
}
@@ builtin/history.c: static int cmd_history_reorder(int argc,
+ struct strbuf *out)
+{
+ if (!provided_message) {
-+ struct wt_status s;
+ const char *path = git_path_commit_editmsg();
+ const char *hint =
+ _("Please enter the commit message for the %s changes. Lines starting\n"
+ "with '%s' will be kept; you may remove them yourself if you want to.\n");
++ int verbose = 1;
+
+ strbuf_addstr(out, default_message);
+ strbuf_addch(out, '\n');
+ strbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);
+ write_file_buf(path, out->buf, out->len);
+
-+ wt_status_prepare(repo, &s);
-+ FREE_AND_NULL(s.branch);
-+ s.ahead_behind_flags = AHEAD_BEHIND_QUICK;
-+ s.commit_template = 1;
-+ s.colopts = 0;
-+ s.display_comment_prefix = 1;
-+ s.hints = 0;
-+ s.use_color = 0;
-+ s.whence = FROM_COMMIT;
-+ s.committable = 1;
-+
-+ s.fp = fopen(git_path_commit_editmsg(), "a");
-+ if (!s.fp)
-+ return error_errno(_("could not open '%s'"), git_path_commit_editmsg());
-+
-+ wt_status_collect_changes_trees(&s, old_tree, new_tree);
-+ wt_status_print(&s);
-+ wt_status_collect_free_buffers(&s);
-+ string_list_clear_func(&s.change, change_data_free);
++ repo_config_get_bool(repo, "commit.verbose", &verbose);
++ if (verbose) {
++ struct wt_status s;
++
++ wt_status_prepare(repo, &s);
++ FREE_AND_NULL(s.branch);
++ s.ahead_behind_flags = AHEAD_BEHIND_QUICK;
++ s.commit_template = 1;
++ s.colopts = 0;
++ s.display_comment_prefix = 1;
++ s.hints = 0;
++ s.use_color = 0;
++ s.whence = FROM_COMMIT;
++ s.committable = 1;
++
++ s.fp = fopen(git_path_commit_editmsg(), "a");
++ if (!s.fp)
++ return error_errno(_("could not open '%s'"), git_path_commit_editmsg());
++
++ wt_status_collect_changes_trees(&s, old_tree, new_tree);
++ wt_status_print(&s);
++ wt_status_collect_free_buffers(&s);
++ string_list_clear_func(&s.change, change_data_free);
++ }
+
+ strbuf_reset(out);
+ if (launch_editor(path, out, NULL)) {
-+ fprintf(stderr, _("Please supply the message using either -m or -F option.\n"));
++ fprintf(stderr, _("Please supply the message using the -m option.\n"));
+ return -1;
+ }
+ strbuf_stripspace(out, comment_line_str);
-+
+ } else {
+ strbuf_addstr(out, provided_message);
+ }
@@ builtin/history.c: static int cmd_history_reorder(int argc,
+ OPT_STRING('m', "message", &commit_message, N_("message"), N_("commit message")),
+ OPT_END(),
+ };
++ struct oidmap rewritten_commits = OIDMAP_INIT;
+ struct commit *original_commit, *head;
+ struct strvec commits = STRVEC_INIT;
+ struct commit_list *list = NULL;
+ struct object_id split_commits[2];
++ struct replay_oid_mapping mapping[2] = { 0 };
+ struct pathspec pathspec = { 0 };
+ int ret;
+
@@ builtin/history.c: static int cmd_history_reorder(int argc,
+ if (ret < 0)
+ goto out;
+
++ mapping[0].entry.oid = split_commits[0];
++ mapping[0].rewritten_oid = original_commit->object.oid;
++ oidmap_put(&rewritten_commits, &mapping[0]);
++ mapping[1].entry.oid = split_commits[1];
++ mapping[1].rewritten_oid = original_commit->object.oid;
++ oidmap_put(&rewritten_commits, &mapping[1]);
++
+ replace_commits(&commits, &original_commit->object.oid,
+ split_commits, ARRAY_SIZE(split_commits));
+
-+ ret = apply_commits(repo, &commits, head, original_commit, "split");
++ ret = apply_commits(repo, &commits, head, original_commit,
++ &rewritten_commits, "split");
+ if (ret < 0)
+ goto out;
+
+ ret = 0;
+
+out:
++ oidmap_clear(&rewritten_commits, 0);
+ clear_pathspec(&pathspec);
+ strvec_clear(&commits);
+ free_commit_list(list);
@@ t/t3453-history-split.sh (new)
+ )
+'
+
++test_expect_success 'skips change summary with commit.verbose=false' '
++ test_when_finished "rm -rf repo" &&
++ git init repo &&
++ (
++ cd repo &&
++ touch bar foo &&
++ git add . &&
++ git commit -m split-me &&
++
++ write_script fake-editor.sh <<-\EOF &&
++ cp "$1" . &&
++ echo "some commit message" >>"$1"
++ EOF
++ test_set_editor "$(pwd)"/fake-editor.sh &&
++
++ git -c commit.verbose=false history split HEAD <<-EOF &&
++ y
++ n
++ EOF
++
++ cat >expect <<-EOF &&
++
++ # Please enter the commit message for the split-out changes. Lines starting
++ # with ${SQ}#${SQ} will be kept; you may remove them yourself if you want to.
++ EOF
++ test_cmp expect COMMIT_EDITMSG &&
++
++ expect_log <<-EOF
++ split-me
++ some commit message
++ EOF
++ )
++'
++
+test_expect_success 'can use pathspec to limit what gets split' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
@@ t/t3453-history-split.sh (new)
+ )
+'
+
++test_expect_success 'hooks are executed for rewritten commits' '
++ test_when_finished "rm -rf repo" &&
++ git init repo &&
++ (
++ cd repo &&
++ touch bar foo &&
++ git add . &&
++ git commit -m split-me &&
++ old_head=$(git rev-parse HEAD) &&
++
++ write_script .git/hooks/prepare-commit-msg <<-EOF &&
++ echo "prepare-commit-msg: \$@" >>"$(pwd)/hooks.log"
++ EOF
++ write_script .git/hooks/post-commit <<-EOF &&
++ echo "post-commit" >>"$(pwd)/hooks.log"
++ EOF
++ write_script .git/hooks/post-rewrite <<-EOF &&
++ {
++ echo "post-rewrite: \$@"
++ cat
++ } >>"$(pwd)/hooks.log"
++ EOF
++
++ set_fake_editor &&
++ git history split HEAD <<-EOF &&
++ y
++ n
++ EOF
++
++ expect_log <<-EOF &&
++ split-me
++ split-out commit
++ EOF
++
++ cat >expect <<-EOF &&
++ prepare-commit-msg: .git/COMMIT_EDITMSG message
++ post-commit
++ prepare-commit-msg: .git/COMMIT_EDITMSG message
++ post-commit
++ post-rewrite: history
++ $old_head $(git rev-parse HEAD~)
++ $old_head $(git rev-parse HEAD)
++ EOF
++ test_cmp expect hooks.log
++ )
++'
++
+test_expect_success 'refuses to create empty original commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
16: 142e1e627b ! 18: 07eca0b266 builtin/history: implement "reword" subcommand
@@ builtin/history.c: static int split_commit(struct repository *repo,
+ struct object_id parent_tree_oid, original_commit_tree_oid;
+ struct object_id rewritten_commit;
+ const char *original_message, *original_body, *ptr;
++ struct oidmap rewritten_commits = OIDMAP_INIT;
++ struct replay_oid_mapping mapping = { 0 };
+ char *original_author = NULL;
+ size_t len;
+ int ret;
@@ builtin/history.c: static int split_commit(struct repository *repo,
+
+ original_commit = lookup_commit_reference_by_name(argv[0]);
+ if (!original_commit) {
-+ ret = error(_("commit to be split cannot be found: %s"), argv[0]);
++ ret = error(_("commit to be reworded cannot be found: %s"), argv[0]);
+ goto out;
+ }
+
@@ builtin/history.c: static int split_commit(struct repository *repo,
+
+ replace_commits(&commits, &original_commit->object.oid, &rewritten_commit, 1);
+
-+ ret = apply_commits(repo, &commits, head, original_commit, "reword");
++ mapping.entry.oid = rewritten_commit;
++ mapping.rewritten_oid = original_commit->object.oid;
++ oidmap_put(&rewritten_commits, &mapping);
++
++ ret = apply_commits(repo, &commits, head, original_commit,
++ &rewritten_commits, "reword");
+ if (ret < 0)
+ goto out;
+
+ ret = 0;
+
+out:
++ oidmap_clear(&rewritten_commits, 0);
+ strbuf_release(&final_message);
+ strvec_clear(&commits);
+ free(original_author);
@@ t/t3454-history-reword.sh (new)
+ )
+'
+
++test_expect_success 'hooks are executed for rewritten commits' '
++ test_when_finished "rm -rf repo" &&
++ git init repo &&
++ (
++ cd repo &&
++ test_commit first &&
++ test_commit second &&
++ test_commit third &&
++
++ write_script .git/hooks/prepare-commit-msg <<-EOF &&
++ echo "prepare-commit-msg: \$@" >>"$(pwd)/hooks.log"
++ EOF
++ write_script .git/hooks/post-commit <<-EOF &&
++ echo "post-commit" >>"$(pwd)/hooks.log"
++ EOF
++ write_script .git/hooks/post-rewrite <<-EOF &&
++ {
++ echo "post-rewrite: \$@"
++ cat
++ } >>"$(pwd)/hooks.log"
++ EOF
++
++ git history reword -m "second reworded" HEAD~ &&
++
++ cat >expect <<-EOF &&
++ third
++ second reworded
++ first
++ EOF
++ git log --format=%s >actual &&
++ test_cmp expect actual &&
++
++ cat >expect <<-EOF &&
++ prepare-commit-msg: .git/COMMIT_EDITMSG message
++ post-commit
++ prepare-commit-msg: .git/COMMIT_EDITMSG message
++ post-commit
++ post-rewrite: history
++ $(git rev-parse second) $(git rev-parse HEAD~)
++ $(git rev-parse third) $(git rev-parse HEAD)
++ EOF
++ test_cmp expect hooks.log
++ )
++'
++
+test_expect_success 'aborts with empty commit message' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
@@ t/t3454-history-reword.sh (new)
+ )
+'
+
-+
+test_done
---
base-commit: c44beea485f0f2feaf460e2ac87fdd5608d63cf0
change-id: 20250819-b4-pks-history-builtin-83398f9a05f0
^ permalink raw reply [flat|nested] 278+ messages in thread* [PATCH RFC v3 01/18] sequencer: optionally skip printing commit summary
2025-09-04 14:27 ` [PATCH RFC v3 00/18] " Patrick Steinhardt
@ 2025-09-04 14:27 ` Patrick Steinhardt
2025-09-10 14:01 ` Phillip Wood
2025-09-04 14:27 ` [PATCH RFC v3 02/18] sequencer: add option to rewind HEAD after picking commits Patrick Steinhardt
` (19 subsequent siblings)
20 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-04 14:27 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
When picking commits by using for example git-cherry-pick(1) we end up
printing a commit summary that gives the reader information around what
exactly we have been picking:
```
$ git cherry-pick main
[other 76c8456] bar
Date: Tue Aug 19 08:07:26 2025 +0200
1 file changed, 1 insertion(+)
create mode 100644 bar
```
While useful for some commands, we're about to introduce a new command
where this output will be less so. But right now there is no way to
disable printing this commit summary.
Introduce a new `skip_commit_summary` replay option that does so.
Persist the option into the sequencer configuration so that it persists
across different processes, e.g. when we need to stop due to a merge
conflict.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
sequencer.c | 12 +++++++++---
sequencer.h | 1 +
2 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/sequencer.c b/sequencer.c
index aaf2e4df64..7066cdc939 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -1742,7 +1742,7 @@ static int do_commit(struct repository *r,
refs_delete_ref(get_main_ref_store(r), "",
"CHERRY_PICK_HEAD", NULL, REF_NO_DEREF);
unlink(git_path_merge_msg(r));
- if (!is_rebase_i(opts))
+ if (!is_rebase_i(opts) && !opts->skip_commit_summary)
print_commit_summary(r, NULL, &oid,
SUMMARY_SHOW_AUTHOR_DATE);
return res;
@@ -3139,8 +3139,12 @@ static int populate_opts_cb(const char *key, const char *value,
else if (!strcmp(key, "options.default-msg-cleanup")) {
opts->explicit_cleanup = 1;
opts->default_msg_cleanup = get_cleanup_mode(value, 1);
- } else
+ } else if (!strcmp(key, "options.skip-commit-summary")) {
+ opts->skip_commit_summary =
+ git_config_bool_or_int(key, value, ctx->kvi, &error_flag);
+ } else {
return error(_("invalid key: %s"), key);
+ }
if (!error_flag)
return error(_("invalid value for '%s': '%s'"), key, value);
@@ -3698,11 +3702,13 @@ static int save_opts(struct replay_opts *opts)
"options.allow-rerere-auto", NULL,
opts->allow_rerere_auto == RERERE_AUTOUPDATE ?
"true" : "false");
-
if (opts->explicit_cleanup)
res |= repo_config_set_in_file_gently(the_repository, opts_file,
"options.default-msg-cleanup", NULL,
describe_cleanup_mode(opts->default_msg_cleanup));
+ if (opts->skip_commit_summary)
+ res |= repo_config_set_in_file_gently(the_repository, opts_file,
+ "options.skip-commit-summary", NULL, "true");
return res;
}
diff --git a/sequencer.h b/sequencer.h
index 304ba4b4d3..1767fd737e 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -52,6 +52,7 @@ struct replay_opts {
int keep_redundant_commits;
int verbose;
int quiet;
+ int skip_commit_summary;
int reschedule_failed_exec;
int committer_date_is_author_date;
int ignore_date;
--
2.51.0.417.g1ba7204a04.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH RFC v3 01/18] sequencer: optionally skip printing commit summary
2025-09-04 14:27 ` [PATCH RFC v3 01/18] sequencer: optionally skip printing commit summary Patrick Steinhardt
@ 2025-09-10 14:01 ` Phillip Wood
2025-09-15 9:32 ` Patrick Steinhardt
0 siblings, 1 reply; 278+ messages in thread
From: Phillip Wood @ 2025-09-10 14:01 UTC (permalink / raw)
To: Patrick Steinhardt, git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
Hi Patrick
On 04/09/2025 15:27, Patrick Steinhardt wrote:
>
> diff --git a/sequencer.c b/sequencer.c
> index aaf2e4df64..7066cdc939 100644
> --- a/sequencer.c
> +++ b/sequencer.c
> @@ -1742,7 +1742,7 @@ static int do_commit(struct repository *r,
> refs_delete_ref(get_main_ref_store(r), "",
> "CHERRY_PICK_HEAD", NULL, REF_NO_DEREF);
> unlink(git_path_merge_msg(r));
> - if (!is_rebase_i(opts))
> + if (!is_rebase_i(opts) && !opts->skip_commit_summary)
I think it would be cleaner to make rebase set the new option and remove
!is_rebase_i(opts) here.
Thanks
Phillip
> print_commit_summary(r, NULL, &oid,
> SUMMARY_SHOW_AUTHOR_DATE);
> return res;
> @@ -3139,8 +3139,12 @@ static int populate_opts_cb(const char *key, const char *value,
> else if (!strcmp(key, "options.default-msg-cleanup")) {
> opts->explicit_cleanup = 1;
> opts->default_msg_cleanup = get_cleanup_mode(value, 1);
> - } else
> + } else if (!strcmp(key, "options.skip-commit-summary")) {
> + opts->skip_commit_summary =
> + git_config_bool_or_int(key, value, ctx->kvi, &error_flag);
> + } else {
> return error(_("invalid key: %s"), key);
> + }
>
> if (!error_flag)
> return error(_("invalid value for '%s': '%s'"), key, value);
> @@ -3698,11 +3702,13 @@ static int save_opts(struct replay_opts *opts)
> "options.allow-rerere-auto", NULL,
> opts->allow_rerere_auto == RERERE_AUTOUPDATE ?
> "true" : "false");
> -
> if (opts->explicit_cleanup)
> res |= repo_config_set_in_file_gently(the_repository, opts_file,
> "options.default-msg-cleanup", NULL,
> describe_cleanup_mode(opts->default_msg_cleanup));
> + if (opts->skip_commit_summary)
> + res |= repo_config_set_in_file_gently(the_repository, opts_file,
> + "options.skip-commit-summary", NULL, "true");
> return res;
> }
>
> diff --git a/sequencer.h b/sequencer.h
> index 304ba4b4d3..1767fd737e 100644
> --- a/sequencer.h
> +++ b/sequencer.h
> @@ -52,6 +52,7 @@ struct replay_opts {
> int keep_redundant_commits;
> int verbose;
> int quiet;
> + int skip_commit_summary;
> int reschedule_failed_exec;
> int committer_date_is_author_date;
> int ignore_date;
>
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC v3 01/18] sequencer: optionally skip printing commit summary
2025-09-10 14:01 ` Phillip Wood
@ 2025-09-15 9:32 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-15 9:32 UTC (permalink / raw)
To: phillip.wood
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
On Wed, Sep 10, 2025 at 03:01:54PM +0100, Phillip Wood wrote:
> On 04/09/2025 15:27, Patrick Steinhardt wrote:
> > diff --git a/sequencer.c b/sequencer.c
> > index aaf2e4df64..7066cdc939 100644
> > --- a/sequencer.c
> > +++ b/sequencer.c
> > @@ -1742,7 +1742,7 @@ static int do_commit(struct repository *r,
> > refs_delete_ref(get_main_ref_store(r), "",
> > "CHERRY_PICK_HEAD", NULL, REF_NO_DEREF);
> > unlink(git_path_merge_msg(r));
> > - if (!is_rebase_i(opts))
> > + if (!is_rebase_i(opts) && !opts->skip_commit_summary)
>
> I think it would be cleaner to make rebase set the new option and remove
> !is_rebase_i(opts) here.
Good suggestion, will do.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH RFC v3 02/18] sequencer: add option to rewind HEAD after picking commits
2025-09-04 14:27 ` [PATCH RFC v3 00/18] " Patrick Steinhardt
2025-09-04 14:27 ` [PATCH RFC v3 01/18] sequencer: optionally skip printing commit summary Patrick Steinhardt
@ 2025-09-04 14:27 ` Patrick Steinhardt
2025-09-10 14:04 ` Phillip Wood
2025-09-04 14:27 ` [PATCH RFC v3 03/18] sequencer: introduce new history editing mode Patrick Steinhardt
` (18 subsequent siblings)
20 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-04 14:27 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
While the sequencer infrastructure knows to rewind "HEAD" to whatever it
was pointing to before a rebase, it doesn't do the same for non-rebase
operations like cherry-picks. This is because the expectation is that
the user directly picks commits on top of whatever "HEAD" points to, and
we advance the reference pointed to by "HEAD" instead of updating it
directly.
We're about to introduce a new command though that needs to detach
"HEAD" while being more similar to git-cherry-pick(1) rathen than to
git-rebase(1). As such, we'll want to restore "HEAD" to point to the
branch that we started on while not using the more heavy-weight rebase
machinery.
Introduce a new option `restore_head_target` to do so. Persist the
option into the sequencer configuration so that it persists across
different processes, e.g. when we need to stop due to a merge conflict.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
sequencer.c | 27 ++++++++++++++++++++++++++-
sequencer.h | 3 +++
2 files changed, 29 insertions(+), 1 deletion(-)
diff --git a/sequencer.c b/sequencer.c
index 7066cdc939..bff181df76 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -413,6 +413,7 @@ void replay_opts_release(struct replay_opts *opts)
struct replay_ctx *ctx = opts->ctx;
free(opts->gpg_sign);
+ free(opts->restore_head_target);
free(opts->reflog_action);
free(opts->default_strategy);
free(opts->strategy);
@@ -3142,6 +3143,8 @@ static int populate_opts_cb(const char *key, const char *value,
} else if (!strcmp(key, "options.skip-commit-summary")) {
opts->skip_commit_summary =
git_config_bool_or_int(key, value, ctx->kvi, &error_flag);
+ } else if (!strcmp(key, "options.restore-head-target")) {
+ git_config_string_dup(&opts->restore_head_target, key, value);
} else {
return error(_("invalid key: %s"), key);
}
@@ -3709,6 +3712,10 @@ static int save_opts(struct replay_opts *opts)
if (opts->skip_commit_summary)
res |= repo_config_set_in_file_gently(the_repository, opts_file,
"options.skip-commit-summary", NULL, "true");
+ if (opts->restore_head_target)
+ res |= repo_config_set_in_file_gently(the_repository, opts_file,
+ "options.restore-head-target", NULL, opts->restore_head_target);
+
return res;
}
@@ -5177,6 +5184,23 @@ static int pick_commits(struct repository *r,
return -1;
}
+ if (opts->restore_head_target) {
+ struct reset_head_opts reset_opts = { 0 };
+ const char *msg;
+
+ msg = reflog_message(opts, "finish", "returning to %s", opts->restore_head_target);
+
+ reset_opts.branch = opts->restore_head_target;
+ reset_opts.flags = RESET_HEAD_REFS_ONLY;
+ reset_opts.branch_msg = msg;
+ reset_opts.head_msg = msg;
+
+ if (reset_head(r, &reset_opts)) {
+ error(_("could not switch HEAD back to %s"), opts->restore_head_target);
+ return -1;
+ }
+ }
+
/*
* Sequence of picks finished successfully; cleanup by
* removing the .git/sequencer directory
@@ -5533,7 +5557,8 @@ int sequencer_pick_revisions(struct repository *r,
if (opts->revs->cmdline.nr == 1 &&
opts->revs->cmdline.rev->whence == REV_CMD_REV &&
opts->revs->no_walk &&
- !opts->revs->cmdline.rev->flags) {
+ !opts->revs->cmdline.rev->flags &&
+ !opts->restore_head_target) {
struct commit *cmit;
if (prepare_revision_walk(opts->revs)) {
diff --git a/sequencer.h b/sequencer.h
index 1767fd737e..a905f6afc7 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -72,6 +72,9 @@ struct replay_opts {
/* Reflog */
char *reflog_action;
+ /* Reference to which HEAD shall be reset to after the operation. */
+ char *restore_head_target;
+
/* placeholder commit for -i --root */
struct object_id squash_onto;
int have_squash_onto;
--
2.51.0.417.g1ba7204a04.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH RFC v3 02/18] sequencer: add option to rewind HEAD after picking commits
2025-09-04 14:27 ` [PATCH RFC v3 02/18] sequencer: add option to rewind HEAD after picking commits Patrick Steinhardt
@ 2025-09-10 14:04 ` Phillip Wood
2025-09-15 9:32 ` Patrick Steinhardt
0 siblings, 1 reply; 278+ messages in thread
From: Phillip Wood @ 2025-09-10 14:04 UTC (permalink / raw)
To: Patrick Steinhardt, git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
Hi Patrick
On 04/09/2025 15:27, Patrick Steinhardt wrote:
> While the sequencer infrastructure knows to rewind "HEAD" to whatever it
> was pointing to before a rebase, it doesn't do the same for non-rebase
> operations like cherry-picks. This is because the expectation is that
> the user directly picks commits on top of whatever "HEAD" points to, and
> we advance the reference pointed to by "HEAD" instead of updating it
> directly.
>
> We're about to introduce a new command though that needs to detach
> "HEAD" while being more similar to git-cherry-pick(1) rathen than to
> git-rebase(1). As such, we'll want to restore "HEAD" to point to the
> branch that we started on while not using the more heavy-weight rebase
> machinery.
>
> Introduce a new option `restore_head_target` to do so. Persist the
> option into the sequencer configuration so that it persists across
> different processes, e.g. when we need to stop due to a merge conflict.
As with the last patch, can we use this new option in "git rebase"? The
sequencer is already a nest of conditionals, it would be nice to
minimize the number of new ones.
Thanks
Phillip
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> sequencer.c | 27 ++++++++++++++++++++++++++-
> sequencer.h | 3 +++
> 2 files changed, 29 insertions(+), 1 deletion(-)
>
> diff --git a/sequencer.c b/sequencer.c
> index 7066cdc939..bff181df76 100644
> --- a/sequencer.c
> +++ b/sequencer.c
> @@ -413,6 +413,7 @@ void replay_opts_release(struct replay_opts *opts)
> struct replay_ctx *ctx = opts->ctx;
>
> free(opts->gpg_sign);
> + free(opts->restore_head_target);
> free(opts->reflog_action);
> free(opts->default_strategy);
> free(opts->strategy);
> @@ -3142,6 +3143,8 @@ static int populate_opts_cb(const char *key, const char *value,
> } else if (!strcmp(key, "options.skip-commit-summary")) {
> opts->skip_commit_summary =
> git_config_bool_or_int(key, value, ctx->kvi, &error_flag);
> + } else if (!strcmp(key, "options.restore-head-target")) {
> + git_config_string_dup(&opts->restore_head_target, key, value);
> } else {
> return error(_("invalid key: %s"), key);
> }
> @@ -3709,6 +3712,10 @@ static int save_opts(struct replay_opts *opts)
> if (opts->skip_commit_summary)
> res |= repo_config_set_in_file_gently(the_repository, opts_file,
> "options.skip-commit-summary", NULL, "true");
> + if (opts->restore_head_target)
> + res |= repo_config_set_in_file_gently(the_repository, opts_file,
> + "options.restore-head-target", NULL, opts->restore_head_target);
> +
> return res;
> }
>
> @@ -5177,6 +5184,23 @@ static int pick_commits(struct repository *r,
> return -1;
> }
>
> + if (opts->restore_head_target) {
> + struct reset_head_opts reset_opts = { 0 };
> + const char *msg;
> +
> + msg = reflog_message(opts, "finish", "returning to %s", opts->restore_head_target);
> +
> + reset_opts.branch = opts->restore_head_target;
> + reset_opts.flags = RESET_HEAD_REFS_ONLY;
> + reset_opts.branch_msg = msg;
> + reset_opts.head_msg = msg;
> +
> + if (reset_head(r, &reset_opts)) {
> + error(_("could not switch HEAD back to %s"), opts->restore_head_target);
> + return -1;
> + }
> + }
> +
> /*
> * Sequence of picks finished successfully; cleanup by
> * removing the .git/sequencer directory
> @@ -5533,7 +5557,8 @@ int sequencer_pick_revisions(struct repository *r,
> if (opts->revs->cmdline.nr == 1 &&
> opts->revs->cmdline.rev->whence == REV_CMD_REV &&
> opts->revs->no_walk &&
> - !opts->revs->cmdline.rev->flags) {
> + !opts->revs->cmdline.rev->flags &&
> + !opts->restore_head_target) {
> struct commit *cmit;
>
> if (prepare_revision_walk(opts->revs)) {
> diff --git a/sequencer.h b/sequencer.h
> index 1767fd737e..a905f6afc7 100644
> --- a/sequencer.h
> +++ b/sequencer.h
> @@ -72,6 +72,9 @@ struct replay_opts {
> /* Reflog */
> char *reflog_action;
>
> + /* Reference to which HEAD shall be reset to after the operation. */
> + char *restore_head_target;
> +
> /* placeholder commit for -i --root */
> struct object_id squash_onto;
> int have_squash_onto;
>
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC v3 02/18] sequencer: add option to rewind HEAD after picking commits
2025-09-10 14:04 ` Phillip Wood
@ 2025-09-15 9:32 ` Patrick Steinhardt
2025-09-15 14:10 ` Phillip Wood
0 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-15 9:32 UTC (permalink / raw)
To: phillip.wood
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
On Wed, Sep 10, 2025 at 03:04:00PM +0100, Phillip Wood wrote:
> Hi Patrick
>
> On 04/09/2025 15:27, Patrick Steinhardt wrote:
> > While the sequencer infrastructure knows to rewind "HEAD" to whatever it
> > was pointing to before a rebase, it doesn't do the same for non-rebase
> > operations like cherry-picks. This is because the expectation is that
> > the user directly picks commits on top of whatever "HEAD" points to, and
> > we advance the reference pointed to by "HEAD" instead of updating it
> > directly.
> >
> > We're about to introduce a new command though that needs to detach
> > "HEAD" while being more similar to git-cherry-pick(1) rathen than to
> > git-rebase(1). As such, we'll want to restore "HEAD" to point to the
> > branch that we started on while not using the more heavy-weight rebase
> > machinery.
> >
> > Introduce a new option `restore_head_target` to do so. Persist the
> > option into the sequencer configuration so that it persists across
> > different processes, e.g. when we need to stop due to a merge conflict.
>
> As with the last patch, can we use this new option in "git rebase"? The
> sequencer is already a nest of conditionals, it would be nice to minimize
> the number of new ones.
You probably refer to the condition in `sequencer_pick_revisions()`
here? Everything else is basically new code.
Honestly, I don't dare touching that condition -- it's already quite
complex, and I wouldn't be surprised if changing it in any way would
cause regressions.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC v3 02/18] sequencer: add option to rewind HEAD after picking commits
2025-09-15 9:32 ` Patrick Steinhardt
@ 2025-09-15 14:10 ` Phillip Wood
0 siblings, 0 replies; 278+ messages in thread
From: Phillip Wood @ 2025-09-15 14:10 UTC (permalink / raw)
To: Patrick Steinhardt, phillip.wood
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
On 15/09/2025 10:32, Patrick Steinhardt wrote:
> On Wed, Sep 10, 2025 at 03:04:00PM +0100, Phillip Wood wrote:
>> Hi Patrick
>>
>> On 04/09/2025 15:27, Patrick Steinhardt wrote:
>
> You probably refer to the condition in `sequencer_pick_revisions()`
> here? Everything else is basically new code.
I was thinking of the code that restores HEAD at the end of
pick_commits() as rebase does something similar already (it looks like
you're using the same reflog message). Though thinking about it again
I'm not sure if "git history" detaches HEAD at the start like rebase
does. Rewriting the history with a detached HEAD is probably a good idea
as it will stop the branch reflog being polluted. One nice aspect of
rebase detaching HEAD is that at the end you can check the result by running
git range-diff $branch@{1}...HEAD
Thanks
Phillip
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH RFC v3 03/18] sequencer: introduce new history editing mode
2025-09-04 14:27 ` [PATCH RFC v3 00/18] " Patrick Steinhardt
2025-09-04 14:27 ` [PATCH RFC v3 01/18] sequencer: optionally skip printing commit summary Patrick Steinhardt
2025-09-04 14:27 ` [PATCH RFC v3 02/18] sequencer: add option to rewind HEAD after picking commits Patrick Steinhardt
@ 2025-09-04 14:27 ` Patrick Steinhardt
2025-09-04 14:27 ` [PATCH RFC v3 04/18] sequencer: stop using `the_repository` in `sequencer_remove_state()` Patrick Steinhardt
` (17 subsequent siblings)
20 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-04 14:27 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
Introduce a new history editing mode into our sequencer machinery. This
mode is basically the same as `REBASE_CHERRY`, but will be used by the
new git-history(1) command that is to be introduced in a subsequent
commit.
Note that the advice already points towards the git-history(1) command.
This advice is bogus right now, but we'll introduce the relevant infra
in a subsequent commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
sequencer.c | 116 +++++++++++++++++++++++++++++++++++++++++++++++-------------
sequencer.h | 3 +-
2 files changed, 93 insertions(+), 26 deletions(-)
diff --git a/sequencer.c b/sequencer.c
index bff181df76..9a66e7d128 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -465,6 +465,8 @@ static const char *action_name(const struct replay_opts *opts)
return N_("cherry-pick");
case REPLAY_INTERACTIVE_REBASE:
return N_("rebase");
+ case REPLAY_HISTORY_EDIT:
+ return N_("history edit");
}
die(_("unknown action: %d"), opts->action);
}
@@ -557,6 +559,13 @@ static void print_advice(struct repository *r, int show_hint,
"You can instead skip this commit with \"git revert --skip\".\n"
"To abort and get back to the state before \"git revert\",\n"
"run \"git revert --abort\"."));
+ else if (opts->action == REPLAY_HISTORY_EDIT)
+ advise_if_enabled(ADVICE_MERGE_CONFLICT,
+ _("After resolving the conflicts, mark them with\n"
+ "\"git add/rm <pathspec>\", then run\n"
+ "\"git history continue\".\n"
+ "To abort and get back to the state before \"git history\",\n"
+ "run \"git history abort\"."));
else
BUG("unexpected pick action in print_advice()");
}
@@ -1742,6 +1751,8 @@ static int do_commit(struct repository *r,
if (!res) {
refs_delete_ref(get_main_ref_store(r), "",
"CHERRY_PICK_HEAD", NULL, REF_NO_DEREF);
+ refs_delete_ref(get_main_ref_store(r), "",
+ "HISTORY_EDIT_HEAD", NULL, REF_NO_DEREF);
unlink(git_path_merge_msg(r));
if (!is_rebase_i(opts) && !opts->skip_commit_summary)
print_commit_summary(r, NULL, &oid,
@@ -2491,16 +2502,24 @@ static int do_pick_commit(struct repository *r,
* However, if the merge did not even start, then we don't want to
* write it at all.
*/
- if ((command == TODO_PICK || command == TODO_REWORD ||
- command == TODO_EDIT) && !opts->no_commit &&
- (res == 0 || res == 1) &&
- refs_update_ref(get_main_ref_store(the_repository), NULL, "CHERRY_PICK_HEAD", &commit->object.oid, NULL,
- REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR))
- res = -1;
- if (command == TODO_REVERT && ((opts->no_commit && res == 0) || res == 1) &&
- refs_update_ref(get_main_ref_store(the_repository), NULL, "REVERT_HEAD", &commit->object.oid, NULL,
- REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR))
- res = -1;
+ if (opts->action == REPLAY_HISTORY_EDIT && command == TODO_PICK &&
+ !opts->no_commit && (res == 0 || res == 1)) {
+ if (refs_update_ref(get_main_ref_store(the_repository), NULL,
+ "HISTORY_EDIT_HEAD", &commit->object.oid, NULL,
+ REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR))
+ res = -1;
+ } else if ((command == TODO_PICK || command == TODO_REWORD ||
+ command == TODO_EDIT) && !opts->no_commit && (res == 0 || res == 1)) {
+ if (refs_update_ref(get_main_ref_store(the_repository), NULL,
+ "CHERRY_PICK_HEAD", &commit->object.oid, NULL,
+ REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR))
+ res = -1;
+ } else if (command == TODO_REVERT && ((opts->no_commit && res == 0) || res == 1)) {
+ if (refs_update_ref(get_main_ref_store(the_repository), NULL,
+ "REVERT_HEAD", &commit->object.oid, NULL,
+ REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR))
+ res = -1;
+ }
if (res) {
error(command == TODO_REVERT
@@ -2526,6 +2545,8 @@ static int do_pick_commit(struct repository *r,
unlink(git_path_merge_msg(r));
refs_delete_ref(get_main_ref_store(r), "", "AUTO_MERGE",
NULL, REF_NO_DEREF);
+ refs_delete_ref(get_main_ref_store(r), "", "HISTORY_EDIT_HEAD",
+ NULL, REF_NO_DEREF);
fprintf(stderr,
_("dropping %s %s -- patch contents already upstream\n"),
oid_to_hex(&commit->object.oid), msg.subject);
@@ -2843,12 +2864,17 @@ static int parse_insn_line(struct repository *r, struct replay_opts *opts,
return 0;
}
-int sequencer_get_last_command(struct repository *r UNUSED, enum replay_action *action)
+int sequencer_get_last_command(struct repository *r, enum replay_action *action)
{
const char *todo_file, *bol;
struct strbuf buf = STRBUF_INIT;
int ret = 0;
+ if (refs_ref_exists(get_main_ref_store(r), "HISTORY_EDIT_HEAD")) {
+ *action = REPLAY_HISTORY_EDIT;
+ return 0;
+ }
+
todo_file = git_path_todo_file();
if (strbuf_read_file(&buf, todo_file, 0) < 0) {
if (errno == ENOENT || errno == ENOTDIR)
@@ -2995,6 +3021,15 @@ void sequencer_post_commit_cleanup(struct repository *r, int verbose)
need_cleanup = 1;
}
+ if (refs_ref_exists(get_main_ref_store(r), "HISTORY_EDIT_HEAD")) {
+ if (!refs_delete_ref(get_main_ref_store(r), "",
+ "HISTORY_EDIT_HEAD", NULL, REF_NO_DEREF) &&
+ verbose)
+ warning(_("cancelling a history edit in progress"));
+ opts.action = REPLAY_HISTORY_EDIT;
+ need_cleanup = 1;
+ }
+
if (refs_ref_exists(get_main_ref_store(r), "REVERT_HEAD")) {
if (!refs_delete_ref(get_main_ref_store(r), "", "REVERT_HEAD",
NULL, REF_NO_DEREF) &&
@@ -3028,6 +3063,19 @@ static void todo_list_write_total_nr(struct todo_list *todo_list)
}
}
+static enum todo_command action_to_command(enum replay_action action)
+{
+ switch (action) {
+ case REPLAY_PICK:
+ case REPLAY_HISTORY_EDIT:
+ return TODO_PICK;
+ case REPLAY_REVERT:
+ return TODO_REVERT;
+ default:
+ BUG("unsupported action %d", action);
+ }
+}
+
static int read_populate_todo(struct repository *r,
struct todo_list *todo_list,
struct replay_opts *opts)
@@ -3052,17 +3100,19 @@ static int read_populate_todo(struct repository *r,
return error(_("no commits parsed."));
if (!is_rebase_i(opts)) {
- enum todo_command valid =
- opts->action == REPLAY_PICK ? TODO_PICK : TODO_REVERT;
+ enum todo_command valid = action_to_command(opts->action);
int i;
- for (i = 0; i < todo_list->nr; i++)
+ for (i = 0; i < todo_list->nr; i++) {
if (valid == todo_list->items[i].command)
continue;
else if (valid == TODO_PICK)
- return error(_("cannot cherry-pick during a revert."));
+ return error(_("cannot cherry-pick during a %s."),
+ action_name(opts));
else
- return error(_("cannot revert during a cherry-pick."));
+ return error(_("cannot revert during a %s."),
+ action_name(opts));
+ }
}
if (is_rebase_i(opts)) {
@@ -3353,15 +3403,16 @@ int write_basic_state(struct replay_opts *opts, const char *head_name,
static int walk_revs_populate_todo(struct todo_list *todo_list,
struct replay_opts *opts)
{
- enum todo_command command = opts->action == REPLAY_PICK ?
- TODO_PICK : TODO_REVERT;
- const char *command_string = todo_command_info[command].str;
+ enum todo_command command;
+ const char *command_string;
const char *encoding;
struct commit *commit;
if (prepare_revs(opts))
return -1;
+ command = action_to_command(opts->action);
+ command_string = todo_command_info[command].str;
encoding = get_log_output_encoding();
while ((commit = get_revision(opts->revs))) {
@@ -3412,6 +3463,11 @@ static int create_seq_dir(struct repository *r)
in_progress_advice =
_("try \"git cherry-pick (--continue | %s--abort | --quit)\"");
break;
+ case REPLAY_HISTORY_EDIT:
+ in_progress_error = _("history edit is already in progress");
+ in_progress_advice =
+ _("try \"git history (continue | abort | quit)\"");
+ break;
default:
BUG("unexpected action in create_seq_dir");
}
@@ -3472,13 +3528,14 @@ static int reset_merge(const struct object_id *oid)
return run_command(&cmd);
}
-static int rollback_single_pick(struct repository *r)
+static int rollback_single_pick(struct repository *r, struct replay_opts *opts)
{
struct object_id head_oid;
if (!refs_ref_exists(get_main_ref_store(r), "CHERRY_PICK_HEAD") &&
+ !refs_ref_exists(get_main_ref_store(r), "HISTORY_EDIT_HEAD") &&
!refs_ref_exists(get_main_ref_store(r), "REVERT_HEAD"))
- return error(_("no cherry-pick or revert in progress"));
+ return error(_("no %s in progress"), action_name(opts));
if (refs_read_ref_full(get_main_ref_store(the_repository), "HEAD", 0, &head_oid, NULL))
return error(_("cannot resolve HEAD"));
if (is_null_oid(&head_oid))
@@ -3509,7 +3566,7 @@ int sequencer_rollback(struct repository *r, struct replay_opts *opts)
* If CHERRY_PICK_HEAD or REVERT_HEAD indicates
* a single-cherry-pick in progress, abort that.
*/
- return rollback_single_pick(r);
+ return rollback_single_pick(r, opts);
}
if (!f)
return error_errno(_("cannot open '%s'"), git_path_head_file());
@@ -5213,8 +5270,9 @@ static int continue_single_pick(struct repository *r, struct replay_opts *opts)
struct child_process cmd = CHILD_PROCESS_INIT;
if (!refs_ref_exists(get_main_ref_store(r), "CHERRY_PICK_HEAD") &&
+ !refs_ref_exists(get_main_ref_store(r), "HISTORY_EDIT_HEAD") &&
!refs_ref_exists(get_main_ref_store(r), "REVERT_HEAD"))
- return error(_("no cherry-pick or revert in progress"));
+ return error(_("no %s in progress"), action_name(opts));
cmd.git_cmd = 1;
strvec_push(&cmd.args, "commit");
@@ -5393,6 +5451,14 @@ static int commit_staged_changes(struct repository *r,
goto out;
}
+ if (refs_ref_exists(get_main_ref_store(r),
+ "HISTORY_EDIT_HEAD") &&
+ refs_delete_ref(get_main_ref_store(r), "",
+ "HISTORY_EDIT_HEAD", NULL, REF_NO_DEREF)) {
+ ret = error(_("could not remove HISTORY_EDIT_HEAD"));
+ goto out;
+ }
+
if (unlink(git_path_merge_msg(r)) && errno != ENOENT) {
ret = error_errno(_("could not remove '%s'"),
git_path_merge_msg(r));
@@ -5471,6 +5537,7 @@ int sequencer_continue(struct repository *r, struct replay_opts *opts)
/* Verify that the conflict has been resolved */
if (refs_ref_exists(get_main_ref_store(r),
"CHERRY_PICK_HEAD") ||
+ refs_ref_exists(get_main_ref_store(r), "HISTORY_EDIT_HEAD") ||
refs_ref_exists(get_main_ref_store(r), "REVERT_HEAD")) {
res = continue_single_pick(r, opts);
if (res)
@@ -5505,8 +5572,7 @@ static int single_pick(struct repository *r,
int check_todo;
struct todo_item item;
- item.command = opts->action == REPLAY_PICK ?
- TODO_PICK : TODO_REVERT;
+ item.command = action_to_command(opts->action);
item.commit = cmit;
return do_pick_commit(r, &item, opts, 0, &check_todo);
diff --git a/sequencer.h b/sequencer.h
index a905f6afc7..082fbe3e35 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -21,7 +21,8 @@ extern const char *rebase_resolvemsg;
enum replay_action {
REPLAY_REVERT,
REPLAY_PICK,
- REPLAY_INTERACTIVE_REBASE
+ REPLAY_INTERACTIVE_REBASE,
+ REPLAY_HISTORY_EDIT,
};
enum commit_msg_cleanup_mode {
--
2.51.0.417.g1ba7204a04.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH RFC v3 04/18] sequencer: stop using `the_repository` in `sequencer_remove_state()`
2025-09-04 14:27 ` [PATCH RFC v3 00/18] " Patrick Steinhardt
` (2 preceding siblings ...)
2025-09-04 14:27 ` [PATCH RFC v3 03/18] sequencer: introduce new history editing mode Patrick Steinhardt
@ 2025-09-04 14:27 ` Patrick Steinhardt
2025-09-04 14:27 ` [PATCH RFC v3 05/18] sequencer: wire up "rewritten-hook" for REPLAY_HISTORY_EDIT Patrick Steinhardt
` (16 subsequent siblings)
20 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-04 14:27 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
Refactor `sequencer_remove_state()` to stop using `the_repository` in
favor of a passed-in repository.
A lot of the other code in our sequencer infrastructure still uses
`the_repository`, but this bigger refactoring is left for another day.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
builtin/rebase.c | 4 ++--
builtin/revert.c | 2 +-
sequencer.c | 18 +++++++++---------
sequencer.h | 2 +-
4 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/builtin/rebase.c b/builtin/rebase.c
index 3c85768d29..66824ae136 100644
--- a/builtin/rebase.c
+++ b/builtin/rebase.c
@@ -568,7 +568,7 @@ static int finish_rebase(struct rebase_options *opts)
struct replay_opts replay = REPLAY_OPTS_INIT;
replay.action = REPLAY_INTERACTIVE_REBASE;
- ret = sequencer_remove_state(&replay);
+ ret = sequencer_remove_state(the_repository, &replay);
replay_opts_release(&replay);
} else {
strbuf_addstr(&dir, opts->state_dir);
@@ -1405,7 +1405,7 @@ int cmd_rebase(int argc,
struct replay_opts replay = REPLAY_OPTS_INIT;
replay.action = REPLAY_INTERACTIVE_REBASE;
- ret = sequencer_remove_state(&replay);
+ ret = sequencer_remove_state(the_repository, &replay);
replay_opts_release(&replay);
} else {
strbuf_reset(&buf);
diff --git a/builtin/revert.c b/builtin/revert.c
index c3f92b585d..6456cf2171 100644
--- a/builtin/revert.c
+++ b/builtin/revert.c
@@ -263,7 +263,7 @@ static int run_sequencer(int argc, const char **argv, const char *prefix,
free(options);
if (cmd == 'q') {
- int ret = sequencer_remove_state(opts);
+ int ret = sequencer_remove_state(the_repository, opts);
if (!ret)
remove_branch_state(the_repository, 0);
return ret;
diff --git a/sequencer.c b/sequencer.c
index 9a66e7d128..36e4db8526 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -425,7 +425,7 @@ void replay_opts_release(struct replay_opts *opts)
free(opts->ctx);
}
-int sequencer_remove_state(struct replay_opts *opts)
+int sequencer_remove_state(struct repository *repo, struct replay_opts *opts)
{
struct strbuf buf = STRBUF_INIT;
int ret = 0;
@@ -437,7 +437,7 @@ int sequencer_remove_state(struct replay_opts *opts)
char *eol = strchr(p, '\n');
if (eol)
*eol = '\0';
- if (refs_delete_ref(get_main_ref_store(the_repository), "(rebase) cleanup", p, NULL, 0) < 0) {
+ if (refs_delete_ref(get_main_ref_store(repo), "(rebase) cleanup", p, NULL, 0) < 0) {
warning(_("could not delete '%s'"), p);
ret = -1;
}
@@ -3048,7 +3048,7 @@ void sequencer_post_commit_cleanup(struct repository *r, int verbose)
if (!have_finished_the_last_pick())
goto out;
- sequencer_remove_state(&opts);
+ sequencer_remove_state(the_repository, &opts);
out:
replay_opts_release(&opts);
}
@@ -3595,7 +3595,7 @@ int sequencer_rollback(struct repository *r, struct replay_opts *opts)
if (reset_merge(&oid))
goto fail;
strbuf_release(&buf);
- return sequencer_remove_state(opts);
+ return sequencer_remove_state(the_repository, opts);
fail:
strbuf_release(&buf);
return -1;
@@ -4897,7 +4897,7 @@ static int checkout_onto(struct repository *r, struct replay_opts *opts,
};
if (reset_head(r, &ropts)) {
apply_autostash(rebase_path_autostash());
- sequencer_remove_state(opts);
+ sequencer_remove_state(the_repository, opts);
return error(_("could not detach HEAD"));
}
@@ -5262,7 +5262,7 @@ static int pick_commits(struct repository *r,
* Sequence of picks finished successfully; cleanup by
* removing the .git/sequencer directory
*/
- return sequencer_remove_state(opts);
+ return sequencer_remove_state(the_repository, opts);
}
static int continue_single_pick(struct repository *r, struct replay_opts *opts)
@@ -6593,7 +6593,7 @@ int complete_action(struct repository *r, struct replay_opts *opts, unsigned fla
if (count_commands(todo_list) == 0) {
apply_autostash(rebase_path_autostash());
- sequencer_remove_state(opts);
+ sequencer_remove_state(the_repository, opts);
return error(_("nothing to do"));
}
@@ -6604,12 +6604,12 @@ int complete_action(struct repository *r, struct replay_opts *opts, unsigned fla
return -1;
else if (res == -2) {
apply_autostash(rebase_path_autostash());
- sequencer_remove_state(opts);
+ sequencer_remove_state(the_repository, opts);
return -1;
} else if (res == -3) {
apply_autostash(rebase_path_autostash());
- sequencer_remove_state(opts);
+ sequencer_remove_state(the_repository, opts);
todo_list_release(&new_todo);
return error(_("nothing to do"));
diff --git a/sequencer.h b/sequencer.h
index 082fbe3e35..0e0e7301b8 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -170,7 +170,7 @@ int sequencer_continue(struct repository *repo, struct replay_opts *opts);
int sequencer_rollback(struct repository *repo, struct replay_opts *opts);
int sequencer_skip(struct repository *repo, struct replay_opts *opts);
void replay_opts_release(struct replay_opts *opts);
-int sequencer_remove_state(struct replay_opts *opts);
+int sequencer_remove_state(struct repository *repo, struct replay_opts *opts);
#define TODO_LIST_KEEP_EMPTY (1U << 0)
#define TODO_LIST_SHORTEN_IDS (1U << 1)
--
2.51.0.417.g1ba7204a04.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH RFC v3 05/18] sequencer: wire up "rewritten-hook" for REPLAY_HISTORY_EDIT
2025-09-04 14:27 ` [PATCH RFC v3 00/18] " Patrick Steinhardt
` (3 preceding siblings ...)
2025-09-04 14:27 ` [PATCH RFC v3 04/18] sequencer: stop using `the_repository` in `sequencer_remove_state()` Patrick Steinhardt
@ 2025-09-04 14:27 ` Patrick Steinhardt
2025-09-04 14:27 ` [PATCH RFC v3 06/18] cache-tree: allow writing in-memory index as tree Patrick Steinhardt
` (15 subsequent siblings)
20 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-04 14:27 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
While the sequencer already knows to drive REPLAY_HISTORY_EDIT, we
currently skip the execution of the "rewritten-hook" as it is only
specific to interactive rebases. We do want to execute this hook though
for commits we're rewriting in the upcoming git-history(1) command.
Wire up the infrastructure so that we also execute this hook with
REPLAY_HISTORY_EDIT.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
sequencer.c | 79 +++++++++++++++++++++++++++++++++++++++++++------------------
1 file changed, 56 insertions(+), 23 deletions(-)
diff --git a/sequencer.c b/sequencer.c
index 36e4db8526..61447e5ccf 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -70,6 +70,8 @@ static GIT_PATH_FUNC(git_path_seq_dir, "sequencer")
static GIT_PATH_FUNC(git_path_todo_file, "sequencer/todo")
static GIT_PATH_FUNC(git_path_opts_file, "sequencer/opts")
static GIT_PATH_FUNC(git_path_head_file, "sequencer/head")
+static GIT_PATH_FUNC(git_path_rewritten_list_file, "sequencer/rewritten-list")
+static GIT_PATH_FUNC(git_path_rewritten_pending_file, "sequencer/rewritten-pending")
static GIT_PATH_FUNC(git_path_abort_safety_file, "sequencer/abort-safety")
static GIT_PATH_FUNC(rebase_path, "rebase-merge")
@@ -2170,15 +2172,25 @@ static int update_squash_messages(struct repository *r,
return res;
}
-static void flush_rewritten_pending(void)
+static void flush_rewritten_pending(struct replay_opts *opts)
{
struct strbuf buf = STRBUF_INIT;
struct object_id newoid;
+ const char *pending_path;
+ const char *list_path;
FILE *out;
- if (strbuf_read_file(&buf, rebase_path_rewritten_pending(), (GIT_MAX_HEXSZ + 1) * 2) > 0 &&
+ if (opts->action == REPLAY_HISTORY_EDIT) {
+ pending_path = git_path_rewritten_pending_file();
+ list_path = git_path_rewritten_list_file();
+ } else {
+ pending_path = rebase_path_rewritten_pending();
+ list_path = rebase_path_rewritten_list();
+ }
+
+ if (strbuf_read_file(&buf, pending_path, (GIT_MAX_HEXSZ + 1) * 2) > 0 &&
!repo_get_oid(the_repository, "HEAD", &newoid) &&
- (out = fopen_or_warn(rebase_path_rewritten_list(), "a"))) {
+ (out = fopen_or_warn(list_path, "a"))) {
char *bol = buf.buf, *eol;
while (*bol) {
@@ -2190,16 +2202,24 @@ static void flush_rewritten_pending(void)
bol = eol + 1;
}
fclose(out);
- unlink(rebase_path_rewritten_pending());
+ unlink(pending_path);
}
strbuf_release(&buf);
}
static void record_in_rewritten(struct object_id *oid,
- enum todo_command next_command)
+ enum todo_command next_command,
+ struct replay_opts *opts)
{
- FILE *out = fopen_or_warn(rebase_path_rewritten_pending(), "a");
+ const char *path;
+ FILE *out;
+ if (opts->action == REPLAY_HISTORY_EDIT)
+ path = git_path_rewritten_pending_file();
+ else
+ path = rebase_path_rewritten_pending();
+
+ out = fopen_or_warn(path, "a");
if (!out)
return;
@@ -2207,7 +2227,7 @@ static void record_in_rewritten(struct object_id *oid,
fclose(out);
if (!is_fixup(next_command))
- flush_rewritten_pending();
+ flush_rewritten_pending(opts);
}
static int should_edit(struct replay_opts *opts) {
@@ -4984,9 +5004,9 @@ static int pick_one_commit(struct repository *r,
return error_with_patch(r, commit,
arg, item->arg_len, opts, res, !res);
}
- if (is_rebase_i(opts) && !res)
+ if ((is_rebase_i(opts) || opts->action == REPLAY_HISTORY_EDIT) && !res)
record_in_rewritten(&item->commit->object.oid,
- peek_command(todo_list, 1));
+ peek_command(todo_list, 1), opts);
if (res && is_fixup(item->command)) {
if (res == 1)
intend_to_amend();
@@ -5020,6 +5040,7 @@ static int pick_commits(struct repository *r,
struct todo_list *todo_list,
struct replay_opts *opts)
{
+ struct strbuf head_ref = STRBUF_INIT, buf = STRBUF_INIT;
struct replay_ctx *ctx = opts->ctx;
int res = 0, reschedule = 0;
@@ -5106,7 +5127,7 @@ static int pick_commits(struct repository *r,
reschedule = 1;
else if (item->commit)
record_in_rewritten(&item->commit->object.oid,
- peek_command(todo_list, 1));
+ peek_command(todo_list, 1), opts);
if (res > 0)
/* failed with merge conflicts */
return error_with_patch(r, item->commit,
@@ -5142,9 +5163,6 @@ static int pick_commits(struct repository *r,
}
if (is_rebase_i(opts)) {
- struct strbuf head_ref = STRBUF_INIT, buf = STRBUF_INIT;
- struct stat st;
-
if (read_oneliner(&head_ref, rebase_path_head_name(), 0) &&
starts_with(head_ref.buf, "refs/")) {
const char *msg;
@@ -5206,13 +5224,24 @@ static int pick_commits(struct repository *r,
}
release_revisions(&log_tree_opt);
}
- flush_rewritten_pending();
- if (!stat(rebase_path_rewritten_list(), &st) &&
- st.st_size > 0) {
+ }
+
+ if (is_rebase_i(opts) || opts->action == REPLAY_HISTORY_EDIT) {
+ const char *rewritten_list_path;
+ struct stat st;
+
+ flush_rewritten_pending(opts);
+
+ if (opts->action == REPLAY_HISTORY_EDIT)
+ rewritten_list_path = git_path_rewritten_list_file();
+ else
+ rewritten_list_path = rebase_path_rewritten_list();
+
+ if (!stat(rewritten_list_path, &st) && st.st_size > 0) {
struct child_process child = CHILD_PROCESS_INIT;
struct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;
- child.in = open(rebase_path_rewritten_list(), O_RDONLY);
+ child.in = open(rewritten_list_path, O_RDONLY);
child.git_cmd = 1;
strvec_push(&child.args, "notes");
strvec_push(&child.args, "copy");
@@ -5220,10 +5249,13 @@ static int pick_commits(struct repository *r,
/* we don't care if this copying failed */
run_command(&child);
- hook_opt.path_to_stdin = rebase_path_rewritten_list();
- strvec_push(&hook_opt.args, "rebase");
+ hook_opt.path_to_stdin = rewritten_list_path;
+ strvec_push(&hook_opt.args, is_rebase_i(opts) ? "rebase" : "history");
run_hooks_opt(r, "post-rewrite", &hook_opt);
}
+ }
+
+ if (is_rebase_i(opts)) {
apply_autostash(rebase_path_autostash());
if (!opts->quiet) {
@@ -5555,7 +5587,7 @@ int sequencer_continue(struct repository *r, struct replay_opts *opts)
if (read_oneliner(&buf, rebase_path_stopped_sha(),
READ_ONELINER_SKIP_IF_EMPTY) &&
!get_oid_hex(buf.buf, &oid))
- record_in_rewritten(&oid, peek_command(&todo_list, 0));
+ record_in_rewritten(&oid, peek_command(&todo_list, 0), opts);
strbuf_release(&buf);
}
@@ -6396,7 +6428,8 @@ int todo_list_write_to_file(struct repository *r, struct todo_list *todo_list,
/* skip picking commits whose parents are unchanged */
static int skip_unnecessary_picks(struct repository *r,
struct todo_list *todo_list,
- struct object_id *base_oid)
+ struct object_id *base_oid,
+ struct replay_opts *opts)
{
struct object_id *parent_oid;
int i;
@@ -6435,7 +6468,7 @@ static int skip_unnecessary_picks(struct repository *r,
todo_list->done_nr += i;
if (is_fixup(peek_command(todo_list, 0)))
- record_in_rewritten(base_oid, peek_command(todo_list, 0));
+ record_in_rewritten(base_oid, peek_command(todo_list, 0), opts);
}
return 0;
@@ -6630,7 +6663,7 @@ int complete_action(struct repository *r, struct replay_opts *opts, unsigned fla
BUG("invalid todo list after expanding IDs:\n%s",
new_todo.buf.buf);
- if (opts->allow_ff && skip_unnecessary_picks(r, &new_todo, &oid)) {
+ if (opts->allow_ff && skip_unnecessary_picks(r, &new_todo, &oid, opts)) {
todo_list_release(&new_todo);
return error(_("could not skip unnecessary pick commands"));
}
--
2.51.0.417.g1ba7204a04.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH RFC v3 06/18] cache-tree: allow writing in-memory index as tree
2025-09-04 14:27 ` [PATCH RFC v3 00/18] " Patrick Steinhardt
` (4 preceding siblings ...)
2025-09-04 14:27 ` [PATCH RFC v3 05/18] sequencer: wire up "rewritten-hook" for REPLAY_HISTORY_EDIT Patrick Steinhardt
@ 2025-09-04 14:27 ` Patrick Steinhardt
2025-09-04 14:27 ` [PATCH RFC v3 07/18] builtin: add new "history" command Patrick Steinhardt
` (14 subsequent siblings)
20 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-04 14:27 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
The function `write_in_core_index_as_tree()` takes a repository and
writes its index into a tree object. What this function cannot do though
is to take an _arbitrary_ in-memory index.
Introduce a new `struct index_state` parameter so that the caller can
pass a different index than the one belonging to the repository. This
will be used in a subsequent commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
builtin/checkout.c | 3 ++-
cache-tree.c | 5 ++---
cache-tree.h | 3 ++-
3 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/builtin/checkout.c b/builtin/checkout.c
index f9453473fe2..43583c8d1be 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -902,7 +902,8 @@ static int merge_working_tree(const struct checkout_opts *opts,
0);
init_ui_merge_options(&o, the_repository);
o.verbosity = 0;
- work = write_in_core_index_as_tree(the_repository);
+ work = write_in_core_index_as_tree(the_repository,
+ the_repository->index);
ret = reset_tree(new_tree,
opts, 1,
diff --git a/cache-tree.c b/cache-tree.c
index 66ef2becbe0..029ec933abe 100644
--- a/cache-tree.c
+++ b/cache-tree.c
@@ -699,11 +699,11 @@ static int write_index_as_tree_internal(struct object_id *oid,
return 0;
}
-struct tree* write_in_core_index_as_tree(struct repository *repo) {
+struct tree *write_in_core_index_as_tree(struct repository *repo,
+ struct index_state *index_state) {
struct object_id o;
int was_valid, ret;
- struct index_state *index_state = repo->index;
was_valid = index_state->cache_tree &&
cache_tree_fully_valid(index_state->cache_tree);
@@ -723,7 +723,6 @@ struct tree* write_in_core_index_as_tree(struct repository *repo) {
return lookup_tree(repo, &index_state->cache_tree->oid);
}
-
int write_index_as_tree(struct object_id *oid, struct index_state *index_state, const char *index_path, int flags, const char *prefix)
{
int entries, was_valid;
diff --git a/cache-tree.h b/cache-tree.h
index b82c4963e7c..f8bddae5235 100644
--- a/cache-tree.h
+++ b/cache-tree.h
@@ -47,7 +47,8 @@ int cache_tree_verify(struct repository *, struct index_state *);
#define WRITE_TREE_UNMERGED_INDEX (-2)
#define WRITE_TREE_PREFIX_ERROR (-3)
-struct tree* write_in_core_index_as_tree(struct repository *repo);
+struct tree *write_in_core_index_as_tree(struct repository *repo,
+ struct index_state *index_state);
int write_index_as_tree(struct object_id *oid, struct index_state *index_state, const char *index_path, int flags, const char *prefix);
void prime_cache_tree(struct repository *, struct index_state *, struct tree *);
--
2.51.0.417.g1ba7204a04.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH RFC v3 07/18] builtin: add new "history" command
2025-09-04 14:27 ` [PATCH RFC v3 00/18] " Patrick Steinhardt
` (5 preceding siblings ...)
2025-09-04 14:27 ` [PATCH RFC v3 06/18] cache-tree: allow writing in-memory index as tree Patrick Steinhardt
@ 2025-09-04 14:27 ` Patrick Steinhardt
2025-09-04 14:27 ` [PATCH RFC v3 08/18] builtin/history: introduce subcommands to manage interrupted rewrites Patrick Steinhardt
` (13 subsequent siblings)
20 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-04 14:27 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
When rewriting history via git-rebase(1) there are a couple of very
common use cases:
- The ordering of two commits should be reversed.
- A commit should be split up into two commits.
- A commit should be dropped from the history completely.
- Multiple commits should be squashed into one.
While these operations are all doable, it often feels needlessly cludgy
to do so by doing an interactive rebase, using the editor to say what
one wants, and then perform the actions. Furthermore, some operations
like splitting up a commit into two are way more involved than that and
require a whole series of commands.
Add a new "history" command to plug this gap. This command will have
several different subcommands to imperatively rewrite history for common
use cases like the above. These commands will be implemented in
subsequent commits.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
.gitignore | 1 +
Documentation/git-history.adoc | 45 ++++++++++++++++++++++++++++++++++++++++++
Documentation/meson.build | 1 +
Makefile | 1 +
builtin.h | 1 +
builtin/history.c | 20 +++++++++++++++++++
command-list.txt | 1 +
git.c | 1 +
meson.build | 1 +
t/meson.build | 3 ++-
t/t3450-history.sh | 12 +++++++++++
11 files changed, 86 insertions(+), 1 deletion(-)
diff --git a/.gitignore b/.gitignore
index 04c444404e..3932d4d618 100644
--- a/.gitignore
+++ b/.gitignore
@@ -77,6 +77,7 @@
/git-grep
/git-hash-object
/git-help
+/git-history
/git-hook
/git-http-backend
/git-http-fetch
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
new file mode 100644
index 0000000000..1537960374
--- /dev/null
+++ b/Documentation/git-history.adoc
@@ -0,0 +1,45 @@
+git-history(1)
+==============
+
+NAME
+----
+git-history - EXPERIMENTAL: Rewrite history of the current branch
+
+SYNOPSIS
+--------
+[synopsis]
+git history [<options>]
+
+DESCRIPTION
+-----------
+
+Rewrite history by rearranging or modifying specific commits in the
+history.
+
+This command is similar to linkgit:git-rebase[1] and uses the same
+underlying machinery. You should use rebases if you either want to
+reapply a range of commits onto a different base, or interactive rebases
+if you want to edit a range of commits.
+
+Note that this command does not (yet) work with histories that contain
+merges. You should use linkgit:git-rebase[1] with the `--rebase-merges`
+flag instead.
+
+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
+
+COMMANDS
+--------
+
+This command requires a subcommand. Several subcommands are available to
+rewrite history in different ways:
+
+CONFIGURATION
+-------------
+
+include::includes/cmd-config-section-all.adoc[]
+
+include::config/sequencer.adoc[]
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Documentation/meson.build b/Documentation/meson.build
index 4404c623f0..a30b5307fd 100644
--- a/Documentation/meson.build
+++ b/Documentation/meson.build
@@ -64,6 +64,7 @@ manpages = {
'git-gui.adoc' : 1,
'git-hash-object.adoc' : 1,
'git-help.adoc' : 1,
+ 'git-history.adoc' : 1,
'git-hook.adoc' : 1,
'git-http-backend.adoc' : 1,
'git-http-fetch.adoc' : 1,
diff --git a/Makefile b/Makefile
index e11340c1ae..bed6eda5e6 100644
--- a/Makefile
+++ b/Makefile
@@ -1261,6 +1261,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o
BUILTIN_OBJS += builtin/grep.o
BUILTIN_OBJS += builtin/hash-object.o
BUILTIN_OBJS += builtin/help.o
+BUILTIN_OBJS += builtin/history.o
BUILTIN_OBJS += builtin/hook.o
BUILTIN_OBJS += builtin/index-pack.o
BUILTIN_OBJS += builtin/init-db.o
diff --git a/builtin.h b/builtin.h
index bff13e3069..2934f4479a 100644
--- a/builtin.h
+++ b/builtin.h
@@ -172,6 +172,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix, struc
int cmd_grep(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_hash_object(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_help(int argc, const char **argv, const char *prefix, struct repository *repo);
+int cmd_history(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_hook(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_index_pack(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_init_db(int argc, const char **argv, const char *prefix, struct repository *repo);
diff --git a/builtin/history.c b/builtin/history.c
new file mode 100644
index 0000000000..d1a40368e0
--- /dev/null
+++ b/builtin/history.c
@@ -0,0 +1,20 @@
+#include "builtin.h"
+#include "gettext.h"
+#include "parse-options.h"
+
+int cmd_history(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo UNUSED)
+{
+ const char * const usage[] = {
+ N_("git history [<options>]"),
+ NULL,
+ };
+ struct option options[] = {
+ OPT_END(),
+ };
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ return 0;
+}
diff --git a/command-list.txt b/command-list.txt
index b7ade3ab9f..f95f0ce926 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -115,6 +115,7 @@ git-grep mainporcelain info
git-gui mainporcelain
git-hash-object plumbingmanipulators
git-help ancillaryinterrogators complete
+git-history mainporcelain history
git-hook purehelpers
git-http-backend synchingrepositories
git-http-fetch synchelpers
diff --git a/git.c b/git.c
index 83eac0aeab..9d2cba2906 100644
--- a/git.c
+++ b/git.c
@@ -560,6 +560,7 @@ static struct cmd_struct commands[] = {
{ "grep", cmd_grep, RUN_SETUP_GENTLY },
{ "hash-object", cmd_hash_object },
{ "help", cmd_help },
+ { "history", cmd_history, RUN_SETUP },
{ "hook", cmd_hook, RUN_SETUP },
{ "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },
{ "init", cmd_init_db },
diff --git a/meson.build b/meson.build
index 5dd299b496..0e40778a23 100644
--- a/meson.build
+++ b/meson.build
@@ -603,6 +603,7 @@ builtin_sources = [
'builtin/grep.c',
'builtin/hash-object.c',
'builtin/help.c',
+ 'builtin/history.c',
'builtin/hook.c',
'builtin/index-pack.c',
'builtin/init-db.c',
diff --git a/t/meson.build b/t/meson.build
index bbeba1a8d5..966d7c14f4 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -376,6 +376,7 @@ integration_tests = [
't3436-rebase-more-options.sh',
't3437-rebase-fixup-options.sh',
't3438-rebase-broken-files.sh',
+ 't3450-history.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
@@ -1214,4 +1215,4 @@ if perl.found() and time.found()
timeout: 0,
)
endforeach
-endif
\ No newline at end of file
+endif
diff --git a/t/t3450-history.sh b/t/t3450-history.sh
new file mode 100755
index 0000000000..9eb1ed6749
--- /dev/null
+++ b/t/t3450-history.sh
@@ -0,0 +1,12 @@
+#!/bin/sh
+
+test_description='tests for git-history command'
+
+. ./test-lib.sh
+
+test_expect_success 'refuses to do anything without subcommand' '
+ test_must_fail git history 2>err &&
+ test_grep foo err
+'
+
+test_done
--
2.51.0.417.g1ba7204a04.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH RFC v3 08/18] builtin/history: introduce subcommands to manage interrupted rewrites
2025-09-04 14:27 ` [PATCH RFC v3 00/18] " Patrick Steinhardt
` (6 preceding siblings ...)
2025-09-04 14:27 ` [PATCH RFC v3 07/18] builtin: add new "history" command Patrick Steinhardt
@ 2025-09-04 14:27 ` Patrick Steinhardt
2025-09-04 14:27 ` [PATCH RFC v3 09/18] builtin/history: implement "drop" subcommand Patrick Steinhardt
` (12 subsequent siblings)
20 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-04 14:27 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
Introduce subcommands to manage the sequencer state for git-history(1).
These aren't really useful yet, but will become useful in subsequent
commits where we will introduce git-history(1) subcommands that actually
edit history.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-history.adoc | 20 +++++++-
builtin/history.c | 114 +++++++++++++++++++++++++++++++++++++++--
t/t3450-history.sh | 32 +++++++++++-
3 files changed, 161 insertions(+), 5 deletions(-)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index 1537960374..3e9a789b83 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -8,7 +8,9 @@ git-history - EXPERIMENTAL: Rewrite history of the current branch
SYNOPSIS
--------
[synopsis]
-git history [<options>]
+git history abort
+git history continue
+git history quit
DESCRIPTION
-----------
@@ -33,6 +35,22 @@ COMMANDS
This command requires a subcommand. Several subcommands are available to
rewrite history in different ways:
+The following commands are used to manage an interrupted history-rewriting
+operation:
+
+`abort`::
+ Abort the history-rewriting operation and reset HEAD to the original
+ branch.
+
+`continue`::
+ Restart the history-rewriting process after having resolved a merge
+ conflict.
+
+`quit`::
+ Abort the history-rewriting operation but `HEAD` is not reset back to
+ the original branch. The index and working tree are also left unchanged
+ as a result.
+
CONFIGURATION
-------------
diff --git a/builtin/history.c b/builtin/history.c
index d1a40368e0..0ad45dbfef 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -1,20 +1,128 @@
#include "builtin.h"
+#include "branch.h"
#include "gettext.h"
#include "parse-options.h"
+#include "sequencer.h"
+
+static int cmd_history_abort(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo)
+{
+ const char * const usage[] = {
+ N_("git history abort"),
+ NULL,
+ };
+ struct option options[] = {
+ OPT_END(),
+ };
+ struct replay_opts opts = REPLAY_OPTS_INIT;
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc) {
+ ret = error(_("command does not take arguments"));
+ goto out;
+ }
+
+ opts.action = REPLAY_HISTORY_EDIT;
+ ret = sequencer_rollback(repo, &opts);
+ if (ret)
+ goto out;
+
+ ret = 0;
+
+out:
+ replay_opts_release(&opts);
+ return ret;
+}
+
+static int cmd_history_continue(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo)
+{
+ const char * const usage[] = {
+ N_("git history continue"),
+ NULL,
+ };
+ struct option options[] = {
+ OPT_END(),
+ };
+ struct replay_opts opts = REPLAY_OPTS_INIT;
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc) {
+ ret = error(_("command does not take arguments"));
+ goto out;
+ }
+
+ opts.action = REPLAY_HISTORY_EDIT;
+ ret = sequencer_continue(repo, &opts);
+ if (ret)
+ goto out;
+
+ ret = 0;
+
+out:
+ replay_opts_release(&opts);
+ return ret;
+}
+
+static int cmd_history_quit(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo)
+{
+ const char * const usage[] = {
+ N_("git history quit"),
+ NULL,
+ };
+ struct option options[] = {
+ OPT_END(),
+ };
+ struct replay_opts opts = REPLAY_OPTS_INIT;
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc) {
+ ret = error(_("command does not take arguments"));
+ goto out;
+ }
+
+ opts.action = REPLAY_HISTORY_EDIT;
+ ret = sequencer_remove_state(repo, &opts);
+ if (ret)
+ goto out;
+ remove_branch_state(repo, 0);
+
+ ret = 0;
+
+out:
+ replay_opts_release(&opts);
+ return ret;
+}
int cmd_history(int argc,
const char **argv,
const char *prefix,
- struct repository *repo UNUSED)
+ struct repository *repo)
{
const char * const usage[] = {
- N_("git history [<options>]"),
+ N_("git history abort"),
+ N_("git history continue"),
+ N_("git history quit"),
NULL,
};
+ parse_opt_subcommand_fn *fn = NULL;
struct option options[] = {
+ OPT_SUBCOMMAND("abort", &fn, cmd_history_abort),
+ OPT_SUBCOMMAND("continue", &fn, cmd_history_continue),
+ OPT_SUBCOMMAND("quit", &fn, cmd_history_quit),
OPT_END(),
};
argc = parse_options(argc, argv, prefix, options, usage, 0);
- return 0;
+ return fn(argc, argv, prefix, repo);
}
diff --git a/t/t3450-history.sh b/t/t3450-history.sh
index 9eb1ed6749..aa9d44c03b 100755
--- a/t/t3450-history.sh
+++ b/t/t3450-history.sh
@@ -6,7 +6,37 @@ test_description='tests for git-history command'
test_expect_success 'refuses to do anything without subcommand' '
test_must_fail git history 2>err &&
- test_grep foo err
+ test_grep "need a subcommand" err
+'
+
+test_expect_success 'abort complains about arguments' '
+ test_must_fail git history abort foo 2>err &&
+ test_grep "command does not take arguments" err
+'
+
+test_expect_success 'abort complains when no history edit is active' '
+ test_must_fail git history abort 2>err &&
+ test_grep "no history edit in progress" err
+'
+
+test_expect_success 'continue complains about arguments' '
+ test_must_fail git history continue foo 2>err &&
+ test_grep "command does not take arguments" err
+'
+
+test_expect_success 'continue complains when no history edit is active' '
+ test_must_fail git history continue 2>err &&
+ test_grep "no history edit in progress" err
+'
+
+test_expect_success 'quit complains about arguments' '
+ test_must_fail git history quit foo 2>err &&
+ test_grep "command does not take arguments" err
+'
+
+test_expect_success 'quit does not complain when no history edit is active' '
+ git history quit 2>err &&
+ test_must_be_empty err
'
test_done
--
2.51.0.417.g1ba7204a04.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH RFC v3 09/18] builtin/history: implement "drop" subcommand
2025-09-04 14:27 ` [PATCH RFC v3 00/18] " Patrick Steinhardt
` (7 preceding siblings ...)
2025-09-04 14:27 ` [PATCH RFC v3 08/18] builtin/history: introduce subcommands to manage interrupted rewrites Patrick Steinhardt
@ 2025-09-04 14:27 ` Patrick Steinhardt
2025-09-04 14:27 ` [PATCH RFC v3 10/18] builtin/history: implement "reorder" subcommand Patrick Steinhardt
` (11 subsequent siblings)
20 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-04 14:27 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
It is a fairly common operation to perform an interactive rebase so that
one of the commits can be dropped from history. Doing this is not very
hard in general, but still requires the user to perform multiple steps:
1. Identify the commit in question that is to be dropped.
2. Perform an interactive rebase on top of that commit's parent.
3. Edit the instruction sheet to drop that commit.
This is needlessly complex for such a supposedly-trivial operation.
Furthermore, the second step doesn't account for certain edge cases like
for example dropping the root commit.
Introduce a new "drop" subcommand to make this use case significantly
simpler: all the user needs to do is to say `git history drop $COMMIT`
and they're done.
Note that for now, this command only allows users to drop a single
commit at once. It should be easy enough though to expand the command at
a later point in time to support dropping whole commit ranges.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-history.adoc | 27 ++++
builtin/history.c | 287 +++++++++++++++++++++++++++++++++++++++++
t/meson.build | 1 +
t/t3451-history-drop.sh | 207 +++++++++++++++++++++++++++++
4 files changed, 522 insertions(+)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index 3e9a789b83..39c9f1e25e 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -11,6 +11,7 @@ SYNOPSIS
git history abort
git history continue
git history quit
+git history drop <commit>
DESCRIPTION
-----------
@@ -35,6 +36,15 @@ COMMANDS
This command requires a subcommand. Several subcommands are available to
rewrite history in different ways:
+`drop <commit>`::
+ Drop a commit from the history and reapply all children of that
+ commit on top of the commit's parent. The commit that is to be
+ dropped must be reachable from the currently checked-out commit.
++
+Dropping the root commit converts the child of that commit into the new
+root commit. It is invalid to drop a root commit that does not have any
+child commits, as that would lead to an empty branch.
+
The following commands are used to manage an interrupted history-rewriting
operation:
@@ -51,6 +61,23 @@ operation:
the original branch. The index and working tree are also left unchanged
as a result.
+EXAMPLES
+--------
+
+Drop a commit from history
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+----------
+$ git log --oneline
+2d4cd6d third
+125a0f3 second
+e098c27 first
+$ git history drop HEAD~
+$ git log
+b1bc1bd third
+e098c27 first
+----------
+
CONFIGURATION
-------------
diff --git a/builtin/history.c b/builtin/history.c
index 0ad45dbfef..2132b6a441 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -1,7 +1,16 @@
#include "builtin.h"
#include "branch.h"
+#include "commit.h"
+#include "commit-reach.h"
+#include "config.h"
+#include "environment.h"
#include "gettext.h"
+#include "hex.h"
+#include "object-name.h"
#include "parse-options.h"
+#include "refs.h"
+#include "reset.h"
+#include "revision.h"
#include "sequencer.h"
static int cmd_history_abort(int argc,
@@ -104,6 +113,282 @@ static int cmd_history_quit(int argc,
return ret;
}
+static int collect_commits(struct repository *repo,
+ struct commit *old_commit,
+ struct commit *new_commit,
+ struct strvec *out)
+{
+ struct setup_revision_opt revision_opts = {
+ .assume_dashdash = 1,
+ };
+ struct strvec revisions = STRVEC_INIT;
+ struct commit_list *from_list = NULL;
+ struct commit *child;
+ struct rev_info rev = { 0 };
+ int ret;
+
+ /*
+ * Check that the old commit actually is an ancestor of HEAD. If not
+ * the whole request becomes nonsensical.
+ */
+ if (old_commit) {
+ commit_list_insert(old_commit, &from_list);
+ if (!repo_is_descendant_of(repo, new_commit, from_list)) {
+ ret = error(_("commit must be reachable from current HEAD commit"));
+ goto out;
+ }
+ }
+
+ repo_init_revisions(repo, &rev, NULL);
+ strvec_push(&revisions, "");
+ strvec_push(&revisions, oid_to_hex(&new_commit->object.oid));
+ if (old_commit)
+ strvec_pushf(&revisions, "^%s", oid_to_hex(&old_commit->object.oid));
+ if (setup_revisions(revisions.nr, revisions.v, &rev, &revision_opts) != 1 ||
+ prepare_revision_walk(&rev)) {
+ ret = error(_("revision walk setup failed"));
+ goto out;
+ }
+
+ while ((child = get_revision(&rev))) {
+ if (old_commit && !child->parents)
+ BUG("revision walk did not find child commit");
+ if (child->parents && child->parents->next) {
+ ret = error(_("cannot rearrange commit history with merges"));
+ goto out;
+ }
+
+ strvec_push(out, oid_to_hex(&child->object.oid));
+
+ if (child->parents && old_commit &&
+ commit_list_contains(old_commit, child->parents))
+ break;
+ }
+
+ /*
+ * Revisions are in newest-order-first. We have to reverse the
+ * array though so that we pick the oldest commits first.
+ */
+ for (size_t i = 0, j = out->nr - 1; i < j; i++, j--)
+ SWAP(out->v[i], out->v[j]);
+
+ ret = 0;
+
+out:
+ free_commit_list(from_list);
+ strvec_clear(&revisions);
+ release_revisions(&rev);
+ reset_revision_walk();
+ return ret;
+}
+
+static int apply_commits(struct repository *repo,
+ const struct strvec *commits,
+ struct commit *head,
+ struct commit *base,
+ const char *action)
+{
+ struct setup_revision_opt revision_opts = {
+ .assume_dashdash = 1,
+ };
+ struct replay_opts replay_opts = REPLAY_OPTS_INIT;
+ struct reset_head_opts reset_opts = { 0 };
+ struct object_id root_commit;
+ struct strvec args = STRVEC_INIT;
+ struct strbuf buf = STRBUF_INIT;
+ char hex[GIT_MAX_HEXSZ + 1];
+ int ref_flags, ret;
+
+ /*
+ * We have performed all safety checks, so we now prepare
+ * replaying the commits.
+ */
+ replay_opts.action = REPLAY_HISTORY_EDIT;
+ sequencer_init_config(&replay_opts);
+ replay_opts.quiet = 1;
+ replay_opts.skip_commit_summary = 1;
+ if (!replay_opts.strategy && replay_opts.default_strategy) {
+ replay_opts.strategy = replay_opts.default_strategy;
+ replay_opts.default_strategy = NULL;
+ }
+
+ strvec_push(&args, "");
+ strvec_pushv(&args, commits->v);
+
+ replay_opts.revs = xmalloc(sizeof(*replay_opts.revs));
+ repo_init_revisions(repo, replay_opts.revs, NULL);
+ replay_opts.revs->no_walk = 1;
+ replay_opts.revs->unsorted_input = 1;
+ if (setup_revisions(args.nr, args.v, replay_opts.revs,
+ &revision_opts) != 1) {
+ ret = error(_("setting up revisions failed"));
+ goto out;
+ }
+
+ /*
+ * If we're dropping the root commit we first need to create
+ * a new empty root. We then instruct the seqencer machinery to
+ * squash that root commit with the first commit we're picking
+ * onto it.
+ */
+ if (!base->parents) {
+ if (commit_tree("", 0, repo->hash_algo->empty_tree, NULL,
+ &root_commit, NULL, NULL) < 0) {
+ ret = error(_("Could not create new root commit"));
+ goto out;
+ }
+
+ replay_opts.squash_onto = root_commit;
+ replay_opts.have_squash_onto = 1;
+ reset_opts.oid = &root_commit;
+ } else {
+ reset_opts.oid = &base->parents->item->object.oid;
+ }
+
+ replay_opts.restore_head_target =
+ xstrdup_or_null(refs_resolve_ref_unsafe(get_main_ref_store(repo),
+ "HEAD", 0, NULL, &ref_flags));
+ if (!(ref_flags & REF_ISSYMREF))
+ FREE_AND_NULL(replay_opts.restore_head_target);
+
+ /*
+ * Perform a hard-reset to the parent of our commit that is to
+ * be dropped. This is the new base onto which we'll pick all
+ * the descendants.
+ */
+ strbuf_addf(&buf, "%s (start): checkout %s", action,
+ oid_to_hex_r(hex, reset_opts.oid));
+ reset_opts.orig_head = &head->object.oid;
+ reset_opts.flags = RESET_HEAD_DETACH | RESET_ORIG_HEAD;
+ reset_opts.head_msg = buf.buf;
+ reset_opts.default_reflog_action = action;
+ if (reset_head(repo, &reset_opts) < 0) {
+ ret = error(_("could not switch to %s"), oid_to_hex(reset_opts.oid));
+ goto out;
+ }
+
+ ret = sequencer_pick_revisions(repo, &replay_opts);
+ if (ret < 0) {
+ ret = error(_("could not pick commits"));
+ goto out;
+ } else if (ret > 0) {
+ /*
+ * A positive return value indicates we've got a merge
+ * conflict. Bail out, but don't print a message as
+ * `sequencer_pick_revisions()` already printed enough
+ * information.
+ */
+ ret = -1;
+ goto out;
+ }
+
+ ret = 0;
+
+out:
+ replay_opts_release(&replay_opts);
+ strbuf_release(&buf);
+ strvec_clear(&args);
+ return ret;
+}
+
+static int cmd_history_drop(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo)
+{
+ const char * const usage[] = {
+ N_("git history drop <commit>"),
+ NULL,
+ };
+ struct option options[] = {
+ OPT_END(),
+ };
+ struct commit *commit_to_drop, *head;
+ struct strvec commits = STRVEC_INIT;
+ struct strbuf buf = STRBUF_INIT;
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc != 1) {
+ ret = error(_("command expects a single revision"));
+ goto out;
+ }
+ repo_config(repo, git_default_config, NULL);
+
+ commit_to_drop = lookup_commit_reference_by_name(argv[0]);
+ if (!commit_to_drop) {
+ ret = error(_("commit to be dropped cannot be found: %s"), argv[0]);
+ goto out;
+ }
+ if (commit_to_drop->parents && commit_to_drop->parents->next) {
+ ret = error(_("commit to be dropped must not be a merge commit"));
+ goto out;
+ }
+
+ head = lookup_commit_reference_by_name("HEAD");
+ if (!head) {
+ ret = error(_("could not resolve HEAD to a commit"));
+ goto out;
+ }
+
+ if (oideq(&commit_to_drop->object.oid, &head->object.oid)) {
+ /*
+ * If we want to drop the tip of the current branch we don't
+ * have to perform any rebase at all. Instead, we simply
+ * perform a hard reset to the parent commit.
+ */
+ struct reset_head_opts reset_opts = {
+ .orig_head = &head->object.oid,
+ .flags = RESET_ORIG_HEAD,
+ .default_reflog_action = "drop",
+ };
+ char hex[GIT_MAX_HEXSZ + 1];
+
+ if (!commit_to_drop->parents) {
+ ret = error(_("cannot drop the only commit on this branch"));
+ goto out;
+ }
+
+ oid_to_hex_r(hex, &commit_to_drop->parents->item->object.oid);
+ strbuf_addf(&buf, "drop (start): checkout %s", hex);
+ reset_opts.oid = &commit_to_drop->parents->item->object.oid;
+ reset_opts.head_msg = buf.buf;
+
+ if (reset_head(repo, &reset_opts) < 0) {
+ ret = error(_("could not switch to %s"), hex);
+ goto out;
+ }
+ } else {
+ /*
+ * Prepare a revision walk from old commit to the commit that is
+ * about to be dropped. This serves three purposes:
+ *
+ * - We verify that the history doesn't contain any merges.
+ * For now, merges aren't yet handled by us.
+ *
+ * - We need to find the child of the commit-to-be-dropped.
+ * This child is what will be adopted by the parent of the
+ * commit that we are about to drop.
+ *
+ * - We compute the list of commits-to-be-picked.
+ */
+ ret = collect_commits(repo, commit_to_drop, head, &commits);
+ if (ret < 0)
+ goto out;
+
+ ret = apply_commits(repo, &commits, head, commit_to_drop, "drop");
+ if (ret < 0)
+ goto out;
+ }
+
+ ret = 0;
+
+out:
+ strvec_clear(&commits);
+ strbuf_release(&buf);
+ return ret;
+}
+
int cmd_history(int argc,
const char **argv,
const char *prefix,
@@ -113,6 +398,7 @@ int cmd_history(int argc,
N_("git history abort"),
N_("git history continue"),
N_("git history quit"),
+ N_("git history drop <commit>"),
NULL,
};
parse_opt_subcommand_fn *fn = NULL;
@@ -120,6 +406,7 @@ int cmd_history(int argc,
OPT_SUBCOMMAND("abort", &fn, cmd_history_abort),
OPT_SUBCOMMAND("continue", &fn, cmd_history_continue),
OPT_SUBCOMMAND("quit", &fn, cmd_history_quit),
+ OPT_SUBCOMMAND("drop", &fn, cmd_history_drop),
OPT_END(),
};
diff --git a/t/meson.build b/t/meson.build
index 966d7c14f4..8189c6c561 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -377,6 +377,7 @@ integration_tests = [
't3437-rebase-fixup-options.sh',
't3438-rebase-broken-files.sh',
't3450-history.sh',
+ 't3451-history-drop.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
diff --git a/t/t3451-history-drop.sh b/t/t3451-history-drop.sh
new file mode 100755
index 0000000000..4fc295ef21
--- /dev/null
+++ b/t/t3451-history-drop.sh
@@ -0,0 +1,207 @@
+#!/bin/sh
+
+test_description='tests for git-history drop subcommand'
+
+. ./test-lib.sh
+
+test_expect_success 'refuses to work with merge commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ git branch branch &&
+ test_commit ours &&
+ git switch branch &&
+ test_commit theirs &&
+ git switch - &&
+ git merge theirs &&
+ test_must_fail git history drop HEAD 2>err &&
+ test_grep "commit to be dropped must not be a merge commit" err &&
+ test_must_fail git history drop HEAD~ 2>err &&
+ test_grep "cannot rearrange commit history with merges" err
+ )
+'
+
+test_expect_success 'refuses to work when history becomes empty' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ test_must_fail git history drop HEAD 2>err &&
+ test_grep "cannot drop the only commit on this branch" err
+ )
+'
+
+test_expect_success 'refuses to work with changes in the worktree or index' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit initial &&
+ test_commit file file &&
+ echo foo >file &&
+ test_must_fail git history drop HEAD 2>err &&
+ test_grep "Your local changes to the following files would be overwritten" err &&
+ git add file &&
+ test_must_fail git history drop HEAD 2>err &&
+ test_grep "Your local changes to the following files would be overwritten" err
+ )
+'
+
+test_expect_success 'can drop tip of a branch' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ git symbolic-ref HEAD >expect &&
+ git history drop HEAD &&
+ git symbolic-ref HEAD >actual &&
+ test_cmp expect actual &&
+
+ cat >expect <<-EOF &&
+ second
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'can drop commit in the middle' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ test_commit fourth &&
+ test_commit fifth &&
+
+ git symbolic-ref HEAD >expect &&
+ git history drop HEAD~2 &&
+ git symbolic-ref HEAD >actual &&
+ test_cmp expect actual &&
+
+ cat >expect <<-EOF &&
+ fifth
+ fourth
+ second
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'correct order is retained' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ test_commit fourth &&
+ test_commit fifth &&
+ git history drop HEAD~3 &&
+ cat >expect <<-EOF &&
+ fifth
+ fourth
+ third
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'hooks are executed for rewritten commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ write_script .git/hooks/prepare-commit-msg <<-EOF &&
+ echo "prepare-commit-msg: \$@" >>"$(pwd)/hooks.log"
+ EOF
+ write_script .git/hooks/post-commit <<-EOF &&
+ echo "post-commit" >>"$(pwd)/hooks.log"
+ EOF
+ write_script .git/hooks/post-rewrite <<-EOF &&
+ {
+ echo "post-rewrite: \$@"
+ cat
+ } >>"$(pwd)/hooks.log"
+ EOF
+
+ git history drop HEAD~ &&
+ cat >expect <<-EOF &&
+ prepare-commit-msg: .git/COMMIT_EDITMSG message
+ post-commit
+ post-rewrite: history
+ $(git rev-parse third) $(git rev-parse HEAD)
+ EOF
+ test_cmp expect hooks.log
+ )
+'
+
+test_expect_success 'can drop root commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ git history drop HEAD~2 &&
+ cat >expect <<-EOF &&
+ third
+ second
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'conflicts are detected' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ echo original >file &&
+ git add . &&
+ git commit -m original &&
+ echo modified >file &&
+ git commit -am modified &&
+
+ test_must_fail git history drop HEAD~ >err 2>&1 &&
+ test_grep CONFLICT err &&
+ test_grep "git history continue" err &&
+ echo resolved >file &&
+ git add file &&
+ git history continue &&
+
+ cat >expect <<-EOF &&
+ modified
+ base
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual &&
+ echo resolved >expect &&
+ git cat-file -p HEAD:file >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_done
--
2.51.0.417.g1ba7204a04.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH RFC v3 10/18] builtin/history: implement "reorder" subcommand
2025-09-04 14:27 ` [PATCH RFC v3 00/18] " Patrick Steinhardt
` (8 preceding siblings ...)
2025-09-04 14:27 ` [PATCH RFC v3 09/18] builtin/history: implement "drop" subcommand Patrick Steinhardt
@ 2025-09-04 14:27 ` Patrick Steinhardt
2025-09-04 14:27 ` [PATCH RFC v3 11/18] add-patch: split out header from "add-interactive.h" Patrick Steinhardt
` (10 subsequent siblings)
20 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-04 14:27 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
When working in projects where having nice commits matters it's quite
common that developers end up reordering commits a lot. This is
typically done via interactive rebases, where they can then rearrange
commits in the instruction sheet.
Still, this operation is a frequent-enough operation to provide a more
direct way of doing this imperatively. As such, introduce a new
"reorder" subcommand where users can reorder a commit A to come after or
before another commit B:
$ git log --oneline
a978f73 fifth
57594ee fourth
04eb1c4 third
d535e30 second
bf7438d first
$ git history reorder :/fourth --before=:/second
$ git log --oneline
1610fe0 fifth
444f97d third
2f90797 second
b0ae659 fourth
bf7438d first
$ git history reorder :/fourth --after=:/second
$ git log --oneline
c48729d fifth
f44a46e third
26693b8 fourth
8cb4171 second
bf7438d first
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-history.adoc | 33 +++++
builtin/history.c | 130 +++++++++++++++++++
t/meson.build | 1 +
t/t3452-history-reorder.sh | 278 +++++++++++++++++++++++++++++++++++++++++
4 files changed, 442 insertions(+)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index 39c9f1e25e..b36cd925dd 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -12,6 +12,7 @@ git history abort
git history continue
git history quit
git history drop <commit>
+git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)
DESCRIPTION
-----------
@@ -45,6 +46,12 @@ Dropping the root commit converts the child of that commit into the new
root commit. It is invalid to drop a root commit that does not have any
child commits, as that would lead to an empty branch.
+`reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)`::
+ Move the commit so that it becomes either the parent of
+ <following-commit> or the child of <preceding-commit>. The commits must
+ be related to one another and must be reachable from the current `HEAD`
+ commit.
+
The following commands are used to manage an interrupted history-rewriting
operation:
@@ -78,6 +85,32 @@ b1bc1bd third
e098c27 first
----------
+Reorder a commit
+~~~~~~~~~~~~~~~~
+
+----------
+$ git log --oneline
+a978f73 fifth
+57594ee fourth
+04eb1c4 third
+d535e30 second
+bf7438d first
+$ git history reorder :/fourth --before=:/second
+$ git log --oneline
+1610fe0 fifth
+444f97d third
+2f90797 second
+b0ae659 fourth
+bf7438d first
+$ git history reorder :/fourth --after=:/second
+$ git log --oneline
+c48729d fifth
+f44a46e third
+26693b8 fourth
+8cb4171 second
+bf7438d first
+----------
+
CONFIGURATION
-------------
diff --git a/builtin/history.c b/builtin/history.c
index 2132b6a441..16b516856e 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -182,6 +182,33 @@ static int collect_commits(struct repository *repo,
return ret;
}
+static void replace_commits(struct strvec *commits,
+ const struct object_id *commit_to_replace,
+ const struct object_id *replacements,
+ size_t replacements_nr)
+{
+ char commit_to_replace_oid[GIT_MAX_HEXSZ + 1];
+ struct strvec replacement_oids = STRVEC_INIT;
+ bool found = false;
+ size_t i;
+
+ oid_to_hex_r(commit_to_replace_oid, commit_to_replace);
+ for (i = 0; i < replacements_nr; i++)
+ strvec_push(&replacement_oids, oid_to_hex(&replacements[i]));
+
+ for (i = 0; i < commits->nr; i++) {
+ if (strcmp(commits->v[i], commit_to_replace_oid))
+ continue;
+ strvec_splice(commits, i, 1, replacement_oids.v, replacement_oids.nr);
+ found = true;
+ break;
+ }
+ if (!found)
+ BUG("could not find commit to replace");
+
+ strvec_clear(&replacement_oids);
+}
+
static int apply_commits(struct repository *repo,
const struct strvec *commits,
struct commit *head,
@@ -389,6 +416,107 @@ static int cmd_history_drop(int argc,
return ret;
}
+static int cmd_history_reorder(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo)
+{
+ const char * const usage[] = {
+ N_("git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)"),
+ NULL,
+ };
+ const char *before = NULL, *after = NULL;
+ struct option options[] = {
+ OPT_STRING(0, "before", &before, N_("commit"), N_("reorder before this commit")),
+ OPT_STRING(0, "after", &after, N_("commit"), N_("reorder after this commit")),
+ OPT_END(),
+ };
+ struct commit *commit_to_reorder, *head, *anchor, *old;
+ struct strvec commits = STRVEC_INIT;
+ struct object_id replacement[2];
+ struct commit_list *list = NULL;
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc != 1)
+ die(_("command expects a single revision"));
+ if (!before && !after)
+ die(_("exactly one option of 'before' or 'after' must be given"));
+ die_for_incompatible_opt2(!!before, "before", !!after, "after");
+
+ repo_config(repo, git_default_config, NULL);
+
+ commit_to_reorder = lookup_commit_reference_by_name(argv[0]);
+ if (!commit_to_reorder)
+ die(_("commit to be reordered cannot be found: %s"), argv[0]);
+ if (commit_to_reorder->parents && commit_to_reorder->parents->next)
+ die(_("commit to be reordered must not be a merge commit"));
+
+ anchor = lookup_commit_reference_by_name(before ? before : after);
+ if (!commit_to_reorder)
+ die(_("anchor commit cannot be found: %s"), before ? before : after);
+
+ if (oideq(&commit_to_reorder->object.oid, &anchor->object.oid))
+ die(_("commit to reorder and anchor must not be the same"));
+
+ head = lookup_commit_reference_by_name("HEAD");
+ if (!head)
+ die(_("could not resolve HEAD to a commit"));
+
+ commit_list_append(commit_to_reorder, &list);
+ if (!repo_is_descendant_of(repo, commit_to_reorder, list))
+ die(_("reordered commit must be reachable from current HEAD commit"));
+
+ /*
+ * There is no requirement for the user to have either one of the
+ * provided commits be the parent or child. We thus have to figure out
+ * ourselves which one is which.
+ */
+ if (repo_is_descendant_of(repo, anchor, list))
+ old = commit_to_reorder;
+ else
+ old = anchor;
+
+ /*
+ * Select the whole range of commits, including the boundary commit
+ * itself. In case the old commit is the root commit we simply pass no
+ * boundary.
+ */
+ ret = collect_commits(repo, old->parents ? old->parents->item : NULL,
+ head, &commits);
+ if (ret < 0)
+ goto out;
+
+ /*
+ * Perform the reordering of commits in the strvec. This is done by:
+ *
+ * - Deleting the to-be-reordered commit from the range of commits.
+ *
+ * - Replacing the anchor commit with the anchor commit plus the
+ * to-be-reordered commit.
+ */
+ if (before) {
+ replacement[0] = commit_to_reorder->object.oid;
+ replacement[1] = anchor->object.oid;
+ } else {
+ replacement[0] = anchor->object.oid;
+ replacement[1] = commit_to_reorder->object.oid;
+ }
+ replace_commits(&commits, &commit_to_reorder->object.oid, NULL, 0);
+ replace_commits(&commits, &anchor->object.oid, replacement, ARRAY_SIZE(replacement));
+
+ ret = apply_commits(repo, &commits, head, old, "reorder");
+ if (ret < 0)
+ goto out;
+
+ ret = 0;
+
+out:
+ free_commit_list(list);
+ strvec_clear(&commits);
+ return ret;
+}
+
int cmd_history(int argc,
const char **argv,
const char *prefix,
@@ -399,6 +527,7 @@ int cmd_history(int argc,
N_("git history continue"),
N_("git history quit"),
N_("git history drop <commit>"),
+ N_("git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)"),
NULL,
};
parse_opt_subcommand_fn *fn = NULL;
@@ -407,6 +536,7 @@ int cmd_history(int argc,
OPT_SUBCOMMAND("continue", &fn, cmd_history_continue),
OPT_SUBCOMMAND("quit", &fn, cmd_history_quit),
OPT_SUBCOMMAND("drop", &fn, cmd_history_drop),
+ OPT_SUBCOMMAND("reorder", &fn, cmd_history_reorder),
OPT_END(),
};
diff --git a/t/meson.build b/t/meson.build
index 8189c6c561..2bf7bcab5a 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -378,6 +378,7 @@ integration_tests = [
't3438-rebase-broken-files.sh',
't3450-history.sh',
't3451-history-drop.sh',
+ 't3452-history-reorder.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
diff --git a/t/t3452-history-reorder.sh b/t/t3452-history-reorder.sh
new file mode 100755
index 0000000000..2e9d64a9fd
--- /dev/null
+++ b/t/t3452-history-reorder.sh
@@ -0,0 +1,278 @@
+#!/bin/sh
+
+test_description='tests for git-history reorder subcommand'
+
+. ./test-lib.sh
+
+test_expect_success 'refuses to work with merge commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ git branch branch &&
+ test_commit ours &&
+ git switch branch &&
+ test_commit theirs &&
+ git switch - &&
+ git merge theirs &&
+ test_must_fail git history reorder HEAD --before=HEAD~ 2>err &&
+ test_grep "commit to be reordered must not be a merge commit" err &&
+ test_must_fail git history reorder HEAD~ --after=HEAD 2>err &&
+ test_grep "cannot rearrange commit history with merges" err
+ )
+'
+
+test_expect_success 'refuses to work with changes in the worktree or index' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit initial &&
+ test_commit file file &&
+ echo foo >file &&
+ test_must_fail git history reorder HEAD --before=HEAD~ 2>err &&
+ test_grep "Your local changes to the following files would be overwritten" err &&
+ git add file &&
+ test_must_fail git history reorder HEAD --before=HEAD~ 2>err &&
+ test_grep "Your local changes to the following files would be overwritten" err
+ )
+'
+
+test_expect_success 'requires exactly one of --before or --after' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_must_fail git history reorder HEAD 2>err &&
+ test_grep "exactly one option of ${SQ}before${SQ} or ${SQ}after${SQ} must be given" err &&
+ test_must_fail git history reorder HEAD --before=a --after=b 2>err &&
+ test_grep "options ${SQ}before${SQ} and ${SQ}after${SQ} cannot be used together" err
+ )
+'
+
+test_expect_success 'refuses to reorder commit with itself' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_must_fail git history reorder HEAD --after=HEAD 2>err &&
+ test_grep "commit to reorder and anchor must not be the same" err
+ )
+'
+
+test_expect_success '--before can move commit back in history' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ test_commit fourth &&
+ test_commit fifth &&
+ git history reorder :/fourth --before=:/second &&
+ cat >expect <<-EOF &&
+ fifth
+ third
+ second
+ fourth
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success '--before can move commit forward in history' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ test_commit fourth &&
+ test_commit fifth &&
+ git history reorder :/second --before=:/fourth &&
+ cat >expect <<-EOF &&
+ fifth
+ fourth
+ second
+ third
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success '--before can make a commit a root commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ git history reorder :/third --before=:/first &&
+ cat >expect <<-EOF &&
+ second
+ first
+ third
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success '--after can move commit back in history' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ test_commit fourth &&
+ test_commit fifth &&
+ git history reorder :/fourth --after=:/second &&
+ cat >expect <<-EOF &&
+ fifth
+ third
+ fourth
+ second
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success '--after can move commit forward in history' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ test_commit fourth &&
+ test_commit fifth &&
+ git history reorder :/second --after=:/fourth &&
+ cat >expect <<-EOF &&
+ fifth
+ second
+ fourth
+ third
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success '--after can make commit the tip' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ git history reorder :/first --after=:/third &&
+ cat >expect <<-EOF &&
+ first
+ third
+ second
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'hooks are executed for rewritten commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ write_script .git/hooks/prepare-commit-msg <<-EOF &&
+ echo "prepare-commit-msg: \$@" >>"$(pwd)/hooks.log"
+ EOF
+ write_script .git/hooks/post-commit <<-EOF &&
+ echo "post-commit" >>"$(pwd)/hooks.log"
+ EOF
+ write_script .git/hooks/post-rewrite <<-EOF &&
+ {
+ echo "post-rewrite: \$@"
+ cat
+ } >>"$(pwd)/hooks.log"
+ EOF
+
+ git history reorder :/third --before=:/second &&
+ cat >expect <<-EOF &&
+ second
+ third
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual &&
+
+ cat >expect <<-EOF &&
+ prepare-commit-msg: .git/COMMIT_EDITMSG message
+ post-commit
+ prepare-commit-msg: .git/COMMIT_EDITMSG message
+ post-commit
+ post-rewrite: history
+ $(git rev-parse third) $(git rev-parse HEAD~)
+ $(git rev-parse second) $(git rev-parse HEAD)
+ EOF
+ test_cmp expect hooks.log
+ )
+'
+
+test_expect_success 'conflicts are detected' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ echo base >file &&
+ git add file &&
+ git commit -m base &&
+ echo "first edit" >file &&
+ git commit -am "first edit" &&
+ echo "second edit" >file &&
+ git commit -am "second edit" &&
+
+ git symbolic-ref HEAD >expect-head &&
+ test_must_fail git history reorder HEAD --before=HEAD~ &&
+ test_must_fail git symbolic-ref HEAD &&
+ echo "second edit" >file &&
+ git add file &&
+ test_must_fail git history continue &&
+ echo "first edit" >file &&
+ git add file &&
+ git history continue &&
+
+ cat >expect <<-EOF &&
+ first edit
+ second edit
+ base
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual &&
+
+ git symbolic-ref HEAD >actual-head &&
+ test_cmp expect-head actual-head
+ )
+'
+
+test_done
--
2.51.0.417.g1ba7204a04.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH RFC v3 11/18] add-patch: split out header from "add-interactive.h"
2025-09-04 14:27 ` [PATCH RFC v3 00/18] " Patrick Steinhardt
` (9 preceding siblings ...)
2025-09-04 14:27 ` [PATCH RFC v3 10/18] builtin/history: implement "reorder" subcommand Patrick Steinhardt
@ 2025-09-04 14:27 ` Patrick Steinhardt
2025-09-04 14:27 ` [PATCH RFC v3 12/18] add-patch: split out `struct interactive_options` Patrick Steinhardt
` (9 subsequent siblings)
20 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-04 14:27 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
While we have a "add-patch.c" code file, its declarations are part of
"add-interactive.h". This makes it somewhat harder than necessary to
find relevant code and to identify clear boundaries between the two
subsystems.
Split up concerns and move declarations that relate to "add-patch.c"
into a new "add-patch.h" header.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
add-interactive.h | 23 +++--------------------
add-patch.c | 1 +
add-patch.h | 26 ++++++++++++++++++++++++++
3 files changed, 30 insertions(+), 20 deletions(-)
diff --git a/add-interactive.h b/add-interactive.h
index 4213dcd67b..fb95b6ee05 100644
--- a/add-interactive.h
+++ b/add-interactive.h
@@ -1,14 +1,11 @@
#ifndef ADD_INTERACTIVE_H
#define ADD_INTERACTIVE_H
+#include "add-patch.h"
#include "color.h"
-struct add_p_opt {
- int context;
- int interhunkcontext;
-};
-
-#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }
+struct pathspec;
+struct repository;
struct add_i_state {
struct repository *r;
@@ -32,21 +29,7 @@ void init_add_i_state(struct add_i_state *s, struct repository *r,
struct add_p_opt *add_p_opt);
void clear_add_i_state(struct add_i_state *s);
-struct repository;
-struct pathspec;
int run_add_i(struct repository *r, const struct pathspec *ps,
struct add_p_opt *add_p_opt);
-enum add_p_mode {
- ADD_P_ADD,
- ADD_P_STASH,
- ADD_P_RESET,
- ADD_P_CHECKOUT,
- ADD_P_WORKTREE,
-};
-
-int run_add_p(struct repository *r, enum add_p_mode mode,
- struct add_p_opt *o, const char *revision,
- const struct pathspec *ps);
-
#endif
diff --git a/add-patch.c b/add-patch.c
index 302e6ba7d9..e2b002fa73 100644
--- a/add-patch.c
+++ b/add-patch.c
@@ -3,6 +3,7 @@
#include "git-compat-util.h"
#include "add-interactive.h"
+#include "add-patch.h"
#include "advice.h"
#include "editor.h"
#include "environment.h"
diff --git a/add-patch.h b/add-patch.h
new file mode 100644
index 0000000000..4394c74107
--- /dev/null
+++ b/add-patch.h
@@ -0,0 +1,26 @@
+#ifndef ADD_PATCH_H
+#define ADD_PATCH_H
+
+struct pathspec;
+struct repository;
+
+struct add_p_opt {
+ int context;
+ int interhunkcontext;
+};
+
+#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }
+
+enum add_p_mode {
+ ADD_P_ADD,
+ ADD_P_STASH,
+ ADD_P_RESET,
+ ADD_P_CHECKOUT,
+ ADD_P_WORKTREE,
+};
+
+int run_add_p(struct repository *r, enum add_p_mode mode,
+ struct add_p_opt *o, const char *revision,
+ const struct pathspec *ps);
+
+#endif
--
2.51.0.417.g1ba7204a04.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH RFC v3 12/18] add-patch: split out `struct interactive_options`
2025-09-04 14:27 ` [PATCH RFC v3 00/18] " Patrick Steinhardt
` (10 preceding siblings ...)
2025-09-04 14:27 ` [PATCH RFC v3 11/18] add-patch: split out header from "add-interactive.h" Patrick Steinhardt
@ 2025-09-04 14:27 ` Patrick Steinhardt
2025-09-04 14:27 ` [PATCH RFC v3 13/18] add-patch: remove dependency on "add-interactive" subsystem Patrick Steinhardt
` (8 subsequent siblings)
20 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-04 14:27 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
The `struct add_p_opt` is reused both by our the infra for "git add -p"
and "git add -i". Users of `run_add_i()` for example are expected to
pass `struct add_p_opt`. This is somewhat confusing and raises the
question which options apply to what part of the stack.
But things are even more confusing than that: while callers are expected
to pass in `struct add_p_opt`, these options ultimately get used to
initialize a `struct add_i_state` that is used by both subsystems. So we
are basically going full circle here.
Refactor the code and split out a new `struct interactive_options` that
hosts common options used by both. These options are then applied to a
`struct interactive_config` that hosts common configuration.
This refactoring doesn't yet fully detangle the two subsystems from one
another, as we still end up calling `init_add_i_state()` in the "git add
-p" subsystem. This will be fixed in a subsequent commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
add-interactive.c | 151 +++++++++++++----------------------------------------
add-interactive.h | 20 ++-----
add-patch.c | 145 +++++++++++++++++++++++++++++++++++++++++---------
add-patch.h | 33 ++++++++++--
builtin/add.c | 22 ++++----
builtin/checkout.c | 4 +-
builtin/commit.c | 16 +++---
builtin/reset.c | 16 +++---
builtin/stash.c | 46 ++++++++--------
commit.h | 2 +-
10 files changed, 243 insertions(+), 212 deletions(-)
diff --git a/add-interactive.c b/add-interactive.c
index 3e692b47ec..3babc3e013 100644
--- a/add-interactive.c
+++ b/add-interactive.c
@@ -3,7 +3,6 @@
#include "git-compat-util.h"
#include "add-interactive.h"
#include "color.h"
-#include "config.h"
#include "diffcore.h"
#include "gettext.h"
#include "hash.h"
@@ -20,96 +19,18 @@
#include "prompt.h"
#include "tree.h"
-static void init_color(struct repository *r, struct add_i_state *s,
- const char *section_and_slot, char *dst,
- const char *default_color)
-{
- char *key = xstrfmt("color.%s", section_and_slot);
- const char *value;
-
- if (!s->use_color)
- dst[0] = '\0';
- else if (repo_config_get_value(r, key, &value) ||
- color_parse(value, dst))
- strlcpy(dst, default_color, COLOR_MAXLEN);
-
- free(key);
-}
-
void init_add_i_state(struct add_i_state *s, struct repository *r,
- struct add_p_opt *add_p_opt)
+ struct interactive_options *opts)
{
- const char *value;
-
s->r = r;
- s->context = -1;
- s->interhunkcontext = -1;
-
- if (repo_config_get_value(r, "color.interactive", &value))
- s->use_color = -1;
- else
- s->use_color =
- git_config_colorbool("color.interactive", value);
- s->use_color = want_color(s->use_color);
-
- init_color(r, s, "interactive.header", s->header_color, GIT_COLOR_BOLD);
- init_color(r, s, "interactive.help", s->help_color, GIT_COLOR_BOLD_RED);
- init_color(r, s, "interactive.prompt", s->prompt_color,
- GIT_COLOR_BOLD_BLUE);
- init_color(r, s, "interactive.error", s->error_color,
- GIT_COLOR_BOLD_RED);
-
- init_color(r, s, "diff.frag", s->fraginfo_color,
- diff_get_color(s->use_color, DIFF_FRAGINFO));
- init_color(r, s, "diff.context", s->context_color, "fall back");
- if (!strcmp(s->context_color, "fall back"))
- init_color(r, s, "diff.plain", s->context_color,
- diff_get_color(s->use_color, DIFF_CONTEXT));
- init_color(r, s, "diff.old", s->file_old_color,
- diff_get_color(s->use_color, DIFF_FILE_OLD));
- init_color(r, s, "diff.new", s->file_new_color,
- diff_get_color(s->use_color, DIFF_FILE_NEW));
-
- strlcpy(s->reset_color,
- s->use_color ? GIT_COLOR_RESET : "", COLOR_MAXLEN);
-
- FREE_AND_NULL(s->interactive_diff_filter);
- repo_config_get_string(r, "interactive.difffilter",
- &s->interactive_diff_filter);
-
- FREE_AND_NULL(s->interactive_diff_algorithm);
- repo_config_get_string(r, "diff.algorithm",
- &s->interactive_diff_algorithm);
-
- if (!repo_config_get_int(r, "diff.context", &s->context))
- if (s->context < 0)
- die(_("%s cannot be negative"), "diff.context");
- if (!repo_config_get_int(r, "diff.interHunkContext", &s->interhunkcontext))
- if (s->interhunkcontext < 0)
- die(_("%s cannot be negative"), "diff.interHunkContext");
-
- repo_config_get_bool(r, "interactive.singlekey", &s->use_single_key);
- if (s->use_single_key)
- setbuf(stdin, NULL);
-
- if (add_p_opt->context != -1) {
- if (add_p_opt->context < 0)
- die(_("%s cannot be negative"), "--unified");
- s->context = add_p_opt->context;
- }
- if (add_p_opt->interhunkcontext != -1) {
- if (add_p_opt->interhunkcontext < 0)
- die(_("%s cannot be negative"), "--inter-hunk-context");
- s->interhunkcontext = add_p_opt->interhunkcontext;
- }
+ interactive_config_init(&s->cfg, r, opts);
}
void clear_add_i_state(struct add_i_state *s)
{
- FREE_AND_NULL(s->interactive_diff_filter);
- FREE_AND_NULL(s->interactive_diff_algorithm);
+ interactive_config_clear(&s->cfg);
memset(s, 0, sizeof(*s));
- s->use_color = -1;
+ interactive_config_clear(&s->cfg);
}
/*
@@ -262,7 +183,7 @@ static void list(struct add_i_state *s, struct string_list *list, int *selected,
return;
if (opts->header)
- color_fprintf_ln(stdout, s->header_color,
+ color_fprintf_ln(stdout, s->cfg.header_color,
"%s", opts->header);
for (i = 0; i < list->nr; i++) {
@@ -330,7 +251,7 @@ static ssize_t list_and_choose(struct add_i_state *s,
list(s, &items->items, items->selected, &opts->list_opts);
- color_fprintf(stdout, s->prompt_color, "%s", opts->prompt);
+ color_fprintf(stdout, s->cfg.prompt_color, "%s", opts->prompt);
fputs(singleton ? "> " : ">> ", stdout);
fflush(stdout);
@@ -408,7 +329,7 @@ static ssize_t list_and_choose(struct add_i_state *s,
if (from < 0 || from >= items->items.nr ||
(singleton && from + 1 != to)) {
- color_fprintf_ln(stderr, s->error_color,
+ color_fprintf_ln(stderr, s->cfg.error_color,
_("Huh (%s)?"), p);
break;
} else if (singleton) {
@@ -968,7 +889,7 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps,
free(files->items.items[i].string);
} else if (item->index.unmerged ||
item->worktree.unmerged) {
- color_fprintf_ln(stderr, s->error_color,
+ color_fprintf_ln(stderr, s->cfg.error_color,
_("ignoring unmerged: %s"),
files->items.items[i].string);
free(item);
@@ -990,9 +911,9 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps,
opts->prompt = N_("Patch update");
count = list_and_choose(s, files, opts);
if (count > 0) {
- struct add_p_opt add_p_opt = {
- .context = s->context,
- .interhunkcontext = s->interhunkcontext,
+ struct interactive_options opts = {
+ .context = s->cfg.context,
+ .interhunkcontext = s->cfg.interhunkcontext,
};
struct strvec args = STRVEC_INIT;
struct pathspec ps_selected = { 0 };
@@ -1004,7 +925,7 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps,
parse_pathspec(&ps_selected,
PATHSPEC_ALL_MAGIC & ~PATHSPEC_LITERAL,
PATHSPEC_LITERAL_PATH, "", args.v);
- res = run_add_p(s->r, ADD_P_ADD, &add_p_opt, NULL, &ps_selected);
+ res = run_add_p(s->r, ADD_P_ADD, &opts, NULL, &ps_selected);
strvec_clear(&args);
clear_pathspec(&ps_selected);
}
@@ -1040,10 +961,10 @@ static int run_diff(struct add_i_state *s, const struct pathspec *ps,
struct child_process cmd = CHILD_PROCESS_INIT;
strvec_pushl(&cmd.args, "git", "diff", "-p", "--cached", NULL);
- if (s->context != -1)
- strvec_pushf(&cmd.args, "--unified=%i", s->context);
- if (s->interhunkcontext != -1)
- strvec_pushf(&cmd.args, "--inter-hunk-context=%i", s->interhunkcontext);
+ if (s->cfg.context != -1)
+ strvec_pushf(&cmd.args, "--unified=%i", s->cfg.context);
+ if (s->cfg.interhunkcontext != -1)
+ strvec_pushf(&cmd.args, "--inter-hunk-context=%i", s->cfg.interhunkcontext);
strvec_pushl(&cmd.args, oid_to_hex(!is_initial ? &oid :
s->r->hash_algo->empty_tree), "--", NULL);
for (i = 0; i < files->items.nr; i++)
@@ -1061,17 +982,17 @@ static int run_help(struct add_i_state *s, const struct pathspec *ps UNUSED,
struct prefix_item_list *files UNUSED,
struct list_and_choose_options *opts UNUSED)
{
- color_fprintf_ln(stdout, s->help_color, "status - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "status - %s",
_("show paths with changes"));
- color_fprintf_ln(stdout, s->help_color, "update - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "update - %s",
_("add working tree state to the staged set of changes"));
- color_fprintf_ln(stdout, s->help_color, "revert - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "revert - %s",
_("revert staged set of changes back to the HEAD version"));
- color_fprintf_ln(stdout, s->help_color, "patch - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "patch - %s",
_("pick hunks and update selectively"));
- color_fprintf_ln(stdout, s->help_color, "diff - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "diff - %s",
_("view diff between HEAD and index"));
- color_fprintf_ln(stdout, s->help_color, "add untracked - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "add untracked - %s",
_("add contents of untracked files to the staged set of changes"));
return 0;
@@ -1079,21 +1000,21 @@ static int run_help(struct add_i_state *s, const struct pathspec *ps UNUSED,
static void choose_prompt_help(struct add_i_state *s)
{
- color_fprintf_ln(stdout, s->help_color, "%s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "%s",
_("Prompt help:"));
- color_fprintf_ln(stdout, s->help_color, "1 - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "1 - %s",
_("select a single item"));
- color_fprintf_ln(stdout, s->help_color, "3-5 - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "3-5 - %s",
_("select a range of items"));
- color_fprintf_ln(stdout, s->help_color, "2-3,6-9 - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "2-3,6-9 - %s",
_("select multiple ranges"));
- color_fprintf_ln(stdout, s->help_color, "foo - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "foo - %s",
_("select item based on unique prefix"));
- color_fprintf_ln(stdout, s->help_color, "-... - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "-... - %s",
_("unselect specified items"));
- color_fprintf_ln(stdout, s->help_color, "* - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "* - %s",
_("choose all items"));
- color_fprintf_ln(stdout, s->help_color, " - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, " - %s",
_("(empty) finish selecting"));
}
@@ -1128,7 +1049,7 @@ static void print_command_item(int i, int selected UNUSED,
static void command_prompt_help(struct add_i_state *s)
{
- const char *help_color = s->help_color;
+ const char *help_color = s->cfg.help_color;
color_fprintf_ln(stdout, help_color, "%s", _("Prompt help:"));
color_fprintf_ln(stdout, help_color, "1 - %s",
_("select a numbered item"));
@@ -1139,7 +1060,7 @@ static void command_prompt_help(struct add_i_state *s)
}
int run_add_i(struct repository *r, const struct pathspec *ps,
- struct add_p_opt *add_p_opt)
+ struct interactive_options *interactive_opts)
{
struct add_i_state s = { NULL };
struct print_command_item_data data = { "[", "]" };
@@ -1182,15 +1103,15 @@ int run_add_i(struct repository *r, const struct pathspec *ps,
->util = util;
}
- init_add_i_state(&s, r, add_p_opt);
+ init_add_i_state(&s, r, interactive_opts);
/*
* When color was asked for, use the prompt color for
* highlighting, otherwise use square brackets.
*/
- if (s.use_color) {
- data.color = s.prompt_color;
- data.reset = s.reset_color;
+ if (s.cfg.use_color) {
+ data.color = s.cfg.prompt_color;
+ data.reset = s.cfg.reset_color;
}
print_file_item_data.color = data.color;
print_file_item_data.reset = data.reset;
diff --git a/add-interactive.h b/add-interactive.h
index fb95b6ee05..eefa2edc7c 100644
--- a/add-interactive.h
+++ b/add-interactive.h
@@ -2,34 +2,20 @@
#define ADD_INTERACTIVE_H
#include "add-patch.h"
-#include "color.h"
struct pathspec;
struct repository;
struct add_i_state {
struct repository *r;
- int use_color;
- char header_color[COLOR_MAXLEN];
- char help_color[COLOR_MAXLEN];
- char prompt_color[COLOR_MAXLEN];
- char error_color[COLOR_MAXLEN];
- char reset_color[COLOR_MAXLEN];
- char fraginfo_color[COLOR_MAXLEN];
- char context_color[COLOR_MAXLEN];
- char file_old_color[COLOR_MAXLEN];
- char file_new_color[COLOR_MAXLEN];
-
- int use_single_key;
- char *interactive_diff_filter, *interactive_diff_algorithm;
- int context, interhunkcontext;
+ struct interactive_config cfg;
};
void init_add_i_state(struct add_i_state *s, struct repository *r,
- struct add_p_opt *add_p_opt);
+ struct interactive_options *opts);
void clear_add_i_state(struct add_i_state *s);
int run_add_i(struct repository *r, const struct pathspec *ps,
- struct add_p_opt *add_p_opt);
+ struct interactive_options *opts);
#endif
diff --git a/add-patch.c b/add-patch.c
index e2b002fa73..45bc254e0c 100644
--- a/add-patch.c
+++ b/add-patch.c
@@ -5,6 +5,8 @@
#include "add-interactive.h"
#include "add-patch.h"
#include "advice.h"
+#include "config.h"
+#include "diff.h"
#include "editor.h"
#include "environment.h"
#include "gettext.h"
@@ -279,6 +281,99 @@ struct add_p_state {
const char *revision;
};
+static void init_color(struct repository *r,
+ struct interactive_config *cfg,
+ const char *section_and_slot, char *dst,
+ const char *default_color)
+{
+ char *key = xstrfmt("color.%s", section_and_slot);
+ const char *value;
+
+ if (!cfg->use_color)
+ dst[0] = '\0';
+ else if (repo_config_get_value(r, key, &value) ||
+ color_parse(value, dst))
+ strlcpy(dst, default_color, COLOR_MAXLEN);
+
+ free(key);
+}
+
+void interactive_config_init(struct interactive_config *cfg,
+ struct repository *r,
+ struct interactive_options *opts)
+{
+ const char *value;
+
+ cfg->context = -1;
+ cfg->interhunkcontext = -1;
+
+ if (repo_config_get_value(r, "color.interactive", &value))
+ cfg->use_color = -1;
+ else
+ cfg->use_color =
+ git_config_colorbool("color.interactive", value);
+ cfg->use_color = want_color(cfg->use_color);
+
+ init_color(r, cfg, "interactive.header", cfg->header_color, GIT_COLOR_BOLD);
+ init_color(r, cfg, "interactive.help", cfg->help_color, GIT_COLOR_BOLD_RED);
+ init_color(r, cfg, "interactive.prompt", cfg->prompt_color,
+ GIT_COLOR_BOLD_BLUE);
+ init_color(r, cfg, "interactive.error", cfg->error_color,
+ GIT_COLOR_BOLD_RED);
+
+ init_color(r, cfg, "diff.frag", cfg->fraginfo_color,
+ diff_get_color(cfg->use_color, DIFF_FRAGINFO));
+ init_color(r, cfg, "diff.context", cfg->context_color, "fall back");
+ if (!strcmp(cfg->context_color, "fall back"))
+ init_color(r, cfg, "diff.plain", cfg->context_color,
+ diff_get_color(cfg->use_color, DIFF_CONTEXT));
+ init_color(r, cfg, "diff.old", cfg->file_old_color,
+ diff_get_color(cfg->use_color, DIFF_FILE_OLD));
+ init_color(r, cfg, "diff.new", cfg->file_new_color,
+ diff_get_color(cfg->use_color, DIFF_FILE_NEW));
+
+ strlcpy(cfg->reset_color,
+ cfg->use_color ? GIT_COLOR_RESET : "", COLOR_MAXLEN);
+
+ FREE_AND_NULL(cfg->interactive_diff_filter);
+ repo_config_get_string(r, "interactive.difffilter",
+ &cfg->interactive_diff_filter);
+
+ FREE_AND_NULL(cfg->interactive_diff_algorithm);
+ repo_config_get_string(r, "diff.algorithm",
+ &cfg->interactive_diff_algorithm);
+
+ if (!repo_config_get_int(r, "diff.context", &cfg->context))
+ if (cfg->context < 0)
+ die(_("%s cannot be negative"), "diff.context");
+ if (!repo_config_get_int(r, "diff.interHunkContext", &cfg->interhunkcontext))
+ if (cfg->interhunkcontext < 0)
+ die(_("%s cannot be negative"), "diff.interHunkContext");
+
+ repo_config_get_bool(r, "interactive.singlekey", &cfg->use_single_key);
+ if (cfg->use_single_key)
+ setbuf(stdin, NULL);
+
+ if (opts->context != -1) {
+ if (opts->context < 0)
+ die(_("%s cannot be negative"), "--unified");
+ cfg->context = opts->context;
+ }
+ if (opts->interhunkcontext != -1) {
+ if (opts->interhunkcontext < 0)
+ die(_("%s cannot be negative"), "--inter-hunk-context");
+ cfg->interhunkcontext = opts->interhunkcontext;
+ }
+}
+
+void interactive_config_clear(struct interactive_config *cfg)
+{
+ FREE_AND_NULL(cfg->interactive_diff_filter);
+ FREE_AND_NULL(cfg->interactive_diff_algorithm);
+ memset(cfg, 0, sizeof(*cfg));
+ cfg->use_color = -1;
+}
+
static void add_p_state_clear(struct add_p_state *s)
{
size_t i;
@@ -299,9 +394,9 @@ static void err(struct add_p_state *s, const char *fmt, ...)
va_list args;
va_start(args, fmt);
- fputs(s->s.error_color, stdout);
+ fputs(s->s.cfg.error_color, stdout);
vprintf(fmt, args);
- puts(s->s.reset_color);
+ puts(s->s.cfg.reset_color);
va_end(args);
}
@@ -424,12 +519,12 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
int res;
strvec_pushv(&args, s->mode->diff_cmd);
- if (s->s.context != -1)
- strvec_pushf(&args, "--unified=%i", s->s.context);
- if (s->s.interhunkcontext != -1)
- strvec_pushf(&args, "--inter-hunk-context=%i", s->s.interhunkcontext);
- if (s->s.interactive_diff_algorithm)
- strvec_pushf(&args, "--diff-algorithm=%s", s->s.interactive_diff_algorithm);
+ if (s->s.cfg.context != -1)
+ strvec_pushf(&args, "--unified=%i", s->s.cfg.context);
+ if (s->s.cfg.interhunkcontext != -1)
+ strvec_pushf(&args, "--inter-hunk-context=%i", s->s.cfg.interhunkcontext);
+ if (s->s.cfg.interactive_diff_algorithm)
+ strvec_pushf(&args, "--diff-algorithm=%s", s->s.cfg.interactive_diff_algorithm);
if (s->revision) {
struct object_id oid;
strvec_push(&args,
@@ -460,7 +555,7 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
if (want_color_fd(1, -1)) {
struct child_process colored_cp = CHILD_PROCESS_INIT;
- const char *diff_filter = s->s.interactive_diff_filter;
+ const char *diff_filter = s->s.cfg.interactive_diff_filter;
setup_child_process(s, &colored_cp, NULL);
xsnprintf((char *)args.v[color_arg_index], 8, "--color");
@@ -693,7 +788,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
hunk->colored_end - hunk->colored_start);
return;
} else {
- strbuf_addstr(out, s->s.fraginfo_color);
+ strbuf_addstr(out, s->s.cfg.fraginfo_color);
p = s->colored.buf + header->colored_extra_start;
len = header->colored_extra_end
- header->colored_extra_start;
@@ -715,7 +810,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
if (len)
strbuf_add(out, p, len);
else if (colored)
- strbuf_addf(out, "%s\n", s->s.reset_color);
+ strbuf_addf(out, "%s\n", s->s.cfg.reset_color);
else
strbuf_addch(out, '\n');
}
@@ -1103,12 +1198,12 @@ static void recolor_hunk(struct add_p_state *s, struct hunk *hunk)
strbuf_addstr(&s->colored,
plain[current] == '-' ?
- s->s.file_old_color :
+ s->s.cfg.file_old_color :
plain[current] == '+' ?
- s->s.file_new_color :
- s->s.context_color);
+ s->s.cfg.file_new_color :
+ s->s.cfg.context_color);
strbuf_add(&s->colored, plain + current, eol - current);
- strbuf_addstr(&s->colored, s->s.reset_color);
+ strbuf_addstr(&s->colored, s->s.cfg.reset_color);
if (next > eol)
strbuf_add(&s->colored, plain + eol, next - eol);
current = next;
@@ -1227,7 +1322,7 @@ static int run_apply_check(struct add_p_state *s,
static int read_single_character(struct add_p_state *s)
{
- if (s->s.use_single_key) {
+ if (s->s.cfg.use_single_key) {
int res = read_key_without_echo(&s->answer);
printf("%s\n", res == EOF ? "" : s->answer.buf);
return res;
@@ -1241,7 +1336,7 @@ static int read_single_character(struct add_p_state *s)
static int prompt_yesno(struct add_p_state *s, const char *prompt)
{
for (;;) {
- color_fprintf(stdout, s->s.prompt_color, "%s", _(prompt));
+ color_fprintf(stdout, s->s.cfg.prompt_color, "%s", _(prompt));
fflush(stdout);
if (read_single_character(s) == EOF)
return -1;
@@ -1522,15 +1617,15 @@ static int patch_update_file(struct add_p_state *s,
else
prompt_mode_type = PROMPT_HUNK;
- printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->s.prompt_color,
+ printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->s.cfg.prompt_color,
(uintmax_t)hunk_index + 1,
(uintmax_t)(file_diff->hunk_nr
? file_diff->hunk_nr
: 1));
printf(_(s->mode->prompt_mode[prompt_mode_type]),
s->buf.buf);
- if (*s->s.reset_color)
- fputs(s->s.reset_color, stdout);
+ if (*s->s.cfg.reset_color)
+ fputs(s->s.cfg.reset_color, stdout);
fflush(stdout);
if (read_single_character(s) == EOF)
break;
@@ -1687,7 +1782,7 @@ static int patch_update_file(struct add_p_state *s,
err(s, _("Sorry, cannot split this hunk"));
} else if (!split_hunk(s, file_diff,
hunk - file_diff->hunk)) {
- color_fprintf_ln(stdout, s->s.header_color,
+ color_fprintf_ln(stdout, s->s.cfg.header_color,
_("Split into %d hunks."),
(int)splittable_into);
rendered_hunk_index = -1;
@@ -1705,7 +1800,7 @@ static int patch_update_file(struct add_p_state *s,
} else if (s->answer.buf[0] == '?') {
const char *p = _(help_patch_remainder), *eol = p;
- color_fprintf(stdout, s->s.help_color, "%s",
+ color_fprintf(stdout, s->s.cfg.help_color, "%s",
_(s->mode->help_patch_text));
/*
@@ -1723,7 +1818,7 @@ static int patch_update_file(struct add_p_state *s,
if (*p != '?' && !strchr(s->buf.buf, *p))
continue;
- color_fprintf_ln(stdout, s->s.help_color,
+ color_fprintf_ln(stdout, s->s.cfg.help_color,
"%.*s", (int)(eol - p), p);
}
} else {
@@ -1764,7 +1859,7 @@ static int patch_update_file(struct add_p_state *s,
}
int run_add_p(struct repository *r, enum add_p_mode mode,
- struct add_p_opt *o, const char *revision,
+ struct interactive_options *opts, const char *revision,
const struct pathspec *ps)
{
struct add_p_state s = {
@@ -1772,7 +1867,7 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
};
size_t i, binary_count = 0;
- init_add_i_state(&s.s, r, o);
+ init_add_i_state(&s.s, r, opts);
if (mode == ADD_P_STASH)
s.mode = &patch_mode_stash;
diff --git a/add-patch.h b/add-patch.h
index 4394c74107..51c0d7bce9 100644
--- a/add-patch.h
+++ b/add-patch.h
@@ -1,15 +1,42 @@
#ifndef ADD_PATCH_H
#define ADD_PATCH_H
+#include "color.h"
+
struct pathspec;
struct repository;
-struct add_p_opt {
+struct interactive_options {
int context;
int interhunkcontext;
};
-#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }
+#define INTERACTIVE_OPTIONS_INIT { \
+ .context = -1, \
+ .interhunkcontext = -1, \
+}
+
+struct interactive_config {
+ int use_color;
+ char header_color[COLOR_MAXLEN];
+ char help_color[COLOR_MAXLEN];
+ char prompt_color[COLOR_MAXLEN];
+ char error_color[COLOR_MAXLEN];
+ char reset_color[COLOR_MAXLEN];
+ char fraginfo_color[COLOR_MAXLEN];
+ char context_color[COLOR_MAXLEN];
+ char file_old_color[COLOR_MAXLEN];
+ char file_new_color[COLOR_MAXLEN];
+
+ int use_single_key;
+ char *interactive_diff_filter, *interactive_diff_algorithm;
+ int context, interhunkcontext;
+};
+
+void interactive_config_init(struct interactive_config *cfg,
+ struct repository *r,
+ struct interactive_options *opts);
+void interactive_config_clear(struct interactive_config *cfg);
enum add_p_mode {
ADD_P_ADD,
@@ -20,7 +47,7 @@ enum add_p_mode {
};
int run_add_p(struct repository *r, enum add_p_mode mode,
- struct add_p_opt *o, const char *revision,
+ struct interactive_options *opts, const char *revision,
const struct pathspec *ps);
#endif
diff --git a/builtin/add.c b/builtin/add.c
index 0235854f80..a94c826c14 100644
--- a/builtin/add.c
+++ b/builtin/add.c
@@ -30,7 +30,7 @@ static const char * const builtin_add_usage[] = {
NULL
};
static int patch_interactive, add_interactive, edit_interactive;
-static struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+static struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
static int take_worktree_changes;
static int add_renormalize;
static int pathspec_file_nul;
@@ -159,7 +159,7 @@ static int refresh(struct repository *repo, int verbose, const struct pathspec *
int interactive_add(struct repository *repo,
const char **argv,
const char *prefix,
- int patch, struct add_p_opt *add_p_opt)
+ int patch, struct interactive_options *interactive_opts)
{
struct pathspec pathspec;
int ret;
@@ -171,9 +171,9 @@ int interactive_add(struct repository *repo,
prefix, argv);
if (patch)
- ret = !!run_add_p(repo, ADD_P_ADD, add_p_opt, NULL, &pathspec);
+ ret = !!run_add_p(repo, ADD_P_ADD, interactive_opts, NULL, &pathspec);
else
- ret = !!run_add_i(repo, &pathspec, add_p_opt);
+ ret = !!run_add_i(repo, &pathspec, interactive_opts);
clear_pathspec(&pathspec);
return ret;
@@ -255,8 +255,8 @@ static struct option builtin_add_options[] = {
OPT_GROUP(""),
OPT_BOOL('i', "interactive", &add_interactive, N_("interactive picking")),
OPT_BOOL('p', "patch", &patch_interactive, N_("select hunks interactively")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT_BOOL('e', "edit", &edit_interactive, N_("edit current diff and apply")),
OPT__FORCE(&ignored_too, N_("allow adding otherwise ignored files"), 0),
OPT_BOOL('u', "update", &take_worktree_changes, N_("update tracked files")),
@@ -398,9 +398,9 @@ int cmd_add(int argc,
prepare_repo_settings(repo);
repo->settings.command_requires_full_index = 0;
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
if (patch_interactive)
@@ -410,11 +410,11 @@ int cmd_add(int argc,
die(_("options '%s' and '%s' cannot be used together"), "--dry-run", "--interactive/--patch");
if (pathspec_from_file)
die(_("options '%s' and '%s' cannot be used together"), "--pathspec-from-file", "--interactive/--patch");
- exit(interactive_add(repo, argv + 1, prefix, patch_interactive, &add_p_opt));
+ exit(interactive_add(repo, argv + 1, prefix, patch_interactive, &interactive_opts));
} else {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--interactive/--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--interactive/--patch");
}
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 43583c8d1b..0b90f398fe 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -546,7 +546,7 @@ static int checkout_paths(const struct checkout_opts *opts,
if (opts->patch_mode) {
enum add_p_mode patch_mode;
- struct add_p_opt add_p_opt = {
+ struct interactive_options interactive_opts = {
.context = opts->patch_context,
.interhunkcontext = opts->patch_interhunk_context,
};
@@ -575,7 +575,7 @@ static int checkout_paths(const struct checkout_opts *opts,
else
BUG("either flag must have been set, worktree=%d, index=%d",
opts->checkout_worktree, opts->checkout_index);
- return !!run_add_p(the_repository, patch_mode, &add_p_opt,
+ return !!run_add_p(the_repository, patch_mode, &interactive_opts,
rev, &opts->pathspec);
}
diff --git a/builtin/commit.c b/builtin/commit.c
index b5b9608813..767351fd87 100644
--- a/builtin/commit.c
+++ b/builtin/commit.c
@@ -123,7 +123,7 @@ static const char *edit_message, *use_message;
static char *fixup_message, *fixup_commit, *squash_message;
static const char *fixup_prefix;
static int all, also, interactive, patch_interactive, only, amend, signoff;
-static struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+static struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
static int edit_flag = -1; /* unspecified */
static int quiet, verbose, no_verify, allow_empty, dry_run, renew_authorship;
static int config_commit_verbose = -1; /* unspecified */
@@ -356,9 +356,9 @@ static const char *prepare_index(const char **argv, const char *prefix,
const char *ret;
char *path = NULL;
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
if (is_status)
@@ -407,7 +407,7 @@ static const char *prepare_index(const char **argv, const char *prefix,
old_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT));
setenv(INDEX_ENVIRONMENT, the_repository->index_file, 1);
- if (interactive_add(the_repository, argv, prefix, patch_interactive, &add_p_opt) != 0)
+ if (interactive_add(the_repository, argv, prefix, patch_interactive, &interactive_opts) != 0)
die(_("interactive add failed"));
the_repository->index_file = old_repo_index_file;
@@ -432,9 +432,9 @@ static const char *prepare_index(const char **argv, const char *prefix,
ret = get_lock_file_path(&index_lock);
goto out;
} else {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--interactive/--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--interactive/--patch");
}
@@ -1738,8 +1738,8 @@ int cmd_commit(int argc,
OPT_BOOL('i', "include", &also, N_("add specified files to index for commit")),
OPT_BOOL(0, "interactive", &interactive, N_("interactively add files")),
OPT_BOOL('p', "patch", &patch_interactive, N_("interactively add changes")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT_BOOL('o', "only", &only, N_("commit only specified files")),
OPT_BOOL('n', "no-verify", &no_verify, N_("bypass pre-commit and commit-msg hooks")),
OPT_BOOL(0, "dry-run", &dry_run, N_("show what would be committed")),
diff --git a/builtin/reset.c b/builtin/reset.c
index ed35802af1..088449e120 100644
--- a/builtin/reset.c
+++ b/builtin/reset.c
@@ -346,7 +346,7 @@ int cmd_reset(int argc,
struct object_id oid;
struct pathspec pathspec;
int intent_to_add = 0;
- struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
const struct option options[] = {
OPT__QUIET(&quiet, N_("be quiet, only report errors")),
OPT_BOOL(0, "no-refresh", &no_refresh,
@@ -371,8 +371,8 @@ int cmd_reset(int argc,
PARSE_OPT_OPTARG,
option_parse_recurse_submodules_worktree_updater),
OPT_BOOL('p', "patch", &patch_mode, N_("select hunks interactively")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT_BOOL('N', "intent-to-add", &intent_to_add,
N_("record only the fact that removed paths will be added later")),
OPT_PATHSPEC_FROM_FILE(&pathspec_from_file),
@@ -423,9 +423,9 @@ int cmd_reset(int argc,
oidcpy(&oid, &tree->object.oid);
}
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
prepare_repo_settings(the_repository);
@@ -436,12 +436,12 @@ int cmd_reset(int argc,
die(_("options '%s' and '%s' cannot be used together"), "--patch", "--{hard,mixed,soft}");
trace2_cmd_mode("patch-interactive");
update_ref_status = !!run_add_p(the_repository, ADD_P_RESET,
- &add_p_opt, rev, &pathspec);
+ &interactive_opts, rev, &pathspec);
goto cleanup;
} else {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--patch");
}
diff --git a/builtin/stash.c b/builtin/stash.c
index 1977e50df2..5070b8a88f 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -1302,7 +1302,7 @@ static int stash_staged(struct stash_info *info, struct strbuf *out_patch,
static int stash_patch(struct stash_info *info, const struct pathspec *ps,
struct strbuf *out_patch, int quiet,
- struct add_p_opt *add_p_opt)
+ struct interactive_options *interactive_opts)
{
int ret = 0;
struct child_process cp_read_tree = CHILD_PROCESS_INIT;
@@ -1327,7 +1327,7 @@ static int stash_patch(struct stash_info *info, const struct pathspec *ps,
old_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT));
setenv(INDEX_ENVIRONMENT, the_repository->index_file, 1);
- ret = !!run_add_p(the_repository, ADD_P_STASH, add_p_opt, NULL, ps);
+ ret = !!run_add_p(the_repository, ADD_P_STASH, interactive_opts, NULL, ps);
the_repository->index_file = old_repo_index_file;
if (old_index_env && *old_index_env)
@@ -1422,7 +1422,8 @@ static int stash_working_tree(struct stash_info *info, const struct pathspec *ps
}
static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_buf,
- int include_untracked, int patch_mode, struct add_p_opt *add_p_opt,
+ int include_untracked, int patch_mode,
+ struct interactive_options *interactive_opts,
int only_staged, struct stash_info *info, struct strbuf *patch,
int quiet)
{
@@ -1504,7 +1505,7 @@ static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_b
untracked_commit_option = 1;
}
if (patch_mode) {
- ret = stash_patch(info, ps, patch, quiet, add_p_opt);
+ ret = stash_patch(info, ps, patch, quiet, interactive_opts);
if (ret < 0) {
if (!quiet)
fprintf_ln(stderr, _("Cannot save the current "
@@ -1590,7 +1591,8 @@ static int create_stash(int argc, const char **argv, const char *prefix UNUSED,
}
static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int quiet,
- int keep_index, int patch_mode, struct add_p_opt *add_p_opt,
+ int keep_index, int patch_mode,
+ struct interactive_options *interactive_opts,
int include_untracked, int only_staged)
{
int ret = 0;
@@ -1662,7 +1664,7 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q
if (stash_msg)
strbuf_addstr(&stash_msg_buf, stash_msg);
if (do_create_stash(ps, &stash_msg_buf, include_untracked, patch_mode,
- add_p_opt, only_staged, &info, &patch, quiet)) {
+ interactive_opts, only_staged, &info, &patch, quiet)) {
ret = -1;
goto done;
}
@@ -1835,7 +1837,7 @@ static int push_stash(int argc, const char **argv, const char *prefix,
const char *stash_msg = NULL;
char *pathspec_from_file = NULL;
struct pathspec ps;
- struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
struct option options[] = {
OPT_BOOL('k', "keep-index", &keep_index,
N_("keep index")),
@@ -1843,8 +1845,8 @@ static int push_stash(int argc, const char **argv, const char *prefix,
N_("stash staged changes only")),
OPT_BOOL('p', "patch", &patch_mode,
N_("stash in patch mode")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT__QUIET(&quiet, N_("quiet mode")),
OPT_BOOL('u', "include-untracked", &include_untracked,
N_("include untracked files in stash")),
@@ -1901,19 +1903,19 @@ static int push_stash(int argc, const char **argv, const char *prefix,
}
if (!patch_mode) {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--patch");
}
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
ret = do_push_stash(&ps, stash_msg, quiet, keep_index, patch_mode,
- &add_p_opt, include_untracked, only_staged);
+ &interactive_opts, include_untracked, only_staged);
clear_pathspec(&ps);
free(pathspec_from_file);
@@ -1938,7 +1940,7 @@ static int save_stash(int argc, const char **argv, const char *prefix,
const char *stash_msg = NULL;
struct pathspec ps;
struct strbuf stash_msg_buf = STRBUF_INIT;
- struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
struct option options[] = {
OPT_BOOL('k', "keep-index", &keep_index,
N_("keep index")),
@@ -1946,8 +1948,8 @@ static int save_stash(int argc, const char **argv, const char *prefix,
N_("stash staged changes only")),
OPT_BOOL('p', "patch", &patch_mode,
N_("stash in patch mode")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT__QUIET(&quiet, N_("quiet mode")),
OPT_BOOL('u', "include-untracked", &include_untracked,
N_("include untracked files in stash")),
@@ -1967,20 +1969,20 @@ static int save_stash(int argc, const char **argv, const char *prefix,
memset(&ps, 0, sizeof(ps));
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
if (!patch_mode) {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--patch");
}
ret = do_push_stash(&ps, stash_msg, quiet, keep_index,
- patch_mode, &add_p_opt, include_untracked,
+ patch_mode, &interactive_opts, include_untracked,
only_staged);
strbuf_release(&stash_msg_buf);
diff --git a/commit.h b/commit.h
index 1d6e0c7518..7b6e59d6c1 100644
--- a/commit.h
+++ b/commit.h
@@ -258,7 +258,7 @@ int for_each_commit_graft(each_commit_graft_fn, void *);
int interactive_add(struct repository *repo,
const char **argv,
const char *prefix,
- int patch, struct add_p_opt *add_p_opt);
+ int patch, struct interactive_options *opts);
struct commit_extra_header {
struct commit_extra_header *next;
--
2.51.0.417.g1ba7204a04.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH RFC v3 13/18] add-patch: remove dependency on "add-interactive" subsystem
2025-09-04 14:27 ` [PATCH RFC v3 00/18] " Patrick Steinhardt
` (11 preceding siblings ...)
2025-09-04 14:27 ` [PATCH RFC v3 12/18] add-patch: split out `struct interactive_options` Patrick Steinhardt
@ 2025-09-04 14:27 ` Patrick Steinhardt
2025-09-04 14:27 ` [PATCH RFC v3 14/18] add-patch: add support for in-memory index patching Patrick Steinhardt
` (7 subsequent siblings)
20 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-04 14:27 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
With the preceding commit we have split out interactive configuration
that is used by both "git add -p" and "git add -i". But we still
initialize that configuration in the "add -p" subsystem by calling
`init_add_i_state()`, even though we only do so to initialize the
interactive configuration as well as a repository pointer.
Stop doing so and instead store and initialize the interactive
configuration in `struct add_p_state` directly.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
add-patch.c | 68 ++++++++++++++++++++++++++++++++-----------------------------
1 file changed, 36 insertions(+), 32 deletions(-)
diff --git a/add-patch.c b/add-patch.c
index 45bc254e0c..1bcbc91de9 100644
--- a/add-patch.c
+++ b/add-patch.c
@@ -2,7 +2,6 @@
#define DISABLE_SIGN_COMPARE_WARNINGS
#include "git-compat-util.h"
-#include "add-interactive.h"
#include "add-patch.h"
#include "advice.h"
#include "config.h"
@@ -263,7 +262,8 @@ struct hunk {
};
struct add_p_state {
- struct add_i_state s;
+ struct repository *r;
+ struct interactive_config cfg;
struct strbuf answer, buf;
/* parsed diff */
@@ -385,7 +385,7 @@ static void add_p_state_clear(struct add_p_state *s)
for (i = 0; i < s->file_diff_nr; i++)
free(s->file_diff[i].hunk);
free(s->file_diff);
- clear_add_i_state(&s->s);
+ interactive_config_clear(&s->cfg);
}
__attribute__((format (printf, 2, 3)))
@@ -394,9 +394,9 @@ static void err(struct add_p_state *s, const char *fmt, ...)
va_list args;
va_start(args, fmt);
- fputs(s->s.cfg.error_color, stdout);
+ fputs(s->cfg.error_color, stdout);
vprintf(fmt, args);
- puts(s->s.cfg.reset_color);
+ puts(s->cfg.reset_color);
va_end(args);
}
@@ -414,7 +414,7 @@ static void setup_child_process(struct add_p_state *s,
cp->git_cmd = 1;
strvec_pushf(&cp->env,
- INDEX_ENVIRONMENT "=%s", s->s.r->index_file);
+ INDEX_ENVIRONMENT "=%s", s->r->index_file);
}
static int parse_range(const char **p,
@@ -519,12 +519,12 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
int res;
strvec_pushv(&args, s->mode->diff_cmd);
- if (s->s.cfg.context != -1)
- strvec_pushf(&args, "--unified=%i", s->s.cfg.context);
- if (s->s.cfg.interhunkcontext != -1)
- strvec_pushf(&args, "--inter-hunk-context=%i", s->s.cfg.interhunkcontext);
- if (s->s.cfg.interactive_diff_algorithm)
- strvec_pushf(&args, "--diff-algorithm=%s", s->s.cfg.interactive_diff_algorithm);
+ if (s->cfg.context != -1)
+ strvec_pushf(&args, "--unified=%i", s->cfg.context);
+ if (s->cfg.interhunkcontext != -1)
+ strvec_pushf(&args, "--inter-hunk-context=%i", s->cfg.interhunkcontext);
+ if (s->cfg.interactive_diff_algorithm)
+ strvec_pushf(&args, "--diff-algorithm=%s", s->cfg.interactive_diff_algorithm);
if (s->revision) {
struct object_id oid;
strvec_push(&args,
@@ -555,7 +555,7 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
if (want_color_fd(1, -1)) {
struct child_process colored_cp = CHILD_PROCESS_INIT;
- const char *diff_filter = s->s.cfg.interactive_diff_filter;
+ const char *diff_filter = s->cfg.interactive_diff_filter;
setup_child_process(s, &colored_cp, NULL);
xsnprintf((char *)args.v[color_arg_index], 8, "--color");
@@ -788,7 +788,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
hunk->colored_end - hunk->colored_start);
return;
} else {
- strbuf_addstr(out, s->s.cfg.fraginfo_color);
+ strbuf_addstr(out, s->cfg.fraginfo_color);
p = s->colored.buf + header->colored_extra_start;
len = header->colored_extra_end
- header->colored_extra_start;
@@ -810,7 +810,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
if (len)
strbuf_add(out, p, len);
else if (colored)
- strbuf_addf(out, "%s\n", s->s.cfg.reset_color);
+ strbuf_addf(out, "%s\n", s->cfg.reset_color);
else
strbuf_addch(out, '\n');
}
@@ -1198,12 +1198,12 @@ static void recolor_hunk(struct add_p_state *s, struct hunk *hunk)
strbuf_addstr(&s->colored,
plain[current] == '-' ?
- s->s.cfg.file_old_color :
+ s->cfg.file_old_color :
plain[current] == '+' ?
- s->s.cfg.file_new_color :
- s->s.cfg.context_color);
+ s->cfg.file_new_color :
+ s->cfg.context_color);
strbuf_add(&s->colored, plain + current, eol - current);
- strbuf_addstr(&s->colored, s->s.cfg.reset_color);
+ strbuf_addstr(&s->colored, s->cfg.reset_color);
if (next > eol)
strbuf_add(&s->colored, plain + eol, next - eol);
current = next;
@@ -1322,7 +1322,7 @@ static int run_apply_check(struct add_p_state *s,
static int read_single_character(struct add_p_state *s)
{
- if (s->s.cfg.use_single_key) {
+ if (s->cfg.use_single_key) {
int res = read_key_without_echo(&s->answer);
printf("%s\n", res == EOF ? "" : s->answer.buf);
return res;
@@ -1336,7 +1336,7 @@ static int read_single_character(struct add_p_state *s)
static int prompt_yesno(struct add_p_state *s, const char *prompt)
{
for (;;) {
- color_fprintf(stdout, s->s.cfg.prompt_color, "%s", _(prompt));
+ color_fprintf(stdout, s->cfg.prompt_color, "%s", _(prompt));
fflush(stdout);
if (read_single_character(s) == EOF)
return -1;
@@ -1617,15 +1617,15 @@ static int patch_update_file(struct add_p_state *s,
else
prompt_mode_type = PROMPT_HUNK;
- printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->s.cfg.prompt_color,
+ printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->cfg.prompt_color,
(uintmax_t)hunk_index + 1,
(uintmax_t)(file_diff->hunk_nr
? file_diff->hunk_nr
: 1));
printf(_(s->mode->prompt_mode[prompt_mode_type]),
s->buf.buf);
- if (*s->s.cfg.reset_color)
- fputs(s->s.cfg.reset_color, stdout);
+ if (*s->cfg.reset_color)
+ fputs(s->cfg.reset_color, stdout);
fflush(stdout);
if (read_single_character(s) == EOF)
break;
@@ -1782,7 +1782,7 @@ static int patch_update_file(struct add_p_state *s,
err(s, _("Sorry, cannot split this hunk"));
} else if (!split_hunk(s, file_diff,
hunk - file_diff->hunk)) {
- color_fprintf_ln(stdout, s->s.cfg.header_color,
+ color_fprintf_ln(stdout, s->cfg.header_color,
_("Split into %d hunks."),
(int)splittable_into);
rendered_hunk_index = -1;
@@ -1800,7 +1800,7 @@ static int patch_update_file(struct add_p_state *s,
} else if (s->answer.buf[0] == '?') {
const char *p = _(help_patch_remainder), *eol = p;
- color_fprintf(stdout, s->s.cfg.help_color, "%s",
+ color_fprintf(stdout, s->cfg.help_color, "%s",
_(s->mode->help_patch_text));
/*
@@ -1818,7 +1818,7 @@ static int patch_update_file(struct add_p_state *s,
if (*p != '?' && !strchr(s->buf.buf, *p))
continue;
- color_fprintf_ln(stdout, s->s.cfg.help_color,
+ color_fprintf_ln(stdout, s->cfg.help_color,
"%.*s", (int)(eol - p), p);
}
} else {
@@ -1838,7 +1838,7 @@ static int patch_update_file(struct add_p_state *s,
strbuf_reset(&s->buf);
reassemble_patch(s, file_diff, 0, &s->buf);
- discard_index(s->s.r->index);
+ discard_index(s->r->index);
if (s->mode->apply_for_checkout)
apply_for_checkout(s, &s->buf,
s->mode->is_reverse);
@@ -1849,8 +1849,8 @@ static int patch_update_file(struct add_p_state *s,
NULL, 0, NULL, 0))
error(_("'git apply' failed"));
}
- if (repo_read_index(s->s.r) >= 0)
- repo_refresh_and_write_index(s->s.r, REFRESH_QUIET, 0,
+ if (repo_read_index(s->r) >= 0)
+ repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,
1, NULL, NULL, NULL);
}
@@ -1863,11 +1863,15 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
const struct pathspec *ps)
{
struct add_p_state s = {
- { r }, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
+ .r = r,
+ .answer = STRBUF_INIT,
+ .buf = STRBUF_INIT,
+ .plain = STRBUF_INIT,
+ .colored = STRBUF_INIT,
};
size_t i, binary_count = 0;
- init_add_i_state(&s.s, r, opts);
+ interactive_config_init(&s.cfg, r, opts);
if (mode == ADD_P_STASH)
s.mode = &patch_mode_stash;
--
2.51.0.417.g1ba7204a04.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH RFC v3 14/18] add-patch: add support for in-memory index patching
2025-09-04 14:27 ` [PATCH RFC v3 00/18] " Patrick Steinhardt
` (12 preceding siblings ...)
2025-09-04 14:27 ` [PATCH RFC v3 13/18] add-patch: remove dependency on "add-interactive" subsystem Patrick Steinhardt
@ 2025-09-04 14:27 ` Patrick Steinhardt
2025-09-04 14:27 ` [PATCH RFC v3 15/18] wt-status: provide function to expose status for trees Patrick Steinhardt
` (6 subsequent siblings)
20 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-04 14:27 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
With `run_add_p()` callers have the ability to apply changes from a
specific revision to a repository's index. This infra supports several
different modes, like for example applying changes to the index,
worktree or both.
One feature that is missing though is the ability to apply changes to an
in-memory index different from the repository's index. Add a new
function `run_add_p_index()` to plug this gap.
This new function will be used in a subsequent commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
add-patch.c | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
add-patch.h | 8 +++++
2 files changed, 115 insertions(+), 3 deletions(-)
diff --git a/add-patch.c b/add-patch.c
index 1bcbc91de9..2a72c7b931 100644
--- a/add-patch.c
+++ b/add-patch.c
@@ -4,11 +4,13 @@
#include "git-compat-util.h"
#include "add-patch.h"
#include "advice.h"
+#include "commit.h"
#include "config.h"
#include "diff.h"
#include "editor.h"
#include "environment.h"
#include "gettext.h"
+#include "hex.h"
#include "object-name.h"
#include "pager.h"
#include "read-cache-ll.h"
@@ -263,6 +265,8 @@ struct hunk {
struct add_p_state {
struct repository *r;
+ struct index_state *index;
+ const char *index_file;
struct interactive_config cfg;
struct strbuf answer, buf;
@@ -414,7 +418,7 @@ static void setup_child_process(struct add_p_state *s,
cp->git_cmd = 1;
strvec_pushf(&cp->env,
- INDEX_ENVIRONMENT "=%s", s->r->index_file);
+ INDEX_ENVIRONMENT "=%s", s->index_file);
}
static int parse_range(const char **p,
@@ -1838,7 +1842,7 @@ static int patch_update_file(struct add_p_state *s,
strbuf_reset(&s->buf);
reassemble_patch(s, file_diff, 0, &s->buf);
- discard_index(s->r->index);
+ discard_index(s->index);
if (s->mode->apply_for_checkout)
apply_for_checkout(s, &s->buf,
s->mode->is_reverse);
@@ -1849,9 +1853,11 @@ static int patch_update_file(struct add_p_state *s,
NULL, 0, NULL, 0))
error(_("'git apply' failed"));
}
- if (repo_read_index(s->r) >= 0)
+ if (read_index_from(s->index, s->index_file, s->r->gitdir) >= 0 &&
+ s->index == s->r->index) {
repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,
1, NULL, NULL, NULL);
+ }
}
putchar('\n');
@@ -1864,6 +1870,8 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
{
struct add_p_state s = {
.r = r,
+ .index = r->index,
+ .index_file = r->index_file,
.answer = STRBUF_INIT,
.buf = STRBUF_INIT,
.plain = STRBUF_INIT,
@@ -1922,3 +1930,99 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
add_p_state_clear(&s);
return 0;
}
+
+int run_add_p_index(struct repository *r,
+ struct index_state *index,
+ const char *index_file,
+ struct interactive_options *opts,
+ const char *revision,
+ const struct pathspec *ps)
+{
+ struct patch_mode mode = {
+ .apply_args = { "--cached", NULL },
+ .apply_check_args = { "--cached", NULL },
+ .prompt_mode = {
+ N_("Stage mode change [y,n,q,a,d%s,?]? "),
+ N_("Stage deletion [y,n,q,a,d%s,?]? "),
+ N_("Stage addition [y,n,q,a,d%s,?]? "),
+ N_("Stage this hunk [y,n,q,a,d%s,?]? ")
+ },
+ .edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk "
+ "will immediately be marked for staging."),
+ .help_patch_text =
+ N_("y - stage this hunk\n"
+ "n - do not stage this hunk\n"
+ "q - quit; do not stage this hunk or any of the remaining "
+ "ones\n"
+ "a - stage this hunk and all later hunks in the file\n"
+ "d - do not stage this hunk or any of the later hunks in "
+ "the file\n"),
+ .index_only = 1,
+ };
+ struct add_p_state s = {
+ .r = r,
+ .index = index,
+ .index_file = index_file,
+ .answer = STRBUF_INIT,
+ .buf = STRBUF_INIT,
+ .plain = STRBUF_INIT,
+ .colored = STRBUF_INIT,
+ .mode = &mode,
+ .revision = revision,
+ };
+ struct strbuf parent_revision = STRBUF_INIT;
+ char parent_tree_oid[GIT_MAX_HEXSZ + 1];
+ size_t binary_count = 0;
+ struct commit *commit;
+ int ret;
+
+ commit = lookup_commit_reference_by_name(revision);
+ if (!commit) {
+ err(&s, _("Revision does not refer to a commit"));
+ ret = -1;
+ goto out;
+ }
+
+ if (commit->parents)
+ oid_to_hex_r(parent_tree_oid, get_commit_tree_oid(commit->parents->item));
+ else
+ oid_to_hex_r(parent_tree_oid, r->hash_algo->empty_tree);
+
+ strbuf_addf(&parent_revision, "%s~", revision);
+ mode.diff_cmd[0] = "diff-tree";
+ mode.diff_cmd[1] = "-r";
+ mode.diff_cmd[2] = parent_tree_oid;
+
+ interactive_config_init(&s.cfg, r, opts);
+
+ if (parse_diff(&s, ps) < 0) {
+ ret = -1;
+ goto out;
+ }
+
+ for (size_t i = 0; i < s.file_diff_nr; i++) {
+ if (s.file_diff[i].binary && !s.file_diff[i].hunk_nr)
+ binary_count++;
+ else if (patch_update_file(&s, s.file_diff + i))
+ break;
+ }
+
+ if (s.file_diff_nr == 0) {
+ err(&s, _("No changes."));
+ ret = -1;
+ goto out;
+ }
+
+ if (binary_count == s.file_diff_nr) {
+ err(&s, _("Only binary files changed."));
+ ret = -1;
+ goto out;
+ }
+
+ ret = 0;
+
+out:
+ strbuf_release(&parent_revision);
+ add_p_state_clear(&s);
+ return ret;
+}
diff --git a/add-patch.h b/add-patch.h
index 51c0d7bce9..d0edfec936 100644
--- a/add-patch.h
+++ b/add-patch.h
@@ -3,6 +3,7 @@
#include "color.h"
+struct index_state;
struct pathspec;
struct repository;
@@ -50,4 +51,11 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
struct interactive_options *opts, const char *revision,
const struct pathspec *ps);
+int run_add_p_index(struct repository *r,
+ struct index_state *index,
+ const char *index_file,
+ struct interactive_options *opts,
+ const char *revision,
+ const struct pathspec *ps);
+
#endif
--
2.51.0.417.g1ba7204a04.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH RFC v3 15/18] wt-status: provide function to expose status for trees
2025-09-04 14:27 ` [PATCH RFC v3 00/18] " Patrick Steinhardt
` (13 preceding siblings ...)
2025-09-04 14:27 ` [PATCH RFC v3 14/18] add-patch: add support for in-memory index patching Patrick Steinhardt
@ 2025-09-04 14:27 ` Patrick Steinhardt
2025-09-04 14:27 ` [PATCH RFC v3 16/18] sequencer: allow callers to provide mappings for the old commit Patrick Steinhardt
` (5 subsequent siblings)
20 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-04 14:27 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
The "wt-status" subsystem is responsible for printing status information
around the current state of the working tree. This most importantly
includes information around whether the working tree or the index have
any changes.
We're about to introduce a new command though where the changes in
neither of them are actually relevant to us. Instead, what we want is to
format the changes between two different trees. While it is a little bit
of a stretch to add this as functionality to _working tree_ status, it
doesn't make any sense to open-code this functionality, either.
Implement a new function `wt_status_collect_changes_trees()` that diffs
two trees and formats the status accordingly. This function is not yet
used, but will be in a subsequent commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
wt-status.c | 24 ++++++++++++++++++++++++
wt-status.h | 3 +++
2 files changed, 27 insertions(+)
diff --git a/wt-status.c b/wt-status.c
index 454601afa15..f09309d12e3 100644
--- a/wt-status.c
+++ b/wt-status.c
@@ -612,6 +612,30 @@ static void wt_status_collect_updated_cb(struct diff_queue_struct *q,
}
}
+void wt_status_collect_changes_trees(struct wt_status *s,
+ const struct object_id *old_treeish,
+ const struct object_id *new_treeish)
+{
+ struct diff_options opts = { 0 };
+
+ repo_diff_setup(s->repo, &opts);
+ opts.output_format = DIFF_FORMAT_CALLBACK;
+ opts.format_callback = wt_status_collect_updated_cb;
+ opts.format_callback_data = s;
+ opts.detect_rename = s->detect_rename >= 0 ? s->detect_rename : opts.detect_rename;
+ opts.rename_limit = s->rename_limit >= 0 ? s->rename_limit : opts.rename_limit;
+ opts.rename_score = s->rename_score >= 0 ? s->rename_score : opts.rename_score;
+ opts.flags.recursive = 1;
+ diff_setup_done(&opts);
+
+ diff_tree_oid(old_treeish, new_treeish, "", &opts);
+ diffcore_std(&opts);
+ diff_flush(&opts);
+ wt_status_get_state(s->repo, &s->state, 0);
+
+ diff_free(&opts);
+}
+
static void wt_status_collect_changes_worktree(struct wt_status *s)
{
struct rev_info rev;
diff --git a/wt-status.h b/wt-status.h
index 4e377ce62b8..b262e345f79 100644
--- a/wt-status.h
+++ b/wt-status.h
@@ -153,6 +153,9 @@ void wt_status_add_cut_line(struct wt_status *s);
void wt_status_prepare(struct repository *r, struct wt_status *s);
void wt_status_print(struct wt_status *s);
void wt_status_collect(struct wt_status *s);
+void wt_status_collect_changes_trees(struct wt_status *s,
+ const struct object_id *old_treeish,
+ const struct object_id *new_treeish);
/*
* Frees the buffers allocated by wt_status_collect.
*/
--
2.51.0.417.g1ba7204a04.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH RFC v3 16/18] sequencer: allow callers to provide mappings for the old commit
2025-09-04 14:27 ` [PATCH RFC v3 00/18] " Patrick Steinhardt
` (14 preceding siblings ...)
2025-09-04 14:27 ` [PATCH RFC v3 15/18] wt-status: provide function to expose status for trees Patrick Steinhardt
@ 2025-09-04 14:27 ` Patrick Steinhardt
2025-09-04 14:27 ` [PATCH RFC v3 17/18] builtin/history: implement "split" subcommand Patrick Steinhardt
` (4 subsequent siblings)
20 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-04 14:27 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
When executing the "rewritten-hook" we provide a list of commit mappings
that tell the hook the original commit ID as well as the commit ID that
specific commit was rewritten to. Typically, these should always be
different from one another, as otherwise there wouldn't have been a
rewrite of those commits in the first place.
With two upcoming subcommands for git-history(1) that is not the case
though, as we have already written the new commits ahead of time. We
only use the sequencer infrastructure in that case to insert those
commits at the correct position in the graph. This has the consequence
that original and rewritten object IDs will be the exact same, which is
quite unhelpful.
Introduce infrastructure so that the caller can tell us the original
object ID for such already-rewritten objects.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
sequencer.c | 7 +++++++
sequencer.h | 14 ++++++++++++++
2 files changed, 21 insertions(+)
diff --git a/sequencer.c b/sequencer.c
index 61447e5ccf..72d26b0eef 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -2214,6 +2214,13 @@ static void record_in_rewritten(struct object_id *oid,
const char *path;
FILE *out;
+ if (opts->old_oid_mappings) {
+ struct replay_oid_mapping *mapping =
+ oidmap_get(opts->old_oid_mappings, oid);
+ if (mapping)
+ oid = &mapping->rewritten_oid;
+ }
+
if (opts->action == REPLAY_HISTORY_EDIT)
path = git_path_rewritten_pending_file();
else
diff --git a/sequencer.h b/sequencer.h
index 0e0e7301b8..e6cc8aeb5d 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -1,6 +1,7 @@
#ifndef SEQUENCER_H
#define SEQUENCER_H
+#include "oidmap.h"
#include "strbuf.h"
#include "strvec.h"
#include "wt-status.h"
@@ -35,6 +36,12 @@ enum commit_msg_cleanup_mode {
struct replay_ctx;
struct replay_ctx* replay_ctx_new(void);
+/* Used as entry for the `original_oid_map`. */
+struct replay_oid_mapping {
+ struct oidmap_entry entry;
+ struct object_id rewritten_oid;
+};
+
struct replay_opts {
enum replay_action action;
@@ -83,6 +90,13 @@ struct replay_opts {
/* Only used by REPLAY_NONE */
struct rev_info *revs;
+ /*
+ * Used by the post-rewrite hook to fix up old object IDs. This can be
+ * used to rewrite the old object ID to whatever is stored as value in
+ * this map. The map contains `struct replay_oid_mapping` entries.
+ */
+ const struct oidmap *old_oid_mappings;
+
/* Private use */
struct replay_ctx *ctx;
};
--
2.51.0.417.g1ba7204a04.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH RFC v3 17/18] builtin/history: implement "split" subcommand
2025-09-04 14:27 ` [PATCH RFC v3 00/18] " Patrick Steinhardt
` (15 preceding siblings ...)
2025-09-04 14:27 ` [PATCH RFC v3 16/18] sequencer: allow callers to provide mappings for the old commit Patrick Steinhardt
@ 2025-09-04 14:27 ` Patrick Steinhardt
2025-09-10 14:04 ` Phillip Wood
2025-09-04 14:27 ` [PATCH RFC v3 18/18] builtin/history: implement "reword" subcommand Patrick Steinhardt
` (3 subsequent siblings)
20 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-04 14:27 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
It is quite a common use case that one wants to split up one commit into
multiple commits by moving parts of the changes of the original commit
out into a separate commit. This is quite an involved operation though:
1. Identify the commit in question that is to be dropped.
2. Perform an interactive rebase on top of that commit's parent.
3. Modify the instruction sheet to "edit" the commit that is to be
split up.
4. Drop the commit via "git reset HEAD~".
5. Stage changes that should go into the first commit and commit it.
6. Stage changes that should go into the second commit and commit it.
7. Finalize the rebase.
This is quite complex, and overall I would claim that most people who
are not experts in Git would struggle with this flow.
Introduce a new "split" subcommand for git-history(1) to make this way
easier. All the user needs to do is to say `git history split $COMMIT`.
From hereon, Git asks the user which parts of the commit shall be moved
out into a separate commit and, once done, asks the user for the commit
message. Git then creates that split-out commit and applies the original
commit on top of it.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-history.adoc | 60 ++++++
builtin/history.c | 313 ++++++++++++++++++++++++++-
t/meson.build | 1 +
t/t3453-history-split.sh | 468 +++++++++++++++++++++++++++++++++++++++++
4 files changed, 840 insertions(+), 2 deletions(-)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index b36cd925dd..6f0c64b90e 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -13,6 +13,7 @@ git history continue
git history quit
git history drop <commit>
git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)
+git history split [<options>] <commit> [--] [<pathspec>...]
DESCRIPTION
-----------
@@ -52,6 +53,26 @@ child commits, as that would lead to an empty branch.
be related to one another and must be reachable from the current `HEAD`
commit.
+`split [--message=<message>] <commit> [--] [<pathspec>...]`::
+ Interactively split up <commit> into two commits by choosing
+ hunks introduced by it that will be moved into the new split-out
+ commit. These hunks will then be written into a new commit that
+ becomes the parent of the previous commit. The original commit
+ stays intact, except that its parent will be the newly split-out
+ commit.
++
+The commit message of the new commit will be asked for by launching the
+configured editor, unless it has been specified with the `-m` option.
+Authorship of the commit will be the same as for the original commit.
++
+If passed, _<pathspec>_ can be used to limit which changes shall be split out
+of the original commit. Files not matching any of the pathspecs will remain
+part of the original commit. For more details, see the 'pathspec' entry in
+linkgit:gitglossary[7].
++
+It is invalid to select either all or no hunks, as that would lead to
+one of the commits becoming empty.
+
The following commands are used to manage an interrupted history-rewriting
operation:
@@ -111,6 +132,45 @@ f44a46e third
bf7438d first
----------
+Split a commit
+~~~~~~~~~~~~~~
+
+----------
+$ git log --stat --oneline
+3f81232 (HEAD -> main) original
+ bar | 1 +
+ foo | 1 +
+ 2 files changed, 2 insertions(+)
+
+$ git history split HEAD --message="split-out commit"
+diff --git a/bar b/bar
+new file mode 100644
+index 0000000..5716ca5
+--- /dev/null
++++ b/bar
+@@ -0,0 +1 @@
++bar
+(1/1) Stage addition [y,n,q,a,d,e,p,?]? y
+
+diff --git a/foo b/foo
+new file mode 100644
+index 0000000..257cc56
+--- /dev/null
++++ b/foo
+@@ -0,0 +1 @@
++foo
+(1/1) Stage addition [y,n,q,a,d,e,p,?]? n
+
+$ git log --stat --oneline
+7cebe64 (HEAD -> main) original
+ foo | 1 +
+ 1 file changed, 1 insertion(+)
+d1582f3 split-out commit
+ bar | 1 +
+ 1 file changed, 1 insertion(+)
+----------
+
+
CONFIGURATION
-------------
diff --git a/builtin/history.c b/builtin/history.c
index 16b516856e..df04b8dfc6 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -1,17 +1,27 @@
+/* Required for `comment_line_str`. */
+#define USE_THE_REPOSITORY_VARIABLE
+
#include "builtin.h"
#include "branch.h"
+#include "cache-tree.h"
#include "commit.h"
#include "commit-reach.h"
#include "config.h"
+#include "editor.h"
#include "environment.h"
#include "gettext.h"
#include "hex.h"
#include "object-name.h"
#include "parse-options.h"
+#include "path.h"
+#include "pathspec.h"
+#include "read-cache-ll.h"
#include "refs.h"
#include "reset.h"
#include "revision.h"
+#include "run-command.h"
#include "sequencer.h"
+#include "sparse-index.h"
static int cmd_history_abort(int argc,
const char **argv,
@@ -213,6 +223,7 @@ static int apply_commits(struct repository *repo,
const struct strvec *commits,
struct commit *head,
struct commit *base,
+ const struct oidmap *rewritten_commits,
const char *action)
{
struct setup_revision_opt revision_opts = {
@@ -238,6 +249,7 @@ static int apply_commits(struct repository *repo,
replay_opts.strategy = replay_opts.default_strategy;
replay_opts.default_strategy = NULL;
}
+ replay_opts.old_oid_mappings = rewritten_commits;
strvec_push(&args, "");
strvec_pushv(&args, commits->v);
@@ -403,7 +415,8 @@ static int cmd_history_drop(int argc,
if (ret < 0)
goto out;
- ret = apply_commits(repo, &commits, head, commit_to_drop, "drop");
+ ret = apply_commits(repo, &commits, head, commit_to_drop,
+ NULL, "drop");
if (ret < 0)
goto out;
}
@@ -505,7 +518,7 @@ static int cmd_history_reorder(int argc,
replace_commits(&commits, &commit_to_reorder->object.oid, NULL, 0);
replace_commits(&commits, &anchor->object.oid, replacement, ARRAY_SIZE(replacement));
- ret = apply_commits(repo, &commits, head, old, "reorder");
+ ret = apply_commits(repo, &commits, head, old, NULL, "reorder");
if (ret < 0)
goto out;
@@ -517,6 +530,300 @@ static int cmd_history_reorder(int argc,
return ret;
}
+static void change_data_free(void *util, const char *str UNUSED)
+{
+ struct wt_status_change_data *d = util;
+ free(d->rename_source);
+ free(d);
+}
+
+static int fill_commit_message(struct repository *repo,
+ const struct object_id *old_tree,
+ const struct object_id *new_tree,
+ const char *default_message,
+ const char *provided_message,
+ const char *action,
+ struct strbuf *out)
+{
+ if (!provided_message) {
+ const char *path = git_path_commit_editmsg();
+ const char *hint =
+ _("Please enter the commit message for the %s changes. Lines starting\n"
+ "with '%s' will be kept; you may remove them yourself if you want to.\n");
+ int verbose = 1;
+
+ strbuf_addstr(out, default_message);
+ strbuf_addch(out, '\n');
+ strbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);
+ write_file_buf(path, out->buf, out->len);
+
+ repo_config_get_bool(repo, "commit.verbose", &verbose);
+ if (verbose) {
+ struct wt_status s;
+
+ wt_status_prepare(repo, &s);
+ FREE_AND_NULL(s.branch);
+ s.ahead_behind_flags = AHEAD_BEHIND_QUICK;
+ s.commit_template = 1;
+ s.colopts = 0;
+ s.display_comment_prefix = 1;
+ s.hints = 0;
+ s.use_color = 0;
+ s.whence = FROM_COMMIT;
+ s.committable = 1;
+
+ s.fp = fopen(git_path_commit_editmsg(), "a");
+ if (!s.fp)
+ return error_errno(_("could not open '%s'"), git_path_commit_editmsg());
+
+ wt_status_collect_changes_trees(&s, old_tree, new_tree);
+ wt_status_print(&s);
+ wt_status_collect_free_buffers(&s);
+ string_list_clear_func(&s.change, change_data_free);
+ }
+
+ strbuf_reset(out);
+ if (launch_editor(path, out, NULL)) {
+ fprintf(stderr, _("Please supply the message using the -m option.\n"));
+ return -1;
+ }
+ strbuf_stripspace(out, comment_line_str);
+ } else {
+ strbuf_addstr(out, provided_message);
+ }
+
+ cleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);
+
+ if (!out->len) {
+ fprintf(stderr, _("Aborting commit due to empty commit message.\n"));
+ return -1;
+ }
+
+ return 0;
+}
+
+static int split_commit(struct repository *repo,
+ struct commit *original_commit,
+ struct pathspec *pathspec,
+ const char *commit_message,
+ struct object_id *out)
+{
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
+ struct strbuf index_file = STRBUF_INIT, split_message = STRBUF_INIT;
+ struct child_process read_tree_cmd = CHILD_PROCESS_INIT;
+ struct index_state index = INDEX_STATE_INIT(repo);
+ struct object_id original_commit_tree_oid, parent_tree_oid;
+ const char *original_message, *original_body, *ptr;
+ char original_commit_oid[GIT_MAX_HEXSZ + 1];
+ char *original_author = NULL;
+ struct commit_list *parents = NULL;
+ struct commit *first_commit;
+ struct tree *split_tree;
+ size_t len;
+ int ret;
+
+ if (original_commit->parents)
+ parent_tree_oid = *get_commit_tree_oid(original_commit->parents->item);
+ else
+ oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
+ original_commit_tree_oid = *get_commit_tree_oid(original_commit);
+
+ /*
+ * Construct the first commit. This is done by taking the original
+ * commit parent's tree and selectively patching changes from the diff
+ * between that parent and its child.
+ */
+ repo_git_path_replace(repo, &index_file, "%s", "history-split.index");
+
+ read_tree_cmd.git_cmd = 1;
+ strvec_pushf(&read_tree_cmd.env, "GIT_INDEX_FILE=%s", index_file.buf);
+ strvec_push(&read_tree_cmd.args, "read-tree");
+ strvec_push(&read_tree_cmd.args, oid_to_hex(&parent_tree_oid));
+ ret = run_command(&read_tree_cmd);
+ if (ret < 0)
+ goto out;
+
+ ret = read_index_from(&index, index_file.buf, repo->gitdir);
+ if (ret < 0) {
+ ret = error(_("failed reading temporary index"));
+ goto out;
+ }
+
+ oid_to_hex_r(original_commit_oid, &original_commit->object.oid);
+ ret = run_add_p_index(repo, &index, index_file.buf, &interactive_opts,
+ original_commit_oid, pathspec);
+ if (ret < 0)
+ goto out;
+
+ split_tree = write_in_core_index_as_tree(repo, &index);
+ if (!split_tree) {
+ ret = error(_("failed split tree"));
+ goto out;
+ }
+
+ unlink(index_file.buf);
+
+ /*
+ * We disallow the cases where either the split-out commit or the
+ * original commit would become empty. Consequently, if we see that the
+ * new tree ID matches either of those trees we abort.
+ */
+ if (oideq(&split_tree->object.oid, &parent_tree_oid)) {
+ ret = error(_("split commit is empty"));
+ goto out;
+ } else if (oideq(&split_tree->object.oid, &original_commit_tree_oid)) {
+ ret = error(_("split commit tree matches original commit"));
+ goto out;
+ }
+
+ /* We retain authorship of the original commit. */
+ original_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);
+ ptr = find_commit_header(original_message, "author", &len);
+ if (ptr)
+ original_author = xmemdupz(ptr, len);
+
+ ret = fill_commit_message(repo, &parent_tree_oid, &split_tree->object.oid,
+ "", commit_message, "split-out", &split_message);
+ if (ret < 0)
+ goto out;
+
+ ret = commit_tree(split_message.buf, split_message.len, &split_tree->object.oid,
+ original_commit->parents, &out[0], original_author, NULL);
+ if (ret < 0) {
+ ret = error(_("failed writing split-out commit"));
+ goto out;
+ }
+
+ /*
+ * The second commit is much simpler to construct, as we can simply use
+ * the original commit details, except that we adjust its parent to be
+ * the newly split-out commit.
+ */
+ find_commit_subject(original_message, &original_body);
+ first_commit = lookup_commit_reference(repo, &out[0]);
+ commit_list_append(first_commit, &parents);
+
+ ret = commit_tree(original_body, strlen(original_body), &original_commit_tree_oid,
+ parents, &out[1], original_author, NULL);
+ if (ret < 0) {
+ ret = error(_("failed writing second commit"));
+ goto out;
+ }
+
+ ret = 0;
+
+out:
+ if (index_file.len)
+ unlink(index_file.buf);
+ strbuf_release(&split_message);
+ strbuf_release(&index_file);
+ free_commit_list(parents);
+ free(original_author);
+ release_index(&index);
+ return ret;
+}
+
+static int cmd_history_split(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo)
+{
+ const char * const usage[] = {
+ N_("git history split [<options>] <commit>"),
+ NULL,
+ };
+ const char *commit_message = NULL;
+ struct option options[] = {
+ OPT_STRING('m', "message", &commit_message, N_("message"), N_("commit message")),
+ OPT_END(),
+ };
+ struct oidmap rewritten_commits = OIDMAP_INIT;
+ struct commit *original_commit, *head;
+ struct strvec commits = STRVEC_INIT;
+ struct commit_list *list = NULL;
+ struct object_id split_commits[2];
+ struct replay_oid_mapping mapping[2] = { 0 };
+ struct pathspec pathspec = { 0 };
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc < 1) {
+ ret = error(_("command expects a revision"));
+ goto out;
+ }
+ repo_config(repo, git_default_config, NULL);
+
+ original_commit = lookup_commit_reference_by_name(argv[0]);
+ if (!original_commit) {
+ ret = error(_("commit to be split cannot be found: %s"), argv[0]);
+ goto out;
+ }
+
+ if (original_commit->parents && original_commit->parents->next) {
+ ret = error(_("commit to be split must not be a merge commit"));
+ goto out;
+ }
+
+ head = lookup_commit_reference_by_name("HEAD");
+ if (!head) {
+ ret = error(_("could not resolve HEAD to a commit"));
+ goto out;
+ }
+
+ commit_list_append(original_commit, &list);
+ if (!repo_is_descendant_of(repo, original_commit, list)) {
+ ret = error (_("split commit must be reachable from current HEAD commit"));
+ goto out;
+ }
+
+ parse_pathspec(&pathspec, 0,
+ PATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,
+ prefix, argv + 1);
+
+ /*
+ * Collect the list of commits that we'll have to reapply now already.
+ * This ensures that we'll abort early on in case the range of commits
+ * contains merges, which we do not yet handle.
+ */
+ ret = collect_commits(repo, original_commit->parents ? original_commit->parents->item : NULL,
+ head, &commits);
+ if (ret < 0)
+ goto out;
+
+ /*
+ * Then we split up the commit and replace the original commit with the
+ * new new ones.
+ */
+ ret = split_commit(repo, original_commit, &pathspec,
+ commit_message, split_commits);
+ if (ret < 0)
+ goto out;
+
+ mapping[0].entry.oid = split_commits[0];
+ mapping[0].rewritten_oid = original_commit->object.oid;
+ oidmap_put(&rewritten_commits, &mapping[0]);
+ mapping[1].entry.oid = split_commits[1];
+ mapping[1].rewritten_oid = original_commit->object.oid;
+ oidmap_put(&rewritten_commits, &mapping[1]);
+
+ replace_commits(&commits, &original_commit->object.oid,
+ split_commits, ARRAY_SIZE(split_commits));
+
+ ret = apply_commits(repo, &commits, head, original_commit,
+ &rewritten_commits, "split");
+ if (ret < 0)
+ goto out;
+
+ ret = 0;
+
+out:
+ oidmap_clear(&rewritten_commits, 0);
+ clear_pathspec(&pathspec);
+ strvec_clear(&commits);
+ free_commit_list(list);
+ return ret;
+}
+
int cmd_history(int argc,
const char **argv,
const char *prefix,
@@ -528,6 +835,7 @@ int cmd_history(int argc,
N_("git history quit"),
N_("git history drop <commit>"),
N_("git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)"),
+ N_("git history split [<options>] <commit> [--] [<pathspec>...]"),
NULL,
};
parse_opt_subcommand_fn *fn = NULL;
@@ -537,6 +845,7 @@ int cmd_history(int argc,
OPT_SUBCOMMAND("quit", &fn, cmd_history_quit),
OPT_SUBCOMMAND("drop", &fn, cmd_history_drop),
OPT_SUBCOMMAND("reorder", &fn, cmd_history_reorder),
+ OPT_SUBCOMMAND("split", &fn, cmd_history_split),
OPT_END(),
};
diff --git a/t/meson.build b/t/meson.build
index 2bf7bcab5a..b3d33c8588 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -379,6 +379,7 @@ integration_tests = [
't3450-history.sh',
't3451-history-drop.sh',
't3452-history-reorder.sh',
+ 't3453-history-split.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
diff --git a/t/t3453-history-split.sh b/t/t3453-history-split.sh
new file mode 100755
index 0000000000..a6a652e7df
--- /dev/null
+++ b/t/t3453-history-split.sh
@@ -0,0 +1,468 @@
+#!/bin/sh
+
+test_description='tests for git-history split subcommand'
+
+. ./test-lib.sh
+
+set_fake_editor () {
+ write_script fake-editor.sh <<-\EOF &&
+ echo "split-out commit" >"$1"
+ EOF
+ test_set_editor "$(pwd)"/fake-editor.sh
+}
+
+expect_log () {
+ git log --format="%s" >actual &&
+ cat >expect &&
+ test_cmp expect actual
+}
+
+expect_tree_entries () {
+ git ls-tree --name-only "$1" >actual &&
+ cat >expect &&
+ test_cmp expect actual
+}
+
+test_expect_success 'refuses to work with merge commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ git branch branch &&
+ test_commit ours &&
+ git switch branch &&
+ test_commit theirs &&
+ git switch - &&
+ git merge theirs &&
+ test_must_fail git history split HEAD 2>err &&
+ test_grep "commit to be split must not be a merge commit" err &&
+ test_must_fail git history split HEAD~ 2>err &&
+ test_grep "cannot rearrange commit history with merges" err
+ )
+'
+
+test_expect_success 'refuses to work with changes in the worktree or index' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit initial &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ echo changed >bar &&
+ test_must_fail git history split -m message HEAD 2>err <<-EOF &&
+ y
+ n
+ EOF
+ test_grep "Your local changes to the following files would be overwritten" err &&
+
+ git add bar &&
+ test_must_fail git history split -m message HEAD 2>err <<-EOF &&
+ y
+ n
+ EOF
+ test_grep "Your local changes to the following files would be overwritten" err
+ )
+'
+
+test_expect_success 'can split up tip commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit initial &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ git symbolic-ref HEAD >expect &&
+ set_fake_editor &&
+ git history split HEAD <<-EOF &&
+ y
+ n
+ EOF
+ git symbolic-ref HEAD >actual &&
+ test_cmp expect actual &&
+
+ expect_log <<-EOF &&
+ split-me
+ split-out commit
+ initial
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ bar
+ initial.t
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ initial.t
+ EOF
+ )
+'
+
+test_expect_success 'can split up root commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m root &&
+ test_commit tip &&
+
+ set_fake_editor &&
+ git history split HEAD~ <<-EOF &&
+ y
+ n
+ EOF
+
+ expect_log <<-EOF &&
+ tip
+ root
+ split-out commit
+ EOF
+
+ expect_tree_entries HEAD~2 <<-EOF &&
+ bar
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ bar
+ foo
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ tip.t
+ EOF
+ )
+'
+
+test_expect_success 'can split up in-between commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit initial &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+ test_commit tip &&
+
+ set_fake_editor &&
+ git history split HEAD~ <<-EOF &&
+ y
+ n
+ EOF
+
+ expect_log <<-EOF &&
+ tip
+ split-me
+ split-out commit
+ initial
+ EOF
+
+ expect_tree_entries HEAD~2 <<-EOF &&
+ bar
+ initial.t
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ bar
+ foo
+ initial.t
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ initial.t
+ tip.t
+ EOF
+ )
+'
+
+test_expect_success 'can pick multiple hunks' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar baz foo qux &&
+ git add . &&
+ git commit -m split-me &&
+
+ git history split HEAD -m "split-out commit" <<-EOF &&
+ y
+ n
+ y
+ n
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ bar
+ foo
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ baz
+ foo
+ qux
+ EOF
+ )
+'
+
+
+test_expect_success 'can use only last hunk' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ git history split HEAD -m "split-out commit" <<-EOF &&
+ n
+ y
+ EOF
+
+ expect_log <<-EOF &&
+ split-me
+ split-out commit
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ foo
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ EOF
+ )
+'
+
+test_expect_success 'aborts with empty commit message' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ test_must_fail git history split HEAD -m "" <<-EOF 2>err &&
+ y
+ n
+ EOF
+ test_grep "Aborting commit due to empty commit message." err
+ )
+'
+
+test_expect_success 'can specify message via option' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ git history split HEAD -m "message option" <<-EOF &&
+ y
+ n
+ EOF
+
+ expect_log <<-EOF
+ split-me
+ message option
+ EOF
+ )
+'
+
+test_expect_success 'commit message editor sees split-out changes' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ write_script fake-editor.sh <<-\EOF &&
+ cp "$1" . &&
+ echo "some commit message" >>"$1"
+ EOF
+ test_set_editor "$(pwd)"/fake-editor.sh &&
+
+ git history split HEAD <<-EOF &&
+ y
+ n
+ EOF
+
+ cat >expect <<-EOF &&
+
+ # Please enter the commit message for the split-out changes. Lines starting
+ # with ${SQ}#${SQ} will be kept; you may remove them yourself if you want to.
+ # Changes to be committed:
+ # new file: bar
+ #
+ EOF
+ test_cmp expect COMMIT_EDITMSG &&
+
+ expect_log <<-EOF
+ split-me
+ some commit message
+ EOF
+ )
+'
+
+test_expect_success 'skips change summary with commit.verbose=false' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ write_script fake-editor.sh <<-\EOF &&
+ cp "$1" . &&
+ echo "some commit message" >>"$1"
+ EOF
+ test_set_editor "$(pwd)"/fake-editor.sh &&
+
+ git -c commit.verbose=false history split HEAD <<-EOF &&
+ y
+ n
+ EOF
+
+ cat >expect <<-EOF &&
+
+ # Please enter the commit message for the split-out changes. Lines starting
+ # with ${SQ}#${SQ} will be kept; you may remove them yourself if you want to.
+ EOF
+ test_cmp expect COMMIT_EDITMSG &&
+
+ expect_log <<-EOF
+ split-me
+ some commit message
+ EOF
+ )
+'
+
+test_expect_success 'can use pathspec to limit what gets split' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ git history split HEAD -m "message option" -- foo <<-EOF &&
+ y
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ foo
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ EOF
+ )
+'
+
+test_expect_success 'refuses to create empty split-out commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ test_must_fail git history split HEAD 2>err <<-EOF &&
+ n
+ n
+ EOF
+ test_grep "split commit is empty" err
+ )
+'
+
+test_expect_success 'hooks are executed for rewritten commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+ old_head=$(git rev-parse HEAD) &&
+
+ write_script .git/hooks/prepare-commit-msg <<-EOF &&
+ echo "prepare-commit-msg: \$@" >>"$(pwd)/hooks.log"
+ EOF
+ write_script .git/hooks/post-commit <<-EOF &&
+ echo "post-commit" >>"$(pwd)/hooks.log"
+ EOF
+ write_script .git/hooks/post-rewrite <<-EOF &&
+ {
+ echo "post-rewrite: \$@"
+ cat
+ } >>"$(pwd)/hooks.log"
+ EOF
+
+ set_fake_editor &&
+ git history split HEAD <<-EOF &&
+ y
+ n
+ EOF
+
+ expect_log <<-EOF &&
+ split-me
+ split-out commit
+ EOF
+
+ cat >expect <<-EOF &&
+ prepare-commit-msg: .git/COMMIT_EDITMSG message
+ post-commit
+ prepare-commit-msg: .git/COMMIT_EDITMSG message
+ post-commit
+ post-rewrite: history
+ $old_head $(git rev-parse HEAD~)
+ $old_head $(git rev-parse HEAD)
+ EOF
+ test_cmp expect hooks.log
+ )
+'
+
+test_expect_success 'refuses to create empty original commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ test_must_fail git history split HEAD 2>err <<-EOF &&
+ y
+ y
+ EOF
+ test_grep "split commit tree matches original commit" err
+ )
+'
+
+test_done
--
2.51.0.417.g1ba7204a04.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH RFC v3 17/18] builtin/history: implement "split" subcommand
2025-09-04 14:27 ` [PATCH RFC v3 17/18] builtin/history: implement "split" subcommand Patrick Steinhardt
@ 2025-09-10 14:04 ` Phillip Wood
2025-09-15 9:32 ` Patrick Steinhardt
0 siblings, 1 reply; 278+ messages in thread
From: Phillip Wood @ 2025-09-10 14:04 UTC (permalink / raw)
To: Patrick Steinhardt, git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
Hi Patrick
On 04/09/2025 15:27, Patrick Steinhardt wrote:
> It is quite a common use case that one wants to split up one commit into
> multiple commits by moving parts of the changes of the original commit
> out into a separate commit. This is quite an involved operation though:
>
> 1. Identify the commit in question that is to be dropped.
>
> 2. Perform an interactive rebase on top of that commit's parent.
>
> 3. Modify the instruction sheet to "edit" the commit that is to be
> split up.
>
> 4. Drop the commit via "git reset HEAD~".
>
> 5. Stage changes that should go into the first commit and commit it.
>
> 6. Stage changes that should go into the second commit and commit it.
>
> 7. Finalize the rebase.
>
> This is quite complex, and overall I would claim that most people who
> are not experts in Git would struggle with this flow.
>
> Introduce a new "split" subcommand for git-history(1) to make this way
> easier. All the user needs to do is to say `git history split $COMMIT`.
> From hereon, Git asks the user which parts of the commit shall be moved
> out into a separate commit and, once done, asks the user for the commit
> message. Git then creates that split-out commit and applies the original
> commit on top of it.
I like the idea of this command, but I think it would be much better to
prompt the user to edit the orginal message after creating each new
commit rather than asking them to write a new message for the first
commit that we create and then not letting them edit the message for the
second commit. We've got no way of knowing how they are splitting the
commit - they could be keeping most of the canges from the orginial in
the first commit in which case they probably want something simiar to
the orginial commit message for that one, or, they could be spitting out
something which means they need to edit the message when creating the
second commit.
If this was implemented in the sequencer then we'd be able to reuse the
existing code for creating commits and editing commit messages. It would
also make the "split" command available to "rebase -i".
Thanks
Phillip
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> Documentation/git-history.adoc | 60 ++++++
> builtin/history.c | 313 ++++++++++++++++++++++++++-
> t/meson.build | 1 +
> t/t3453-history-split.sh | 468 +++++++++++++++++++++++++++++++++++++++++
> 4 files changed, 840 insertions(+), 2 deletions(-)
>
> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
> index b36cd925dd..6f0c64b90e 100644
> --- a/Documentation/git-history.adoc
> +++ b/Documentation/git-history.adoc
> @@ -13,6 +13,7 @@ git history continue
> git history quit
> git history drop <commit>
> git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)
> +git history split [<options>] <commit> [--] [<pathspec>...]
>
> DESCRIPTION
> -----------
> @@ -52,6 +53,26 @@ child commits, as that would lead to an empty branch.
> be related to one another and must be reachable from the current `HEAD`
> commit.
>
> +`split [--message=<message>] <commit> [--] [<pathspec>...]`::
> + Interactively split up <commit> into two commits by choosing
> + hunks introduced by it that will be moved into the new split-out
> + commit. These hunks will then be written into a new commit that
> + becomes the parent of the previous commit. The original commit
> + stays intact, except that its parent will be the newly split-out
> + commit.
> ++
> +The commit message of the new commit will be asked for by launching the
> +configured editor, unless it has been specified with the `-m` option.
> +Authorship of the commit will be the same as for the original commit.
> ++
> +If passed, _<pathspec>_ can be used to limit which changes shall be split out
> +of the original commit. Files not matching any of the pathspecs will remain
> +part of the original commit. For more details, see the 'pathspec' entry in
> +linkgit:gitglossary[7].
> ++
> +It is invalid to select either all or no hunks, as that would lead to
> +one of the commits becoming empty.
> +
> The following commands are used to manage an interrupted history-rewriting
> operation:
>
> @@ -111,6 +132,45 @@ f44a46e third
> bf7438d first
> ----------
>
> +Split a commit
> +~~~~~~~~~~~~~~
> +
> +----------
> +$ git log --stat --oneline
> +3f81232 (HEAD -> main) original
> + bar | 1 +
> + foo | 1 +
> + 2 files changed, 2 insertions(+)
> +
> +$ git history split HEAD --message="split-out commit"
> +diff --git a/bar b/bar
> +new file mode 100644
> +index 0000000..5716ca5
> +--- /dev/null
> ++++ b/bar
> +@@ -0,0 +1 @@
> ++bar
> +(1/1) Stage addition [y,n,q,a,d,e,p,?]? y
> +
> +diff --git a/foo b/foo
> +new file mode 100644
> +index 0000000..257cc56
> +--- /dev/null
> ++++ b/foo
> +@@ -0,0 +1 @@
> ++foo
> +(1/1) Stage addition [y,n,q,a,d,e,p,?]? n
> +
> +$ git log --stat --oneline
> +7cebe64 (HEAD -> main) original
> + foo | 1 +
> + 1 file changed, 1 insertion(+)
> +d1582f3 split-out commit
> + bar | 1 +
> + 1 file changed, 1 insertion(+)
> +----------
> +
> +
> CONFIGURATION
> -------------
>
> diff --git a/builtin/history.c b/builtin/history.c
> index 16b516856e..df04b8dfc6 100644
> --- a/builtin/history.c
> +++ b/builtin/history.c
> @@ -1,17 +1,27 @@
> +/* Required for `comment_line_str`. */
> +#define USE_THE_REPOSITORY_VARIABLE
> +
> #include "builtin.h"
> #include "branch.h"
> +#include "cache-tree.h"
> #include "commit.h"
> #include "commit-reach.h"
> #include "config.h"
> +#include "editor.h"
> #include "environment.h"
> #include "gettext.h"
> #include "hex.h"
> #include "object-name.h"
> #include "parse-options.h"
> +#include "path.h"
> +#include "pathspec.h"
> +#include "read-cache-ll.h"
> #include "refs.h"
> #include "reset.h"
> #include "revision.h"
> +#include "run-command.h"
> #include "sequencer.h"
> +#include "sparse-index.h"
>
> static int cmd_history_abort(int argc,
> const char **argv,
> @@ -213,6 +223,7 @@ static int apply_commits(struct repository *repo,
> const struct strvec *commits,
> struct commit *head,
> struct commit *base,
> + const struct oidmap *rewritten_commits,
> const char *action)
> {
> struct setup_revision_opt revision_opts = {
> @@ -238,6 +249,7 @@ static int apply_commits(struct repository *repo,
> replay_opts.strategy = replay_opts.default_strategy;
> replay_opts.default_strategy = NULL;
> }
> + replay_opts.old_oid_mappings = rewritten_commits;
>
> strvec_push(&args, "");
> strvec_pushv(&args, commits->v);
> @@ -403,7 +415,8 @@ static int cmd_history_drop(int argc,
> if (ret < 0)
> goto out;
>
> - ret = apply_commits(repo, &commits, head, commit_to_drop, "drop");
> + ret = apply_commits(repo, &commits, head, commit_to_drop,
> + NULL, "drop");
> if (ret < 0)
> goto out;
> }
> @@ -505,7 +518,7 @@ static int cmd_history_reorder(int argc,
> replace_commits(&commits, &commit_to_reorder->object.oid, NULL, 0);
> replace_commits(&commits, &anchor->object.oid, replacement, ARRAY_SIZE(replacement));
>
> - ret = apply_commits(repo, &commits, head, old, "reorder");
> + ret = apply_commits(repo, &commits, head, old, NULL, "reorder");
> if (ret < 0)
> goto out;
>
> @@ -517,6 +530,300 @@ static int cmd_history_reorder(int argc,
> return ret;
> }
>
> +static void change_data_free(void *util, const char *str UNUSED)
> +{
> + struct wt_status_change_data *d = util;
> + free(d->rename_source);
> + free(d);
> +}
> +
> +static int fill_commit_message(struct repository *repo,
> + const struct object_id *old_tree,
> + const struct object_id *new_tree,
> + const char *default_message,
> + const char *provided_message,
> + const char *action,
> + struct strbuf *out)
> +{
> + if (!provided_message) {
> + const char *path = git_path_commit_editmsg();
> + const char *hint =
> + _("Please enter the commit message for the %s changes. Lines starting\n"
> + "with '%s' will be kept; you may remove them yourself if you want to.\n");
> + int verbose = 1;
> +
> + strbuf_addstr(out, default_message);
> + strbuf_addch(out, '\n');
> + strbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);
> + write_file_buf(path, out->buf, out->len);
> +
> + repo_config_get_bool(repo, "commit.verbose", &verbose);
> + if (verbose) {
> + struct wt_status s;
> +
> + wt_status_prepare(repo, &s);
> + FREE_AND_NULL(s.branch);
> + s.ahead_behind_flags = AHEAD_BEHIND_QUICK;
> + s.commit_template = 1;
> + s.colopts = 0;
> + s.display_comment_prefix = 1;
> + s.hints = 0;
> + s.use_color = 0;
> + s.whence = FROM_COMMIT;
> + s.committable = 1;
> +
> + s.fp = fopen(git_path_commit_editmsg(), "a");
> + if (!s.fp)
> + return error_errno(_("could not open '%s'"), git_path_commit_editmsg());
> +
> + wt_status_collect_changes_trees(&s, old_tree, new_tree);
> + wt_status_print(&s);
> + wt_status_collect_free_buffers(&s);
> + string_list_clear_func(&s.change, change_data_free);
> + }
> +
> + strbuf_reset(out);
> + if (launch_editor(path, out, NULL)) {
> + fprintf(stderr, _("Please supply the message using the -m option.\n"));
> + return -1;
> + }
> + strbuf_stripspace(out, comment_line_str);
> + } else {
> + strbuf_addstr(out, provided_message);
> + }
> +
> + cleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);
> +
> + if (!out->len) {
> + fprintf(stderr, _("Aborting commit due to empty commit message.\n"));
> + return -1;
> + }
> +
> + return 0;
> +}
> +
> +static int split_commit(struct repository *repo,
> + struct commit *original_commit,
> + struct pathspec *pathspec,
> + const char *commit_message,
> + struct object_id *out)
> +{
> + struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
> + struct strbuf index_file = STRBUF_INIT, split_message = STRBUF_INIT;
> + struct child_process read_tree_cmd = CHILD_PROCESS_INIT;
> + struct index_state index = INDEX_STATE_INIT(repo);
> + struct object_id original_commit_tree_oid, parent_tree_oid;
> + const char *original_message, *original_body, *ptr;
> + char original_commit_oid[GIT_MAX_HEXSZ + 1];
> + char *original_author = NULL;
> + struct commit_list *parents = NULL;
> + struct commit *first_commit;
> + struct tree *split_tree;
> + size_t len;
> + int ret;
> +
> + if (original_commit->parents)
> + parent_tree_oid = *get_commit_tree_oid(original_commit->parents->item);
> + else
> + oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
> + original_commit_tree_oid = *get_commit_tree_oid(original_commit);
> +
> + /*
> + * Construct the first commit. This is done by taking the original
> + * commit parent's tree and selectively patching changes from the diff
> + * between that parent and its child.
> + */
> + repo_git_path_replace(repo, &index_file, "%s", "history-split.index");
> +
> + read_tree_cmd.git_cmd = 1;
> + strvec_pushf(&read_tree_cmd.env, "GIT_INDEX_FILE=%s", index_file.buf);
> + strvec_push(&read_tree_cmd.args, "read-tree");
> + strvec_push(&read_tree_cmd.args, oid_to_hex(&parent_tree_oid));
> + ret = run_command(&read_tree_cmd);
> + if (ret < 0)
> + goto out;
> +
> + ret = read_index_from(&index, index_file.buf, repo->gitdir);
> + if (ret < 0) {
> + ret = error(_("failed reading temporary index"));
> + goto out;
> + }
> +
> + oid_to_hex_r(original_commit_oid, &original_commit->object.oid);
> + ret = run_add_p_index(repo, &index, index_file.buf, &interactive_opts,
> + original_commit_oid, pathspec);
> + if (ret < 0)
> + goto out;
> +
> + split_tree = write_in_core_index_as_tree(repo, &index);
> + if (!split_tree) {
> + ret = error(_("failed split tree"));
> + goto out;
> + }
> +
> + unlink(index_file.buf);
> +
> + /*
> + * We disallow the cases where either the split-out commit or the
> + * original commit would become empty. Consequently, if we see that the
> + * new tree ID matches either of those trees we abort.
> + */
> + if (oideq(&split_tree->object.oid, &parent_tree_oid)) {
> + ret = error(_("split commit is empty"));
> + goto out;
> + } else if (oideq(&split_tree->object.oid, &original_commit_tree_oid)) {
> + ret = error(_("split commit tree matches original commit"));
> + goto out;
> + }
> +
> + /* We retain authorship of the original commit. */
> + original_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);
> + ptr = find_commit_header(original_message, "author", &len);
> + if (ptr)
> + original_author = xmemdupz(ptr, len);
> +
> + ret = fill_commit_message(repo, &parent_tree_oid, &split_tree->object.oid,
> + "", commit_message, "split-out", &split_message);
> + if (ret < 0)
> + goto out;
> +
> + ret = commit_tree(split_message.buf, split_message.len, &split_tree->object.oid,
> + original_commit->parents, &out[0], original_author, NULL);
> + if (ret < 0) {
> + ret = error(_("failed writing split-out commit"));
> + goto out;
> + }
> +
> + /*
> + * The second commit is much simpler to construct, as we can simply use
> + * the original commit details, except that we adjust its parent to be
> + * the newly split-out commit.
> + */
> + find_commit_subject(original_message, &original_body);
> + first_commit = lookup_commit_reference(repo, &out[0]);
> + commit_list_append(first_commit, &parents);
> +
> + ret = commit_tree(original_body, strlen(original_body), &original_commit_tree_oid,
> + parents, &out[1], original_author, NULL);
> + if (ret < 0) {
> + ret = error(_("failed writing second commit"));
> + goto out;
> + }
> +
> + ret = 0;
> +
> +out:
> + if (index_file.len)
> + unlink(index_file.buf);
> + strbuf_release(&split_message);
> + strbuf_release(&index_file);
> + free_commit_list(parents);
> + free(original_author);
> + release_index(&index);
> + return ret;
> +}
> +
> +static int cmd_history_split(int argc,
> + const char **argv,
> + const char *prefix,
> + struct repository *repo)
> +{
> + const char * const usage[] = {
> + N_("git history split [<options>] <commit>"),
> + NULL,
> + };
> + const char *commit_message = NULL;
> + struct option options[] = {
> + OPT_STRING('m', "message", &commit_message, N_("message"), N_("commit message")),
> + OPT_END(),
> + };
> + struct oidmap rewritten_commits = OIDMAP_INIT;
> + struct commit *original_commit, *head;
> + struct strvec commits = STRVEC_INIT;
> + struct commit_list *list = NULL;
> + struct object_id split_commits[2];
> + struct replay_oid_mapping mapping[2] = { 0 };
> + struct pathspec pathspec = { 0 };
> + int ret;
> +
> + argc = parse_options(argc, argv, prefix, options, usage, 0);
> + if (argc < 1) {
> + ret = error(_("command expects a revision"));
> + goto out;
> + }
> + repo_config(repo, git_default_config, NULL);
> +
> + original_commit = lookup_commit_reference_by_name(argv[0]);
> + if (!original_commit) {
> + ret = error(_("commit to be split cannot be found: %s"), argv[0]);
> + goto out;
> + }
> +
> + if (original_commit->parents && original_commit->parents->next) {
> + ret = error(_("commit to be split must not be a merge commit"));
> + goto out;
> + }
> +
> + head = lookup_commit_reference_by_name("HEAD");
> + if (!head) {
> + ret = error(_("could not resolve HEAD to a commit"));
> + goto out;
> + }
> +
> + commit_list_append(original_commit, &list);
> + if (!repo_is_descendant_of(repo, original_commit, list)) {
> + ret = error (_("split commit must be reachable from current HEAD commit"));
> + goto out;
> + }
> +
> + parse_pathspec(&pathspec, 0,
> + PATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,
> + prefix, argv + 1);
> +
> + /*
> + * Collect the list of commits that we'll have to reapply now already.
> + * This ensures that we'll abort early on in case the range of commits
> + * contains merges, which we do not yet handle.
> + */
> + ret = collect_commits(repo, original_commit->parents ? original_commit->parents->item : NULL,
> + head, &commits);
> + if (ret < 0)
> + goto out;
> +
> + /*
> + * Then we split up the commit and replace the original commit with the
> + * new new ones.
> + */
> + ret = split_commit(repo, original_commit, &pathspec,
> + commit_message, split_commits);
> + if (ret < 0)
> + goto out;
> +
> + mapping[0].entry.oid = split_commits[0];
> + mapping[0].rewritten_oid = original_commit->object.oid;
> + oidmap_put(&rewritten_commits, &mapping[0]);
> + mapping[1].entry.oid = split_commits[1];
> + mapping[1].rewritten_oid = original_commit->object.oid;
> + oidmap_put(&rewritten_commits, &mapping[1]);
> +
> + replace_commits(&commits, &original_commit->object.oid,
> + split_commits, ARRAY_SIZE(split_commits));
> +
> + ret = apply_commits(repo, &commits, head, original_commit,
> + &rewritten_commits, "split");
> + if (ret < 0)
> + goto out;
> +
> + ret = 0;
> +
> +out:
> + oidmap_clear(&rewritten_commits, 0);
> + clear_pathspec(&pathspec);
> + strvec_clear(&commits);
> + free_commit_list(list);
> + return ret;
> +}
> +
> int cmd_history(int argc,
> const char **argv,
> const char *prefix,
> @@ -528,6 +835,7 @@ int cmd_history(int argc,
> N_("git history quit"),
> N_("git history drop <commit>"),
> N_("git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)"),
> + N_("git history split [<options>] <commit> [--] [<pathspec>...]"),
> NULL,
> };
> parse_opt_subcommand_fn *fn = NULL;
> @@ -537,6 +845,7 @@ int cmd_history(int argc,
> OPT_SUBCOMMAND("quit", &fn, cmd_history_quit),
> OPT_SUBCOMMAND("drop", &fn, cmd_history_drop),
> OPT_SUBCOMMAND("reorder", &fn, cmd_history_reorder),
> + OPT_SUBCOMMAND("split", &fn, cmd_history_split),
> OPT_END(),
> };
>
> diff --git a/t/meson.build b/t/meson.build
> index 2bf7bcab5a..b3d33c8588 100644
> --- a/t/meson.build
> +++ b/t/meson.build
> @@ -379,6 +379,7 @@ integration_tests = [
> 't3450-history.sh',
> 't3451-history-drop.sh',
> 't3452-history-reorder.sh',
> + 't3453-history-split.sh',
> 't3500-cherry.sh',
> 't3501-revert-cherry-pick.sh',
> 't3502-cherry-pick-merge.sh',
> diff --git a/t/t3453-history-split.sh b/t/t3453-history-split.sh
> new file mode 100755
> index 0000000000..a6a652e7df
> --- /dev/null
> +++ b/t/t3453-history-split.sh
> @@ -0,0 +1,468 @@
> +#!/bin/sh
> +
> +test_description='tests for git-history split subcommand'
> +
> +. ./test-lib.sh
> +
> +set_fake_editor () {
> + write_script fake-editor.sh <<-\EOF &&
> + echo "split-out commit" >"$1"
> + EOF
> + test_set_editor "$(pwd)"/fake-editor.sh
> +}
> +
> +expect_log () {
> + git log --format="%s" >actual &&
> + cat >expect &&
> + test_cmp expect actual
> +}
> +
> +expect_tree_entries () {
> + git ls-tree --name-only "$1" >actual &&
> + cat >expect &&
> + test_cmp expect actual
> +}
> +
> +test_expect_success 'refuses to work with merge commits' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + test_commit base &&
> + git branch branch &&
> + test_commit ours &&
> + git switch branch &&
> + test_commit theirs &&
> + git switch - &&
> + git merge theirs &&
> + test_must_fail git history split HEAD 2>err &&
> + test_grep "commit to be split must not be a merge commit" err &&
> + test_must_fail git history split HEAD~ 2>err &&
> + test_grep "cannot rearrange commit history with merges" err
> + )
> +'
> +
> +test_expect_success 'refuses to work with changes in the worktree or index' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + test_commit initial &&
> + touch bar foo &&
> + git add . &&
> + git commit -m split-me &&
> +
> + echo changed >bar &&
> + test_must_fail git history split -m message HEAD 2>err <<-EOF &&
> + y
> + n
> + EOF
> + test_grep "Your local changes to the following files would be overwritten" err &&
> +
> + git add bar &&
> + test_must_fail git history split -m message HEAD 2>err <<-EOF &&
> + y
> + n
> + EOF
> + test_grep "Your local changes to the following files would be overwritten" err
> + )
> +'
> +
> +test_expect_success 'can split up tip commit' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + test_commit initial &&
> + touch bar foo &&
> + git add . &&
> + git commit -m split-me &&
> +
> + git symbolic-ref HEAD >expect &&
> + set_fake_editor &&
> + git history split HEAD <<-EOF &&
> + y
> + n
> + EOF
> + git symbolic-ref HEAD >actual &&
> + test_cmp expect actual &&
> +
> + expect_log <<-EOF &&
> + split-me
> + split-out commit
> + initial
> + EOF
> +
> + expect_tree_entries HEAD~ <<-EOF &&
> + bar
> + initial.t
> + EOF
> +
> + expect_tree_entries HEAD <<-EOF
> + bar
> + foo
> + initial.t
> + EOF
> + )
> +'
> +
> +test_expect_success 'can split up root commit' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + touch bar foo &&
> + git add . &&
> + git commit -m root &&
> + test_commit tip &&
> +
> + set_fake_editor &&
> + git history split HEAD~ <<-EOF &&
> + y
> + n
> + EOF
> +
> + expect_log <<-EOF &&
> + tip
> + root
> + split-out commit
> + EOF
> +
> + expect_tree_entries HEAD~2 <<-EOF &&
> + bar
> + EOF
> +
> + expect_tree_entries HEAD~ <<-EOF &&
> + bar
> + foo
> + EOF
> +
> + expect_tree_entries HEAD <<-EOF
> + bar
> + foo
> + tip.t
> + EOF
> + )
> +'
> +
> +test_expect_success 'can split up in-between commit' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + test_commit initial &&
> + touch bar foo &&
> + git add . &&
> + git commit -m split-me &&
> + test_commit tip &&
> +
> + set_fake_editor &&
> + git history split HEAD~ <<-EOF &&
> + y
> + n
> + EOF
> +
> + expect_log <<-EOF &&
> + tip
> + split-me
> + split-out commit
> + initial
> + EOF
> +
> + expect_tree_entries HEAD~2 <<-EOF &&
> + bar
> + initial.t
> + EOF
> +
> + expect_tree_entries HEAD~ <<-EOF &&
> + bar
> + foo
> + initial.t
> + EOF
> +
> + expect_tree_entries HEAD <<-EOF
> + bar
> + foo
> + initial.t
> + tip.t
> + EOF
> + )
> +'
> +
> +test_expect_success 'can pick multiple hunks' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + touch bar baz foo qux &&
> + git add . &&
> + git commit -m split-me &&
> +
> + git history split HEAD -m "split-out commit" <<-EOF &&
> + y
> + n
> + y
> + n
> + EOF
> +
> + expect_tree_entries HEAD~ <<-EOF &&
> + bar
> + foo
> + EOF
> +
> + expect_tree_entries HEAD <<-EOF
> + bar
> + baz
> + foo
> + qux
> + EOF
> + )
> +'
> +
> +
> +test_expect_success 'can use only last hunk' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + touch bar foo &&
> + git add . &&
> + git commit -m split-me &&
> +
> + git history split HEAD -m "split-out commit" <<-EOF &&
> + n
> + y
> + EOF
> +
> + expect_log <<-EOF &&
> + split-me
> + split-out commit
> + EOF
> +
> + expect_tree_entries HEAD~ <<-EOF &&
> + foo
> + EOF
> +
> + expect_tree_entries HEAD <<-EOF
> + bar
> + foo
> + EOF
> + )
> +'
> +
> +test_expect_success 'aborts with empty commit message' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + touch bar foo &&
> + git add . &&
> + git commit -m split-me &&
> +
> + test_must_fail git history split HEAD -m "" <<-EOF 2>err &&
> + y
> + n
> + EOF
> + test_grep "Aborting commit due to empty commit message." err
> + )
> +'
> +
> +test_expect_success 'can specify message via option' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + touch bar foo &&
> + git add . &&
> + git commit -m split-me &&
> +
> + git history split HEAD -m "message option" <<-EOF &&
> + y
> + n
> + EOF
> +
> + expect_log <<-EOF
> + split-me
> + message option
> + EOF
> + )
> +'
> +
> +test_expect_success 'commit message editor sees split-out changes' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + touch bar foo &&
> + git add . &&
> + git commit -m split-me &&
> +
> + write_script fake-editor.sh <<-\EOF &&
> + cp "$1" . &&
> + echo "some commit message" >>"$1"
> + EOF
> + test_set_editor "$(pwd)"/fake-editor.sh &&
> +
> + git history split HEAD <<-EOF &&
> + y
> + n
> + EOF
> +
> + cat >expect <<-EOF &&
> +
> + # Please enter the commit message for the split-out changes. Lines starting
> + # with ${SQ}#${SQ} will be kept; you may remove them yourself if you want to.
> + # Changes to be committed:
> + # new file: bar
> + #
> + EOF
> + test_cmp expect COMMIT_EDITMSG &&
> +
> + expect_log <<-EOF
> + split-me
> + some commit message
> + EOF
> + )
> +'
> +
> +test_expect_success 'skips change summary with commit.verbose=false' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + touch bar foo &&
> + git add . &&
> + git commit -m split-me &&
> +
> + write_script fake-editor.sh <<-\EOF &&
> + cp "$1" . &&
> + echo "some commit message" >>"$1"
> + EOF
> + test_set_editor "$(pwd)"/fake-editor.sh &&
> +
> + git -c commit.verbose=false history split HEAD <<-EOF &&
> + y
> + n
> + EOF
> +
> + cat >expect <<-EOF &&
> +
> + # Please enter the commit message for the split-out changes. Lines starting
> + # with ${SQ}#${SQ} will be kept; you may remove them yourself if you want to.
> + EOF
> + test_cmp expect COMMIT_EDITMSG &&
> +
> + expect_log <<-EOF
> + split-me
> + some commit message
> + EOF
> + )
> +'
> +
> +test_expect_success 'can use pathspec to limit what gets split' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + touch bar foo &&
> + git add . &&
> + git commit -m split-me &&
> +
> + git history split HEAD -m "message option" -- foo <<-EOF &&
> + y
> + EOF
> +
> + expect_tree_entries HEAD~ <<-EOF &&
> + foo
> + EOF
> +
> + expect_tree_entries HEAD <<-EOF
> + bar
> + foo
> + EOF
> + )
> +'
> +
> +test_expect_success 'refuses to create empty split-out commit' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + test_commit base &&
> + touch bar foo &&
> + git add . &&
> + git commit -m split-me &&
> +
> + test_must_fail git history split HEAD 2>err <<-EOF &&
> + n
> + n
> + EOF
> + test_grep "split commit is empty" err
> + )
> +'
> +
> +test_expect_success 'hooks are executed for rewritten commits' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + touch bar foo &&
> + git add . &&
> + git commit -m split-me &&
> + old_head=$(git rev-parse HEAD) &&
> +
> + write_script .git/hooks/prepare-commit-msg <<-EOF &&
> + echo "prepare-commit-msg: \$@" >>"$(pwd)/hooks.log"
> + EOF
> + write_script .git/hooks/post-commit <<-EOF &&
> + echo "post-commit" >>"$(pwd)/hooks.log"
> + EOF
> + write_script .git/hooks/post-rewrite <<-EOF &&
> + {
> + echo "post-rewrite: \$@"
> + cat
> + } >>"$(pwd)/hooks.log"
> + EOF
> +
> + set_fake_editor &&
> + git history split HEAD <<-EOF &&
> + y
> + n
> + EOF
> +
> + expect_log <<-EOF &&
> + split-me
> + split-out commit
> + EOF
> +
> + cat >expect <<-EOF &&
> + prepare-commit-msg: .git/COMMIT_EDITMSG message
> + post-commit
> + prepare-commit-msg: .git/COMMIT_EDITMSG message
> + post-commit
> + post-rewrite: history
> + $old_head $(git rev-parse HEAD~)
> + $old_head $(git rev-parse HEAD)
> + EOF
> + test_cmp expect hooks.log
> + )
> +'
> +
> +test_expect_success 'refuses to create empty original commit' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + touch bar foo &&
> + git add . &&
> + git commit -m split-me &&
> +
> + test_must_fail git history split HEAD 2>err <<-EOF &&
> + y
> + y
> + EOF
> + test_grep "split commit tree matches original commit" err
> + )
> +'
> +
> +test_done
>
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC v3 17/18] builtin/history: implement "split" subcommand
2025-09-10 14:04 ` Phillip Wood
@ 2025-09-15 9:32 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-15 9:32 UTC (permalink / raw)
To: phillip.wood
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
On Wed, Sep 10, 2025 at 03:04:49PM +0100, Phillip Wood wrote:
> On 04/09/2025 15:27, Patrick Steinhardt wrote:
> > It is quite a common use case that one wants to split up one commit into
> > multiple commits by moving parts of the changes of the original commit
> > out into a separate commit. This is quite an involved operation though:
> >
> > 1. Identify the commit in question that is to be dropped.
> >
> > 2. Perform an interactive rebase on top of that commit's parent.
> >
> > 3. Modify the instruction sheet to "edit" the commit that is to be
> > split up.
> >
> > 4. Drop the commit via "git reset HEAD~".
> >
> > 5. Stage changes that should go into the first commit and commit it.
> >
> > 6. Stage changes that should go into the second commit and commit it.
> >
> > 7. Finalize the rebase.
> >
> > This is quite complex, and overall I would claim that most people who
> > are not experts in Git would struggle with this flow.
> >
> > Introduce a new "split" subcommand for git-history(1) to make this way
> > easier. All the user needs to do is to say `git history split $COMMIT`.
> > From hereon, Git asks the user which parts of the commit shall be moved
> > out into a separate commit and, once done, asks the user for the commit
> > message. Git then creates that split-out commit and applies the original
> > commit on top of it.
>
> I like the idea of this command, but I think it would be much better to
> prompt the user to edit the orginal message after creating each new commit
> rather than asking them to write a new message for the first commit that we
> create and then not letting them edit the message for the second commit.
> We've got no way of knowing how they are splitting the commit - they could
> be keeping most of the canges from the orginial in the first commit in which
> case they probably want something simiar to the orginial commit message for
> that one, or, they could be spitting out something which means they need to
> edit the message when creating the second commit.
Yeah, Junio already said the same. I can do that.
> If this was implemented in the sequencer then we'd be able to reuse the
> existing code for creating commits and editing commit messages. It would
> also make the "split" command available to "rebase -i".
True, having it available in "rebase -i" would be nice indeed.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH RFC v3 18/18] builtin/history: implement "reword" subcommand
2025-09-04 14:27 ` [PATCH RFC v3 00/18] " Patrick Steinhardt
` (16 preceding siblings ...)
2025-09-04 14:27 ` [PATCH RFC v3 17/18] builtin/history: implement "split" subcommand Patrick Steinhardt
@ 2025-09-04 14:27 ` Patrick Steinhardt
2025-09-10 14:05 ` Phillip Wood
2025-09-05 10:29 ` [PATCH RFC v3 00/18] Introduce git-history(1) command for easy history editing Kristoffer Haugsbakk
` (2 subsequent siblings)
20 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-04 14:27 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
Implement a new "reword" subcommand for git-history(1). This subcommand
is essentially the same as if a user performed an interactive rebase
with a single commit changed to use the "reword" verb.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-history.adoc | 5 +
builtin/history.c | 104 +++++++++++++++++++++
t/meson.build | 1 +
t/t3454-history-reword.sh | 202 +++++++++++++++++++++++++++++++++++++++++
4 files changed, 312 insertions(+)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index 6f0c64b90e..cbbcef3582 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -13,6 +13,7 @@ git history continue
git history quit
git history drop <commit>
git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)
+git history reword [<options>] <commit>
git history split [<options>] <commit> [--] [<pathspec>...]
DESCRIPTION
@@ -53,6 +54,10 @@ child commits, as that would lead to an empty branch.
be related to one another and must be reachable from the current `HEAD`
commit.
+`reword <commit> [--message=<message>]`::
+ Rewrite the commit message of the specified commit. All the other
+ details of this commit remain unchanged.
+
`split [--message=<message>] <commit> [--] [<pathspec>...]`::
Interactively split up <commit> into two commits by choosing
hunks introduced by it that will be moved into the new split-out
diff --git a/builtin/history.c b/builtin/history.c
index df04b8dfc6..39acf4df28 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -723,6 +723,108 @@ static int split_commit(struct repository *repo,
return ret;
}
+static int cmd_history_reword(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo)
+{
+ const char * const usage[] = {
+ N_("git history reword [<options>] <commit>"),
+ NULL,
+ };
+ const char *commit_message = NULL;
+ struct option options[] = {
+ OPT_STRING('m', "message", &commit_message, N_("message"), N_("commit message")),
+ OPT_END(),
+ };
+ struct strbuf final_message = STRBUF_INIT;
+ struct commit *original_commit, *head;
+ struct strvec commits = STRVEC_INIT;
+ struct object_id parent_tree_oid, original_commit_tree_oid;
+ struct object_id rewritten_commit;
+ const char *original_message, *original_body, *ptr;
+ struct oidmap rewritten_commits = OIDMAP_INIT;
+ struct replay_oid_mapping mapping = { 0 };
+ char *original_author = NULL;
+ size_t len;
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc != 1) {
+ ret = error(_("command expects a single revision"));
+ goto out;
+ }
+ repo_config(repo, git_default_config, NULL);
+
+ original_commit = lookup_commit_reference_by_name(argv[0]);
+ if (!original_commit) {
+ ret = error(_("commit to be reworded cannot be found: %s"), argv[0]);
+ goto out;
+ }
+
+ head = lookup_commit_reference_by_name("HEAD");
+ if (!head) {
+ ret = error(_("could not resolve HEAD to a commit"));
+ goto out;
+ }
+
+ /*
+ * Collect the list of commits that we'll have to reapply now already.
+ * This ensures that we'll abort early on in case the range of commits
+ * contains merges, which we do not yet handle.
+ */
+ ret = collect_commits(repo, original_commit->parents ? original_commit->parents->item : NULL,
+ head, &commits);
+ if (ret < 0)
+ goto out;
+
+ /* We retain authorship of the original commit. */
+ original_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);
+ ptr = find_commit_header(original_message, "author", &len);
+ if (ptr)
+ original_author = xmemdupz(ptr, len);
+ find_commit_subject(original_message, &original_body);
+
+ if (original_commit->parents)
+ parent_tree_oid = *get_commit_tree_oid(original_commit->parents->item);
+ else
+ oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
+ original_commit_tree_oid = *get_commit_tree_oid(original_commit);
+
+ ret = fill_commit_message(repo, &parent_tree_oid, &original_commit_tree_oid,
+ original_body, commit_message, "reworded", &final_message);
+ if (ret < 0)
+ goto out;
+
+ ret = commit_tree(final_message.buf, final_message.len,
+ &repo_get_commit_tree(repo, original_commit)->object.oid,
+ original_commit->parents, &rewritten_commit, original_author, NULL);
+ if (ret < 0) {
+ ret = error(_("failed writing reworded commit"));
+ goto out;
+ }
+
+ replace_commits(&commits, &original_commit->object.oid, &rewritten_commit, 1);
+
+ mapping.entry.oid = rewritten_commit;
+ mapping.rewritten_oid = original_commit->object.oid;
+ oidmap_put(&rewritten_commits, &mapping);
+
+ ret = apply_commits(repo, &commits, head, original_commit,
+ &rewritten_commits, "reword");
+ if (ret < 0)
+ goto out;
+
+ ret = 0;
+
+out:
+ oidmap_clear(&rewritten_commits, 0);
+ strbuf_release(&final_message);
+ strvec_clear(&commits);
+ free(original_author);
+ return ret;
+}
+
static int cmd_history_split(int argc,
const char **argv,
const char *prefix,
@@ -835,6 +937,7 @@ int cmd_history(int argc,
N_("git history quit"),
N_("git history drop <commit>"),
N_("git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)"),
+ N_("git history reword [<options>] <commit>"),
N_("git history split [<options>] <commit> [--] [<pathspec>...]"),
NULL,
};
@@ -845,6 +948,7 @@ int cmd_history(int argc,
OPT_SUBCOMMAND("quit", &fn, cmd_history_quit),
OPT_SUBCOMMAND("drop", &fn, cmd_history_drop),
OPT_SUBCOMMAND("reorder", &fn, cmd_history_reorder),
+ OPT_SUBCOMMAND("reword", &fn, cmd_history_reword),
OPT_SUBCOMMAND("split", &fn, cmd_history_split),
OPT_END(),
};
diff --git a/t/meson.build b/t/meson.build
index b3d33c8588..948223f453 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -380,6 +380,7 @@ integration_tests = [
't3451-history-drop.sh',
't3452-history-reorder.sh',
't3453-history-split.sh',
+ 't3454-history-reword.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
diff --git a/t/t3454-history-reword.sh b/t/t3454-history-reword.sh
new file mode 100755
index 0000000000..97bdd755fa
--- /dev/null
+++ b/t/t3454-history-reword.sh
@@ -0,0 +1,202 @@
+#!/bin/sh
+
+test_description='tests for git-history reword subcommand'
+
+. ./test-lib.sh
+
+test_expect_success 'refuses to work with merge commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ git branch branch &&
+ test_commit ours &&
+ git switch branch &&
+ test_commit theirs &&
+ git switch - &&
+ git merge theirs &&
+ test_must_fail git history reword HEAD~ 2>err &&
+ test_grep "cannot rearrange commit history with merges" err &&
+ test_must_fail git history reword HEAD 2>err &&
+ test_grep "cannot rearrange commit history with merges" err
+ )
+'
+
+test_expect_success 'refuses to work with changes in the worktree or index' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base file &&
+ echo foo >file &&
+ test_must_fail git history reword HEAD 2>err &&
+ test_grep "Your local changes to the following files would be overwritten" err &&
+ git add file &&
+ test_must_fail git history reword HEAD 2>err &&
+ test_grep "Your local changes to the following files would be overwritten" err
+ )
+'
+
+test_expect_success 'can reword tip of a branch' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ git symbolic-ref HEAD >expect &&
+ git history reword -m "third reworded" HEAD &&
+ git symbolic-ref HEAD >actual &&
+ test_cmp expect actual &&
+
+ cat >expect <<-EOF &&
+ third reworded
+ second
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'can reword commit in the middle' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ git symbolic-ref HEAD >expect &&
+ git history reword -m "second reworded" HEAD~ &&
+ git symbolic-ref HEAD >actual &&
+ test_cmp expect actual &&
+
+ cat >expect <<-EOF &&
+ third
+ second reworded
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'can reword root commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ git history reword -m "first reworded" HEAD~2 &&
+
+ cat >expect <<-EOF &&
+ third
+ second
+ first reworded
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'can use editor to rewrite commit message' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+
+ write_script fake-editor.sh <<-\EOF &&
+ cp "$1" . &&
+ printf "\namend a comment\n" >>"$1"
+ EOF
+ test_set_editor "$(pwd)"/fake-editor.sh &&
+ git history reword HEAD &&
+
+ cat >expect <<-EOF &&
+ first
+
+ # Please enter the commit message for the reworded changes. Lines starting
+ # with ${SQ}#${SQ} will be kept; you may remove them yourself if you want to.
+ # Changes to be committed:
+ # new file: first.t
+ #
+ EOF
+ test_cmp expect COMMIT_EDITMSG &&
+
+ cat >expect <<-EOF &&
+ first
+
+ amend a comment
+
+ EOF
+ git log --format=%B >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'hooks are executed for rewritten commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ write_script .git/hooks/prepare-commit-msg <<-EOF &&
+ echo "prepare-commit-msg: \$@" >>"$(pwd)/hooks.log"
+ EOF
+ write_script .git/hooks/post-commit <<-EOF &&
+ echo "post-commit" >>"$(pwd)/hooks.log"
+ EOF
+ write_script .git/hooks/post-rewrite <<-EOF &&
+ {
+ echo "post-rewrite: \$@"
+ cat
+ } >>"$(pwd)/hooks.log"
+ EOF
+
+ git history reword -m "second reworded" HEAD~ &&
+
+ cat >expect <<-EOF &&
+ third
+ second reworded
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual &&
+
+ cat >expect <<-EOF &&
+ prepare-commit-msg: .git/COMMIT_EDITMSG message
+ post-commit
+ prepare-commit-msg: .git/COMMIT_EDITMSG message
+ post-commit
+ post-rewrite: history
+ $(git rev-parse second) $(git rev-parse HEAD~)
+ $(git rev-parse third) $(git rev-parse HEAD)
+ EOF
+ test_cmp expect hooks.log
+ )
+'
+
+test_expect_success 'aborts with empty commit message' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+
+ test_must_fail git history reword -m "" HEAD 2>err &&
+ test_grep "Aborting commit due to empty commit message." err
+ )
+'
+
+test_done
--
2.51.0.417.g1ba7204a04.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH RFC v3 18/18] builtin/history: implement "reword" subcommand
2025-09-04 14:27 ` [PATCH RFC v3 18/18] builtin/history: implement "reword" subcommand Patrick Steinhardt
@ 2025-09-10 14:05 ` Phillip Wood
2025-09-15 9:32 ` Patrick Steinhardt
0 siblings, 1 reply; 278+ messages in thread
From: Phillip Wood @ 2025-09-10 14:05 UTC (permalink / raw)
To: Patrick Steinhardt, git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
Hi Patrick
On 04/09/2025 15:27, Patrick Steinhardt wrote:
> Implement a new "reword" subcommand for git-history(1). This subcommand
> is essentially the same as if a user performed an interactive rebase
> with a single commit changed to use the "reword" verb.
The sequencer already knows how to reword a commit, it would be much
simpler to reuse that code.
Thanks
Phillip
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> Documentation/git-history.adoc | 5 +
> builtin/history.c | 104 +++++++++++++++++++++
> t/meson.build | 1 +
> t/t3454-history-reword.sh | 202 +++++++++++++++++++++++++++++++++++++++++
> 4 files changed, 312 insertions(+)
>
> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
> index 6f0c64b90e..cbbcef3582 100644
> --- a/Documentation/git-history.adoc
> +++ b/Documentation/git-history.adoc
> @@ -13,6 +13,7 @@ git history continue
> git history quit
> git history drop <commit>
> git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)
> +git history reword [<options>] <commit>
> git history split [<options>] <commit> [--] [<pathspec>...]
>
> DESCRIPTION
> @@ -53,6 +54,10 @@ child commits, as that would lead to an empty branch.
> be related to one another and must be reachable from the current `HEAD`
> commit.
>
> +`reword <commit> [--message=<message>]`::
> + Rewrite the commit message of the specified commit. All the other
> + details of this commit remain unchanged.
> +
> `split [--message=<message>] <commit> [--] [<pathspec>...]`::
> Interactively split up <commit> into two commits by choosing
> hunks introduced by it that will be moved into the new split-out
> diff --git a/builtin/history.c b/builtin/history.c
> index df04b8dfc6..39acf4df28 100644
> --- a/builtin/history.c
> +++ b/builtin/history.c
> @@ -723,6 +723,108 @@ static int split_commit(struct repository *repo,
> return ret;
> }
>
> +static int cmd_history_reword(int argc,
> + const char **argv,
> + const char *prefix,
> + struct repository *repo)
> +{
> + const char * const usage[] = {
> + N_("git history reword [<options>] <commit>"),
> + NULL,
> + };
> + const char *commit_message = NULL;
> + struct option options[] = {
> + OPT_STRING('m', "message", &commit_message, N_("message"), N_("commit message")),
> + OPT_END(),
> + };
> + struct strbuf final_message = STRBUF_INIT;
> + struct commit *original_commit, *head;
> + struct strvec commits = STRVEC_INIT;
> + struct object_id parent_tree_oid, original_commit_tree_oid;
> + struct object_id rewritten_commit;
> + const char *original_message, *original_body, *ptr;
> + struct oidmap rewritten_commits = OIDMAP_INIT;
> + struct replay_oid_mapping mapping = { 0 };
> + char *original_author = NULL;
> + size_t len;
> + int ret;
> +
> + argc = parse_options(argc, argv, prefix, options, usage, 0);
> + if (argc != 1) {
> + ret = error(_("command expects a single revision"));
> + goto out;
> + }
> + repo_config(repo, git_default_config, NULL);
> +
> + original_commit = lookup_commit_reference_by_name(argv[0]);
> + if (!original_commit) {
> + ret = error(_("commit to be reworded cannot be found: %s"), argv[0]);
> + goto out;
> + }
> +
> + head = lookup_commit_reference_by_name("HEAD");
> + if (!head) {
> + ret = error(_("could not resolve HEAD to a commit"));
> + goto out;
> + }
> +
> + /*
> + * Collect the list of commits that we'll have to reapply now already.
> + * This ensures that we'll abort early on in case the range of commits
> + * contains merges, which we do not yet handle.
> + */
> + ret = collect_commits(repo, original_commit->parents ? original_commit->parents->item : NULL,
> + head, &commits);
> + if (ret < 0)
> + goto out;
> +
> + /* We retain authorship of the original commit. */
> + original_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);
> + ptr = find_commit_header(original_message, "author", &len);
> + if (ptr)
> + original_author = xmemdupz(ptr, len);
> + find_commit_subject(original_message, &original_body);
> +
> + if (original_commit->parents)
> + parent_tree_oid = *get_commit_tree_oid(original_commit->parents->item);
> + else
> + oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
> + original_commit_tree_oid = *get_commit_tree_oid(original_commit);
> +
> + ret = fill_commit_message(repo, &parent_tree_oid, &original_commit_tree_oid,
> + original_body, commit_message, "reworded", &final_message);
> + if (ret < 0)
> + goto out;
> +
> + ret = commit_tree(final_message.buf, final_message.len,
> + &repo_get_commit_tree(repo, original_commit)->object.oid,
> + original_commit->parents, &rewritten_commit, original_author, NULL);
> + if (ret < 0) {
> + ret = error(_("failed writing reworded commit"));
> + goto out;
> + }
> +
> + replace_commits(&commits, &original_commit->object.oid, &rewritten_commit, 1);
> +
> + mapping.entry.oid = rewritten_commit;
> + mapping.rewritten_oid = original_commit->object.oid;
> + oidmap_put(&rewritten_commits, &mapping);
> +
> + ret = apply_commits(repo, &commits, head, original_commit,
> + &rewritten_commits, "reword");
> + if (ret < 0)
> + goto out;
> +
> + ret = 0;
> +
> +out:
> + oidmap_clear(&rewritten_commits, 0);
> + strbuf_release(&final_message);
> + strvec_clear(&commits);
> + free(original_author);
> + return ret;
> +}
> +
> static int cmd_history_split(int argc,
> const char **argv,
> const char *prefix,
> @@ -835,6 +937,7 @@ int cmd_history(int argc,
> N_("git history quit"),
> N_("git history drop <commit>"),
> N_("git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)"),
> + N_("git history reword [<options>] <commit>"),
> N_("git history split [<options>] <commit> [--] [<pathspec>...]"),
> NULL,
> };
> @@ -845,6 +948,7 @@ int cmd_history(int argc,
> OPT_SUBCOMMAND("quit", &fn, cmd_history_quit),
> OPT_SUBCOMMAND("drop", &fn, cmd_history_drop),
> OPT_SUBCOMMAND("reorder", &fn, cmd_history_reorder),
> + OPT_SUBCOMMAND("reword", &fn, cmd_history_reword),
> OPT_SUBCOMMAND("split", &fn, cmd_history_split),
> OPT_END(),
> };
> diff --git a/t/meson.build b/t/meson.build
> index b3d33c8588..948223f453 100644
> --- a/t/meson.build
> +++ b/t/meson.build
> @@ -380,6 +380,7 @@ integration_tests = [
> 't3451-history-drop.sh',
> 't3452-history-reorder.sh',
> 't3453-history-split.sh',
> + 't3454-history-reword.sh',
> 't3500-cherry.sh',
> 't3501-revert-cherry-pick.sh',
> 't3502-cherry-pick-merge.sh',
> diff --git a/t/t3454-history-reword.sh b/t/t3454-history-reword.sh
> new file mode 100755
> index 0000000000..97bdd755fa
> --- /dev/null
> +++ b/t/t3454-history-reword.sh
> @@ -0,0 +1,202 @@
> +#!/bin/sh
> +
> +test_description='tests for git-history reword subcommand'
> +
> +. ./test-lib.sh
> +
> +test_expect_success 'refuses to work with merge commits' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + test_commit base &&
> + git branch branch &&
> + test_commit ours &&
> + git switch branch &&
> + test_commit theirs &&
> + git switch - &&
> + git merge theirs &&
> + test_must_fail git history reword HEAD~ 2>err &&
> + test_grep "cannot rearrange commit history with merges" err &&
> + test_must_fail git history reword HEAD 2>err &&
> + test_grep "cannot rearrange commit history with merges" err
> + )
> +'
> +
> +test_expect_success 'refuses to work with changes in the worktree or index' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + test_commit base file &&
> + echo foo >file &&
> + test_must_fail git history reword HEAD 2>err &&
> + test_grep "Your local changes to the following files would be overwritten" err &&
> + git add file &&
> + test_must_fail git history reword HEAD 2>err &&
> + test_grep "Your local changes to the following files would be overwritten" err
> + )
> +'
> +
> +test_expect_success 'can reword tip of a branch' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + test_commit first &&
> + test_commit second &&
> + test_commit third &&
> +
> + git symbolic-ref HEAD >expect &&
> + git history reword -m "third reworded" HEAD &&
> + git symbolic-ref HEAD >actual &&
> + test_cmp expect actual &&
> +
> + cat >expect <<-EOF &&
> + third reworded
> + second
> + first
> + EOF
> + git log --format=%s >actual &&
> + test_cmp expect actual
> + )
> +'
> +
> +test_expect_success 'can reword commit in the middle' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + test_commit first &&
> + test_commit second &&
> + test_commit third &&
> +
> + git symbolic-ref HEAD >expect &&
> + git history reword -m "second reworded" HEAD~ &&
> + git symbolic-ref HEAD >actual &&
> + test_cmp expect actual &&
> +
> + cat >expect <<-EOF &&
> + third
> + second reworded
> + first
> + EOF
> + git log --format=%s >actual &&
> + test_cmp expect actual
> + )
> +'
> +
> +test_expect_success 'can reword root commit' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + test_commit first &&
> + test_commit second &&
> + test_commit third &&
> + git history reword -m "first reworded" HEAD~2 &&
> +
> + cat >expect <<-EOF &&
> + third
> + second
> + first reworded
> + EOF
> + git log --format=%s >actual &&
> + test_cmp expect actual
> + )
> +'
> +
> +test_expect_success 'can use editor to rewrite commit message' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + test_commit first &&
> +
> + write_script fake-editor.sh <<-\EOF &&
> + cp "$1" . &&
> + printf "\namend a comment\n" >>"$1"
> + EOF
> + test_set_editor "$(pwd)"/fake-editor.sh &&
> + git history reword HEAD &&
> +
> + cat >expect <<-EOF &&
> + first
> +
> + # Please enter the commit message for the reworded changes. Lines starting
> + # with ${SQ}#${SQ} will be kept; you may remove them yourself if you want to.
> + # Changes to be committed:
> + # new file: first.t
> + #
> + EOF
> + test_cmp expect COMMIT_EDITMSG &&
> +
> + cat >expect <<-EOF &&
> + first
> +
> + amend a comment
> +
> + EOF
> + git log --format=%B >actual &&
> + test_cmp expect actual
> + )
> +'
> +
> +test_expect_success 'hooks are executed for rewritten commits' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + test_commit first &&
> + test_commit second &&
> + test_commit third &&
> +
> + write_script .git/hooks/prepare-commit-msg <<-EOF &&
> + echo "prepare-commit-msg: \$@" >>"$(pwd)/hooks.log"
> + EOF
> + write_script .git/hooks/post-commit <<-EOF &&
> + echo "post-commit" >>"$(pwd)/hooks.log"
> + EOF
> + write_script .git/hooks/post-rewrite <<-EOF &&
> + {
> + echo "post-rewrite: \$@"
> + cat
> + } >>"$(pwd)/hooks.log"
> + EOF
> +
> + git history reword -m "second reworded" HEAD~ &&
> +
> + cat >expect <<-EOF &&
> + third
> + second reworded
> + first
> + EOF
> + git log --format=%s >actual &&
> + test_cmp expect actual &&
> +
> + cat >expect <<-EOF &&
> + prepare-commit-msg: .git/COMMIT_EDITMSG message
> + post-commit
> + prepare-commit-msg: .git/COMMIT_EDITMSG message
> + post-commit
> + post-rewrite: history
> + $(git rev-parse second) $(git rev-parse HEAD~)
> + $(git rev-parse third) $(git rev-parse HEAD)
> + EOF
> + test_cmp expect hooks.log
> + )
> +'
> +
> +test_expect_success 'aborts with empty commit message' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + test_commit first &&
> +
> + test_must_fail git history reword -m "" HEAD 2>err &&
> + test_grep "Aborting commit due to empty commit message." err
> + )
> +'
> +
> +test_done
>
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC v3 18/18] builtin/history: implement "reword" subcommand
2025-09-10 14:05 ` Phillip Wood
@ 2025-09-15 9:32 ` Patrick Steinhardt
2025-09-15 14:10 ` Phillip Wood
0 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-15 9:32 UTC (permalink / raw)
To: phillip.wood
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
On Wed, Sep 10, 2025 at 03:05:04PM +0100, Phillip Wood wrote:
> On 04/09/2025 15:27, Patrick Steinhardt wrote:
> > Implement a new "reword" subcommand for git-history(1). This subcommand
> > is essentially the same as if a user performed an interactive rebase
> > with a single commit changed to use the "reword" verb.
>
> The sequencer already knows how to reword a commit, it would be much simpler
> to reuse that code.
I'll drop the second half of this patch series for now to reduce the
scope of this series a bit. But once I send the second half I'll have a
look at whether this can be simplified.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC v3 18/18] builtin/history: implement "reword" subcommand
2025-09-15 9:32 ` Patrick Steinhardt
@ 2025-09-15 14:10 ` Phillip Wood
2025-09-16 8:09 ` Patrick Steinhardt
0 siblings, 1 reply; 278+ messages in thread
From: Phillip Wood @ 2025-09-15 14:10 UTC (permalink / raw)
To: Patrick Steinhardt, phillip.wood
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
On 15/09/2025 10:32, Patrick Steinhardt wrote:
> On Wed, Sep 10, 2025 at 03:05:04PM +0100, Phillip Wood wrote:
>> On 04/09/2025 15:27, Patrick Steinhardt wrote:
>>> Implement a new "reword" subcommand for git-history(1). This subcommand
>>> is essentially the same as if a user performed an interactive rebase
>>> with a single commit changed to use the "reword" verb.
>>
>> The sequencer already knows how to reword a commit, it would be much simpler
>> to reuse that code.
>
> I'll drop the second half of this patch series for now to reduce the
> scope of this series a bit. But once I send the second half I'll have a
> look at whether this can be simplified.
If we passed a todo-list rather than just a list of commits to the
sequencer then it would be as simple as writing "reword $oid"[*] in the
todo-list.
Thanks
Phillip
[*] I'm probably simplifying slightly as we might want to tweak the
hooks that the sequencer runs for "git-history" but it should be fairly
easy.
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC v3 18/18] builtin/history: implement "reword" subcommand
2025-09-15 14:10 ` Phillip Wood
@ 2025-09-16 8:09 ` Patrick Steinhardt
2025-09-16 8:42 ` Phillip Wood
0 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-16 8:09 UTC (permalink / raw)
To: phillip.wood
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
On Mon, Sep 15, 2025 at 03:10:56PM +0100, Phillip Wood wrote:
> On 15/09/2025 10:32, Patrick Steinhardt wrote:
> > On Wed, Sep 10, 2025 at 03:05:04PM +0100, Phillip Wood wrote:
> > > On 04/09/2025 15:27, Patrick Steinhardt wrote:
> > > > Implement a new "reword" subcommand for git-history(1). This subcommand
> > > > is essentially the same as if a user performed an interactive rebase
> > > > with a single commit changed to use the "reword" verb.
> > >
> > > The sequencer already knows how to reword a commit, it would be much simpler
> > > to reuse that code.
> >
> > I'll drop the second half of this patch series for now to reduce the
> > scope of this series a bit. But once I send the second half I'll have a
> > look at whether this can be simplified.
>
> If we passed a todo-list rather than just a list of commits to the sequencer
> then it would be as simple as writing "reword $oid"[*] in the todo-list.
One downside though is that we'll now be in interactive-rebase mode
instead of in history-editing mode. We could of course introduce
history-editing mode as somewhat of an alias for interactive-rebases.
But the required changes are non-trivial and all over the place in
"sequencer.c", so I eventually stopped pursuing that route.
I still think it should be possible to at least separate out the actual
operations and share them across the sequencer and git-history(1) so
that we can avoid some of the duplication.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC v3 18/18] builtin/history: implement "reword" subcommand
2025-09-16 8:09 ` Patrick Steinhardt
@ 2025-09-16 8:42 ` Phillip Wood
0 siblings, 0 replies; 278+ messages in thread
From: Phillip Wood @ 2025-09-16 8:42 UTC (permalink / raw)
To: Patrick Steinhardt, phillip.wood
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
On 16/09/2025 09:09, Patrick Steinhardt wrote:
> On Mon, Sep 15, 2025 at 03:10:56PM +0100, Phillip Wood wrote:
>> On 15/09/2025 10:32, Patrick Steinhardt wrote:
>>> On Wed, Sep 10, 2025 at 03:05:04PM +0100, Phillip Wood wrote:
>>>> On 04/09/2025 15:27, Patrick Steinhardt wrote:
>>>>> Implement a new "reword" subcommand for git-history(1). This subcommand
>>>>> is essentially the same as if a user performed an interactive rebase
>>>>> with a single commit changed to use the "reword" verb.
>>>>
>>>> The sequencer already knows how to reword a commit, it would be much simpler
>>>> to reuse that code.
>>>
>>> I'll drop the second half of this patch series for now to reduce the
>>> scope of this series a bit. But once I send the second half I'll have a
>>> look at whether this can be simplified.
>>
>> If we passed a todo-list rather than just a list of commits to the sequencer
>> then it would be as simple as writing "reword $oid"[*] in the todo-list.
>
> One downside though is that we'll now be in interactive-rebase mode
> instead of in history-editing mode. We could of course introduce
> history-editing mode as somewhat of an alias for interactive-rebases.
> But the required changes are non-trivial and all over the place in
> "sequencer.c", so I eventually stopped pursuing that route.
I'm not sure I understand. At git-history uses the sequencer to
cherry-pick commits by calling sequencer_pick_revisions()[1]. Both
cherry-pick and rebase share the same todo-list format and the main loop
in pick_commits() processes that list in the same way for both commands.
As I said in a previous mail I think we should add a new entry point to
the sequencer that takes a todo list rather than a list of revisions but
that should be simple enough and then we get most of the functionality
we want such as rewording commits and updating refs more or less for
free. So I'm not sure what you mean by "we'll now be in
interactive-rebase" mode.
There are good arguments for not using the sequencer at all so that we
don't update the worktree each time we pick a commit (that would be a
lot more work though), but I cannot currently see a good reason for the
approach of using the sequencer to cherry-pick commits but implementing
all the other operations separately.
Thanks
Phillip
[1] At some point we should figure out how to teach "git status" to
distinguish between "git cherry-pick" and "git history" as I think at
the moment both probably look like a cherry-pick.
> I still think it should be possible to at least separate out the actual
> operations and share them across the sequencer and git-history(1) so
> that we can avoid some of the duplication.
>
> Patrick
>
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC v3 00/18] Introduce git-history(1) command for easy history editing
2025-09-04 14:27 ` [PATCH RFC v3 00/18] " Patrick Steinhardt
` (17 preceding siblings ...)
2025-09-04 14:27 ` [PATCH RFC v3 18/18] builtin/history: implement "reword" subcommand Patrick Steinhardt
@ 2025-09-05 10:29 ` Kristoffer Haugsbakk
2025-09-05 11:29 ` Patrick Steinhardt
2025-09-07 6:46 ` Elijah Newren
2025-09-10 20:05 ` Junio C Hamano
20 siblings, 1 reply; 278+ messages in thread
From: Kristoffer Haugsbakk @ 2025-09-05 10:29 UTC (permalink / raw)
To: Patrick Steinhardt, git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk
On Thu, Sep 4, 2025, at 16:27, Patrick Steinhardt wrote:
> Hi,
>[snip]
> I thus had a look at implementing some of these commands in Git itself,
> where the result is this patch series. Specifically, the following
> commands are introduced by this patch series:
>
> - `git history drop` to drop a specific commit. This is basically the
> same as jj-abandon(1).
>
> - `git history reorder` to reorder a specific commit before or after
> another commit. This is inspired by jj-new(1).
>
> - `git history split` takes a commit and splits it into two. This is
> basically the same as jj-split(1).
>
> If this is something we want to have I think it'd be just a starting
> point. There's other commands that I think are quite common and that
> might make sense to introduce eventually:
>
> - An equivalent to jj-absorb(1) would be awesome to have.
>
> - `git history reword` to change only the commit message of a specific
> commit.
The cover letter is a bit outdated. Reword has been here since v2.
>
> - `git history squash` to squash together multiple commits into one.
>
>[snip]
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC v3 00/18] Introduce git-history(1) command for easy history editing
2025-09-05 10:29 ` [PATCH RFC v3 00/18] Introduce git-history(1) command for easy history editing Kristoffer Haugsbakk
@ 2025-09-05 11:29 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-05 11:29 UTC (permalink / raw)
To: Kristoffer Haugsbakk
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk
On Fri, Sep 05, 2025 at 12:29:55PM +0200, Kristoffer Haugsbakk wrote:
> On Thu, Sep 4, 2025, at 16:27, Patrick Steinhardt wrote:
> > Hi,
> >[snip]
> > I thus had a look at implementing some of these commands in Git itself,
> > where the result is this patch series. Specifically, the following
> > commands are introduced by this patch series:
> >
> > - `git history drop` to drop a specific commit. This is basically the
> > same as jj-abandon(1).
> >
> > - `git history reorder` to reorder a specific commit before or after
> > another commit. This is inspired by jj-new(1).
> >
> > - `git history split` takes a commit and splits it into two. This is
> > basically the same as jj-split(1).
> >
> > If this is something we want to have I think it'd be just a starting
> > point. There's other commands that I think are quite common and that
> > might make sense to introduce eventually:
> >
> > - An equivalent to jj-absorb(1) would be awesome to have.
> >
> > - `git history reword` to change only the commit message of a specific
> > commit.
>
> The cover letter is a bit outdated. Reword has been here since v2.
Good point, addressed locally.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC v3 00/18] Introduce git-history(1) command for easy history editing
2025-09-04 14:27 ` [PATCH RFC v3 00/18] " Patrick Steinhardt
` (18 preceding siblings ...)
2025-09-05 10:29 ` [PATCH RFC v3 00/18] Introduce git-history(1) command for easy history editing Kristoffer Haugsbakk
@ 2025-09-07 6:46 ` Elijah Newren
2025-09-10 14:05 ` Phillip Wood
2025-09-15 9:33 ` Patrick Steinhardt
2025-09-10 20:05 ` Junio C Hamano
20 siblings, 2 replies; 278+ messages in thread
From: Elijah Newren @ 2025-09-07 6:46 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
On Thu, Sep 4, 2025 at 11:43 PM Patrick Steinhardt <ps@pks.im> wrote:
>
> Hi,
>
> over recent months I've been playing around with Jujutsu quite
> frequently. While I still prefer using Git, there's been a couple
> features in it that I really like and that I'd like to have in Git, as
> well.
>
> A copule of these features relate to history editing. Most importantly,
> I really dig the following commands:
>
> - jj-abandon(1) to drop a specific commit from your history.
>
> - jj-absorb(1) to take some changes and automatically apply them to
> commits in your history that last modified the respective hunks.
>
> - jj-split(1) to split a commit into two.
>
> - jj-new(1) to insert a new commit after or before a specific other
> commit.
Cool, I had been thinking of adding some jj-like functionality as
well, to git-replay in my case, though I was more interested in fixing
up the infrastructure to handle replaying merges sanely first.
> Not all of these commands can be ported directly into Git. jj-new(1) for
> example doesn't really make a ton of sense for us, I'd claim. But some
> of these commands _do_ make sense.
>
> I thus had a look at implementing some of these commands in Git itself,
> where the result is this patch series. Specifically, the following
> commands are introduced by this patch series:
>
> - `git history drop` to drop a specific commit. This is basically the
> same as jj-abandon(1).
>
> - `git history reorder` to reorder a specific commit before or after
> another commit. This is inspired by jj-new(1).
>
> - `git history split` takes a commit and splits it into two. This is
> basically the same as jj-split(1).
>
> If this is something we want to have I think it'd be just a starting
> point. There's other commands that I think are quite common and that
> might make sense to introduce eventually:
>
> - An equivalent to jj-absorb(1) would be awesome to have.
>
> - `git history reword` to change only the commit message of a specific
> commit.
>
> - `git history squash` to squash together multiple commits into one.
>
> In the end, I'd like us to learn from what people like about Jujutsu and
> apply those learnings to Git. We won't be able to apply all learnings
> from Jujutsu, as the workflow is quite different there due to the lack
> of the index. But other things we certainly can apply to Git directly.
So, this brings up a question. Should we have git-rebase &
git-cherry-pick & git-replay & git-history, or should we consolidate?
I had envisioned having git-replay consolidate both cherry-pick and
rebase functionality into one (then got pulled away by combination of
work reassgniment & multiple life crises hitting at once taking my
focus away for quite some time). But now we're going in the other
direction. And further along that other direction is another extreme
-- just having these be top-level commands, e.g. "git reorder", "git
split", etc.
In a separate conversation we had (and I hope I'm paraphrasing
correctly; if not please correct me), you mentioned you wanted
git-history to be the home of history rewriting, and viewed git-replay
as just a server side thing (whereas I created git-replay specifically
as a user-focusing thing and then Christian changed it into a
server-side thing since that part was complete and enough for his
purposes). But if git history is the home of history editing, how far
does that go? Do we have a "git history reset"? "git history
commit"? "git history fast-export/fast-import" "git history
filter-repo"? Or is it just the home for certain kinds of history
rewriting operations? If so, which ones?
That all said, I'm a big fan of the idea of incorporating more of jj
capabilities, and you clearly marked the command as experimental
(thanks!), which leave us room to adjust later if we don't like this
path. So I don't want to serve as a roadblock, I just think it's a
useful conversation to have...
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC v3 00/18] Introduce git-history(1) command for easy history editing
2025-09-07 6:46 ` Elijah Newren
@ 2025-09-10 14:05 ` Phillip Wood
2025-09-10 14:08 ` Phillip Wood
2025-09-15 9:33 ` Patrick Steinhardt
1 sibling, 1 reply; 278+ messages in thread
From: Phillip Wood @ 2025-09-10 14:05 UTC (permalink / raw)
To: Elijah Newren, Patrick Steinhardt
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
On 07/09/2025 07:46, Elijah Newren wrote:
> On Thu, Sep 4, 2025 at 11:43 PM Patrick Steinhardt <ps@pks.im> wrote:
>>
>> Hi,
>>
>> over recent months I've been playing around with Jujutsu quite
>> frequently. While I still prefer using Git, there's been a couple
>> features in it that I really like and that I'd like to have in Git, as
>> well.
Fantastic, thanks for working on this
>> A copule of these features relate to history editing. Most importantly,
>> I really dig the following commands:
>>
>> - jj-abandon(1) to drop a specific commit from your history.
>>
>> - jj-absorb(1) to take some changes and automatically apply them to
>> commits in your history that last modified the respective hunks.
>>
>> - jj-split(1) to split a commit into two.
>>
>> - jj-new(1) to insert a new commit after or before a specific other
>> commit.
>
> Cool, I had been thinking of adding some jj-like functionality as
> well, to git-replay in my case, though I was more interested in fixing
> up the infrastructure to handle replaying merges sanely first.
Interesting, one of the questions I have about this series is whether it
makes sense to use the sequencer or extend git-replay. I do like the
idea and name "history" though. I've been using a wrapper around
git-rebase that I call "git-rewrite" that lets me amend, reword, or drop
commits and rewrite all the branches that contain the modified commit
for quite a while now. I find amending the commit directly rather than
using fixup commits and then squashing much more convenient and it
avoids the problem of the fixup having conflicts when you try and squash
it (though not the conflicts caused by amending the commit of course).
One feature I particularly like, which we might want to add to "git
history" in the future, is being able to specify a filename, line pair
instead of a commit name. The script then uses "git diff" to map the
line number to the file in HEAD and "git blame" to find the relevant
commit. This makes it easy to start a rewrite directly from my editor
when I see something that needs fixing up.
If we do want to use the sequencer then I think we need to decide
exactly what behavior we want from the new command with regard to
running hooks and copying commit headers and implement that behavior
rather than just accepting the status quo which is largely a historical
accident. I would suggest that for commits that we're not modifing we
should not be running any hooks. For commits that are reworded we should
be running the "commit-msg" hook and possibly "prepare-commit-msg" as
well but no others. Where we're amending the commit content or splitting
a commit then we should be running the "pre-commit" hook as well.
Currently "git-replay" copies any extra commit headers when it creates a
new commit whereas "git-rebase" does not. There is some discussion at
[1] where people were pushing back against copying extra headers by default.
I think it would make sense in the long run to update all the branches
that contain the modified commit. To do that we can use the sequencer's
"update-ref" command. To that end I think we should add a new entry
point to the sequencer that takes a todo list rather than a list of
commits to pick. That would also allow us to implement the "split"
command in the sequencer and reuse the infrastructure that already
exists for rewording commits. The way this series is currently
implemented makes it hard to extend in the future because it is based
around cherry-picking commits rather than creating a todo list for the
sequencer to execute.
Thanks
Phillip
>> Not all of these commands can be ported directly into Git. jj-new(1) for
>> example doesn't really make a ton of sense for us, I'd claim. But some
>> of these commands _do_ make sense.
>>
>> I thus had a look at implementing some of these commands in Git itself,
>> where the result is this patch series. Specifically, the following
>> commands are introduced by this patch series:
>>
>> - `git history drop` to drop a specific commit. This is basically the
>> same as jj-abandon(1).
>>
>> - `git history reorder` to reorder a specific commit before or after
>> another commit. This is inspired by jj-new(1).
>>
>> - `git history split` takes a commit and splits it into two. This is
>> basically the same as jj-split(1).
>>>> If this is something we want to have I think it'd be just a starting
>> point. There's other commands that I think are quite common and that
>> might make sense to introduce eventually:
>>
>> - An equivalent to jj-absorb(1) would be awesome to have.
>>
>> - `git history reword` to change only the commit message of a specific
>> commit.
>>
>> - `git history squash` to squash together multiple commits into one.
>>
>> In the end, I'd like us to learn from what people like about Jujutsu and
>> apply those learnings to Git. We won't be able to apply all learnings
>> from Jujutsu, as the workflow is quite different there due to the lack
>> of the index. But other things we certainly can apply to Git directly.
>
> So, this brings up a question. Should we have git-rebase &
> git-cherry-pick & git-replay & git-history, or should we consolidate?
> I had envisioned having git-replay consolidate both cherry-pick and
> rebase functionality into one (then got pulled away by combination of
> work reassgniment & multiple life crises hitting at once taking my
> focus away for quite some time). But now we're going in the other
> direction. And further along that other direction is another extreme
> -- just having these be top-level commands, e.g. "git reorder", "git
> split", etc.
>
> In a separate conversation we had (and I hope I'm paraphrasing
> correctly; if not please correct me), you mentioned you wanted
> git-history to be the home of history rewriting, and viewed git-replay
> as just a server side thing (whereas I created git-replay specifically
> as a user-focusing thing and then Christian changed it into a
> server-side thing since that part was complete and enough for his
> purposes). But if git history is the home of history editing, how far
> does that go? Do we have a "git history reset"? "git history
> commit"? "git history fast-export/fast-import" "git history
> filter-repo"? Or is it just the home for certain kinds of history
> rewriting operations? If so, which ones?
>
> That all said, I'm a big fan of the idea of incorporating more of jj
> capabilities, and you clearly marked the command as experimental
> (thanks!), which leave us room to adjust later if we don't like this
> path. So I don't want to serve as a roadblock, I just think it's a
> useful conversation to have...
>
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC v3 00/18] Introduce git-history(1) command for easy history editing
2025-09-07 6:46 ` Elijah Newren
2025-09-10 14:05 ` Phillip Wood
@ 2025-09-15 9:33 ` Patrick Steinhardt
2025-09-16 11:23 ` Oswald Buddenhagen
1 sibling, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-15 9:33 UTC (permalink / raw)
To: Elijah Newren
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
On Sat, Sep 06, 2025 at 11:46:48PM -0700, Elijah Newren wrote:
> So, this brings up a question. Should we have git-rebase &
> git-cherry-pick & git-replay & git-history, or should we consolidate?
> I had envisioned having git-replay consolidate both cherry-pick and
> rebase functionality into one (then got pulled away by combination of
> work reassgniment & multiple life crises hitting at once taking my
> focus away for quite some time). But now we're going in the other
> direction. And further along that other direction is another extreme
> -- just having these be top-level commands, e.g. "git reorder", "git
> split", etc.
Yeah, we should consolidate from my point of view. With the current
status quo I'd say that:
- git-replay(1) becomes the home for all plumbing-level functionality
used by scripts and on the server-side.
- git-history(1) becomes the home for history editing functionality
that is user-facing. Potentially, we could also move (or rather
alias) git-rebase(1) into git-history(1) to complete that vision at
one point in time.
The main reason why I propose to introduce a top-level command with
different subcommands is that it helps users discover related
functionality. If we had "git reorder", "git split" et cetera as
separate subcommands it would be way harder for a user to find out "what
commands do I have to modify history?"
In the worst case, users can still create an alias for git-split(1).
> In a separate conversation we had (and I hope I'm paraphrasing
> correctly; if not please correct me), you mentioned you wanted
> git-history to be the home of history rewriting, and viewed git-replay
> as just a server side thing (whereas I created git-replay specifically
> as a user-focusing thing and then Christian changed it into a
> server-side thing since that part was complete and enough for his
> purposes). But if git history is the home of history editing, how far
> does that go? Do we have a "git history reset"? "git history
> commit"? "git history fast-export/fast-import" "git history
> filter-repo"? Or is it just the home for certain kinds of history
> rewriting operations? If so, which ones?
My take in once sentence: git-history(1) modifies a preexisting sequence
of commits. With that definition we rule out:
- "git history commit" because this creates a new commit on top.
- "git history fast-export" because this doesn't edit the commit
sequence.
- "git history fast-import" because this imports nonexistent commits.
- "git history reset" (which I assume is an alias of git-reset(1))
because this command doesn't only care about commits, but it also
modifies the working tree depending on the mode.
Something like "git history filter-repo" would probably be in the
picture though, as it matches the above definition.
I was also wondering whether "git history" is too broad with that
definition in mind. At one point in time I though about "git histedit"
instead, which may be a bit of a better fit?
> That all said, I'm a big fan of the idea of incorporating more of jj
> capabilities, and you clearly marked the command as experimental
> (thanks!), which leave us room to adjust later if we don't like this
> path. So I don't want to serve as a roadblock, I just think it's a
> useful conversation to have...
Definitely, thanks a lot for your thoughts!
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC v3 00/18] Introduce git-history(1) command for easy history editing
2025-09-15 9:33 ` Patrick Steinhardt
@ 2025-09-16 11:23 ` Oswald Buddenhagen
0 siblings, 0 replies; 278+ messages in thread
From: Oswald Buddenhagen @ 2025-09-16 11:23 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: Elijah Newren, git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk
On Mon, Sep 15, 2025 at 11:33:59AM +0200, Patrick Steinhardt wrote:
>On Sat, Sep 06, 2025 at 11:46:48PM -0700, Elijah Newren wrote:
>> So, this brings up a question. Should we have git-rebase &
>> git-cherry-pick & git-replay & git-history, or should we consolidate?
>> [...]
>
>The main reason why I propose to introduce a top-level command with
>different subcommands is that it helps users discover related
>functionality.
>
i think this is better addressed with proper documentation.
>In the worst case, users can still create an alias for git-split(1).
>
it seems backwards to basically require aliases for efficient use. this
would also lead to a mismatch between what people write into how-tos and
what is actually in use (aliases are individual and therefore
inconsistent, so one needs to use the full form in writing).
>My take in once sentence: git-history(1) modifies a preexisting
>sequence of commits.
>
the details of this definition are arbitrary, and you already noticed
that there are grey areas. we will find more when we start looking for
them.
so, obviously, i'm in favor of atomizing.
>I was also wondering whether "git history" is too broad with that
>definition in mind. At one point in time I though about "git histedit"
>instead, which may be a bit of a better fit?
>
a natural name for this would be "revise", which, not coindicentally at
all, is actually an existing 3rd-party tool with a quite similar scope.
but then, see above.
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH RFC v3 00/18] Introduce git-history(1) command for easy history editing
2025-09-04 14:27 ` [PATCH RFC v3 00/18] " Patrick Steinhardt
` (19 preceding siblings ...)
2025-09-07 6:46 ` Elijah Newren
@ 2025-09-10 20:05 ` Junio C Hamano
2025-09-15 9:32 ` Patrick Steinhardt
20 siblings, 1 reply; 278+ messages in thread
From: Junio C Hamano @ 2025-09-10 20:05 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, D. Ben Knoble, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk
Patrick Steinhardt <ps@pks.im> writes:
> Note: this patch series is growing quite large overall. I'll send one
> last version of the complete series with the RFC tag, but after that
> I'll probably split the series into two and stop after introducing the
> "reorder" command.
I haven't merged this to 'seen', not because I do not like what it
does, but simply because I do not have enough concentration to deal
with conflicts with some in-flight topics (IIRC it textually overlapped
with Peff's add-i color topic).
Thanks.
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH RFC v3 00/18] Introduce git-history(1) command for easy history editing
2025-09-10 20:05 ` Junio C Hamano
@ 2025-09-15 9:32 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-09-15 9:32 UTC (permalink / raw)
To: Junio C Hamano
Cc: git, D. Ben Knoble, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk
On Wed, Sep 10, 2025 at 01:05:57PM -0700, Junio C Hamano wrote:
> Patrick Steinhardt <ps@pks.im> writes:
>
> > Note: this patch series is growing quite large overall. I'll send one
> > last version of the complete series with the RFC tag, but after that
> > I'll probably split the series into two and stop after introducing the
> > "reorder" command.
>
> I haven't merged this to 'seen', not because I do not like what it
> does, but simply because I do not have enough concentration to deal
> with conflicts with some in-flight topics (IIRC it textually overlapped
> with Peff's add-i color topic).
That's fair, it's been in RFC state anyway. I'll trim down the size of
this series, resolve conflicts with 'seen' and then send a new version.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH v4 00/12] Introduce git-history(1) command for easy history editing
2025-08-19 10:55 [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing Patrick Steinhardt
` (16 preceding siblings ...)
2025-09-04 14:27 ` [PATCH RFC v3 00/18] " Patrick Steinhardt
@ 2025-10-01 15:57 ` Patrick Steinhardt
2025-10-01 15:57 ` [PATCH v4 01/12] wt-status: provide function to expose status for trees Patrick Steinhardt
` (13 more replies)
2025-10-21 14:15 ` [PATCH v5 " Patrick Steinhardt
` (2 subsequent siblings)
20 siblings, 14 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-01 15:57 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren
Hi,
over recent months I've been playing around with Jujutsu quite
frequently. While I still prefer using Git, there's been a couple
features in it that I really like and that I'd like to have in Git, as
well.
A copule of these features relate to history editing. Most importantly,
I really dig the following commands:
- jj-abandon(1) to drop a specific commit from your history.
- jj-absorb(1) to take some changes and automatically apply them to
commits in your history that last modified the respective hunks.
- jj-split(1) to split a commit into two.
- jj-new(1) to insert a new commit after or before a specific other
commit.
Not all of these commands can be ported directly into Git. jj-new(1) for
example doesn't really make a ton of sense for us, I'd claim. But some
of these commands _do_ make sense.
This patch series is a starting point for such a command. I've
significantly slimmed it down from the first couple revisions now
following the discussions at the Contributor's Summit yesterday. This
was my intent anyway, as I already mentioned on the last iteration.
Changes in v2:
- Add a new "reword" subcommand.
- List git-history(1) in "command-list.txt".
- Add some missing error handling.
- Simplify calling convention of `apply_commits()` to handle root
commits internally instead of requiring every caller to do so.
- Add tests to verify that git-history(1) refuses to work with changes
in the worktree or index.
- Mark git-history(1) as experimental.
- Introduce commands to manage interrupted history edits.
- A bunch of improvements to the manpage.
- Link to v1: https://lore.kernel.org/r/20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im
Changes in v3:
- Add logic to drive the "post-rewrite" hook and add tests to verify
that all hooks are executed as expected.
- Deduplicate logic to turn a replay action into a todo command.
- Move the addition of tests for the top-level git-history(1) command
to the correct commit.
- Some smaller commit message fixes.
- Honor "commit.verbose".
- Fix copy-paste error with an error message.
- Link to v2: https://lore.kernel.org/r/20250824-b4-pks-history-builtin-v2-0-964ac12f65bd@pks.im
Changes in v4:
- I've rebuilt the patch series. It is now based on 821f583da6 (The
thirteenth batcn, 2025-09-29) with sa/replay-atomic-ref-updates
at 665c66a743 (replay: make atomic ref updates the default behavior,
2025-09-27) merged into it. This should fix all conflicts with seen.
- I've reworked this patch series to use the same infra as
git-replay(1), as discussed during the Contributor's Summit.
- I've slimmed down the patch series to only tackle those commands
that cannot result in a conflict to keep it simple. I also learned
that Elijah has been working on a "git replay edit" command, so I
dropped that command so that we can instead use his version.
- During the Contributor's Summit we have agreed that for now, we
won't care about hook execution just yet. This may be backfilled at
a later point in time.
- I dropped "commit.verbose" handling for now, as my understanding of
it was wrong at first. This is something we should backfill.
- Link to v3: https://lore.kernel.org/r/20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im
Thanks!
Patrick
---
Patrick Steinhardt (12):
wt-status: provide function to expose status for trees
replay: extract logic to pick commits
replay: stop using `the_repository`
replay: parse commits before dereferencing them
builtin: add new "history" command
builtin/history: implement "reword" subcommand
add-patch: split out header from "add-interactive.h"
add-patch: split out `struct interactive_options`
add-patch: remove dependency on "add-interactive" subsystem
add-patch: add support for in-memory index patching
cache-tree: allow writing in-memory index as tree
builtin/history: implement "split" subcommand
.gitignore | 1 +
Documentation/git-history.adoc | 114 ++++++++
Documentation/meson.build | 1 +
Makefile | 2 +
add-interactive.c | 174 +++---------
add-interactive.h | 46 +--
add-patch.c | 295 +++++++++++++++++---
add-patch.h | 64 +++++
builtin.h | 1 +
builtin/add.c | 22 +-
builtin/checkout.c | 7 +-
builtin/commit.c | 16 +-
builtin/history.c | 614 +++++++++++++++++++++++++++++++++++++++++
builtin/replay.c | 110 +-------
builtin/reset.c | 16 +-
builtin/stash.c | 46 +--
cache-tree.c | 5 +-
cache-tree.h | 3 +-
command-list.txt | 1 +
commit.h | 2 +-
git.c | 1 +
meson.build | 2 +
replay.c | 118 ++++++++
replay.h | 18 ++
t/meson.build | 3 +
t/t3450-history.sh | 17 ++
t/t3451-history-reword.sh | 202 ++++++++++++++
t/t3452-history-split.sh | 432 +++++++++++++++++++++++++++++
wt-status.c | 24 ++
wt-status.h | 3 +
30 files changed, 1984 insertions(+), 376 deletions(-)
Range-diff versus v3:
1: 166ba26bea < -: ---------- sequencer: optionally skip printing commit summary
2: 60173823a9 < -: ---------- sequencer: add option to rewind HEAD after picking commits
3: 74d21ea445 < -: ---------- sequencer: introduce new history editing mode
4: b1c052afc7 < -: ---------- sequencer: stop using `the_repository` in `sequencer_remove_state()`
5: 0884c4fd96 < -: ---------- sequencer: wire up "rewritten-hook" for REPLAY_HISTORY_EDIT
15: b77e378615 = 1: 695fc789d5 wt-status: provide function to expose status for trees
-: ---------- > 2: 55707c8cb3 replay: extract logic to pick commits
-: ---------- > 3: 1308a0bbbb replay: stop using `the_repository`
-: ---------- > 4: d153d366b5 replay: parse commits before dereferencing them
7: 243d36cb45 ! 5: 820a2f4c17 builtin: add new "history" command
@@ builtin/history.c (new)
+ };
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
++ if (argc)
++ usagef("unrecognized argument: %s", argv[0]);
+ return 0;
+}
@@ t/meson.build: integration_tests = [
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
-@@ t/meson.build: if perl.found() and time.found()
- timeout: 0,
- )
- endforeach
--endif
- \ No newline at end of file
-+endif
## t/t3450-history.sh (new) ##
@@
@@ t/t3450-history.sh (new)
+
+. ./test-lib.sh
+
-+test_expect_success 'refuses to do anything without subcommand' '
-+ test_must_fail git history 2>err &&
-+ test_grep foo err
++test_expect_success 'does nothing without any arguments' '
++ git history >out 2>&1 &&
++ test_must_be_empty out
++'
++
++test_expect_success 'raises an error with unknown argument' '
++ test_must_fail git history garbage 2>err &&
++ test_grep "unrecognized argument: garbage" err
+'
+
+test_done
18: 0a1ff48827 ! 6: 43af6dc4dc builtin/history: implement "reword" subcommand
@@ Commit message
Signed-off-by: Patrick Steinhardt <ps@pks.im>
## Documentation/git-history.adoc ##
-@@ Documentation/git-history.adoc: git history continue
- git history quit
- git history drop <commit>
- git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)
+@@ Documentation/git-history.adoc: SYNOPSIS
+ --------
+ [synopsis]
+ git history [<options>]
+git history reword [<options>] <commit>
- git history split [<options>] <commit> [--] [<pathspec>...]
DESCRIPTION
-@@ Documentation/git-history.adoc: child commits, as that would lead to an empty branch.
- be related to one another and must be reachable from the current `HEAD`
- commit.
+ -----------
+@@ Documentation/git-history.adoc: COMMANDS
+ This command requires a subcommand. Several subcommands are available to
+ rewrite history in different ways:
+`reword <commit> [--message=<message>]`::
+ Rewrite the commit message of the specified commit. All the other
-+ details of this commit remain unchanged.
++ details of this commit remain unchanged. If no commit message is
++ provided, then this command will spawn an editor with the current
++ message of that commit.
+
- `split [--message=<message>] <commit> [--] [<pathspec>...]`::
- Interactively split up <commit> into two commits by choosing
- hunks introduced by it that will be moved into the new split-out
+ CONFIGURATION
+ -------------
+
## builtin/history.c ##
-@@ builtin/history.c: static int split_commit(struct repository *repo,
- return ret;
- }
-
+@@
++#define USE_THE_REPOSITORY_VARIABLE
++
+ #include "builtin.h"
++#include "commit-reach.h"
++#include "commit.h"
++#include "config.h"
++#include "editor.h"
++#include "environment.h"
+ #include "gettext.h"
++#include "hex.h"
++#include "oidmap.h"
+ #include "parse-options.h"
++#include "refs.h"
++#include "replay.h"
++#include "reset.h"
++#include "revision.h"
++#include "sequencer.h"
++#include "strvec.h"
++#include "tree.h"
++#include "wt-status.h"
++
++static int collect_commits(struct repository *repo,
++ struct commit *old_commit,
++ struct commit *new_commit,
++ struct strvec *out)
++{
++ struct setup_revision_opt revision_opts = {
++ .assume_dashdash = 1,
++ };
++ struct strvec revisions = STRVEC_INIT;
++ struct commit_list *from_list = NULL;
++ struct commit *child;
++ struct rev_info rev = { 0 };
++ int ret;
++
++ /*
++ * Check that the old commit actually is an ancestor of HEAD. If not
++ * the whole request becomes nonsensical.
++ */
++ if (old_commit) {
++ commit_list_insert(old_commit, &from_list);
++ if (!repo_is_descendant_of(repo, new_commit, from_list)) {
++ ret = error(_("commit must be reachable from current HEAD commit"));
++ goto out;
++ }
++ }
++
++ repo_init_revisions(repo, &rev, NULL);
++ strvec_push(&revisions, "");
++ strvec_push(&revisions, oid_to_hex(&new_commit->object.oid));
++ if (old_commit)
++ strvec_pushf(&revisions, "^%s", oid_to_hex(&old_commit->object.oid));
++
++ setup_revisions_from_strvec(&revisions, &rev, &revision_opts);
++ if (revisions.nr != 1 || prepare_revision_walk(&rev)) {
++ ret = error(_("revision walk setup failed"));
++ goto out;
++ }
++
++ while ((child = get_revision(&rev))) {
++ if (old_commit && !child->parents)
++ BUG("revision walk did not find child commit");
++ if (child->parents && child->parents->next) {
++ ret = error(_("cannot rearrange commit history with merges"));
++ goto out;
++ }
++
++ strvec_push(out, oid_to_hex(&child->object.oid));
++
++ if (child->parents && old_commit &&
++ commit_list_contains(old_commit, child->parents))
++ break;
++ }
++
++ /*
++ * Revisions are in newest-order-first. We have to reverse the
++ * array though so that we pick the oldest commits first.
++ */
++ for (size_t i = 0, j = out->nr - 1; i < j; i++, j--)
++ SWAP(out->v[i], out->v[j]);
++
++ ret = 0;
++
++out:
++ free_commit_list(from_list);
++ strvec_clear(&revisions);
++ release_revisions(&rev);
++ reset_revision_walk();
++ return ret;
++}
++
++static void replace_commits(struct strvec *commits,
++ const struct object_id *commit_to_replace,
++ const struct object_id *replacements,
++ size_t replacements_nr)
++{
++ char commit_to_replace_oid[GIT_MAX_HEXSZ + 1];
++ struct strvec replacement_oids = STRVEC_INIT;
++ bool found = false;
++
++ oid_to_hex_r(commit_to_replace_oid, commit_to_replace);
++ for (size_t i = 0; i < replacements_nr; i++)
++ strvec_push(&replacement_oids, oid_to_hex(&replacements[i]));
++
++ for (size_t i = 0; i < commits->nr; i++) {
++ if (strcmp(commits->v[i], commit_to_replace_oid))
++ continue;
++ strvec_splice(commits, i, 1, replacement_oids.v, replacement_oids.nr);
++ found = true;
++ break;
++ }
++ if (!found)
++ BUG("could not find commit to replace");
++
++ strvec_clear(&replacement_oids);
++}
++
++static int apply_commits(struct repository *repo,
++ const struct strvec *commits,
++ struct commit *onto,
++ struct commit *orig_head,
++ const char *action)
++{
++ struct reset_head_opts reset_opts = { 0 };
++ struct merge_options merge_opts = { 0 };
++ struct merge_result result = { 0 };
++ struct strbuf buf = STRBUF_INIT;
++ kh_oid_map_t *replayed_commits;
++ int ret;
++
++ replayed_commits = kh_init_oid_map();
++
++ init_basic_merge_options(&merge_opts, repo);
++ merge_opts.show_rename_progress = 0;
++
++ for (size_t i = 0; i < commits->nr; i++) {
++ struct object_id commit_id;
++ struct commit *commit;
++ const char *end;
++ int hash_result;
++ khint_t pos;
++
++ if (parse_oid_hex_algop(commits->v[i], &commit_id, &end,
++ repo->hash_algo)) {
++ ret = error(_("invalid object ID: %s"), commits->v[i]);
++ goto out;
++ }
++
++ commit = lookup_commit(repo, &commit_id);
++ if (!commit || repo_parse_commit(repo, commit)) {
++ ret = error(_("failed to look up commit: %s"), oid_to_hex(&commit_id));
++ goto out;
++ }
++
++ if (!onto) {
++ onto = commit;
++ result.clean = 1;
++ result.tree = repo_get_commit_tree(repo, commit);
++ } else {
++ onto = replay_pick_regular_commit(repo, commit, replayed_commits,
++ onto, &merge_opts, &result);
++ if (!onto)
++ break;
++ }
++
++ pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hash_result);
++ if (hash_result == 0) {
++ ret = error(_("duplicate rewritten commit: %s\n"),
++ oid_to_hex(&commit->object.oid));
++ goto out;
++ }
++ kh_value(replayed_commits, pos) = onto;
++ }
++
++ if (!result.clean) {
++ ret = error(_("could not merge"));
++ goto out;
++ }
++
++ reset_opts.oid = &onto->object.oid;
++ strbuf_addf(&buf, "%s: switch to rewritten %s", action, oid_to_hex(reset_opts.oid));
++ reset_opts.flags = RESET_HEAD_REFS_ONLY | RESET_ORIG_HEAD;
++ reset_opts.orig_head = &orig_head->object.oid;
++ reset_opts.default_reflog_action = action;
++ if (reset_head(repo, &reset_opts) < 0) {
++ ret = error(_("could not switch to %s"), oid_to_hex(reset_opts.oid));
++ goto out;
++ }
++
++ ret = 0;
++
++out:
++ kh_destroy_oid_map(replayed_commits);
++ merge_finalize(&merge_opts, &result);
++ strbuf_release(&buf);
++ return ret;
++}
++
++static void change_data_free(void *util, const char *str UNUSED)
++{
++ struct wt_status_change_data *d = util;
++ free(d->rename_source);
++ free(d);
++}
++
++static int fill_commit_message(struct repository *repo,
++ const struct object_id *old_tree,
++ const struct object_id *new_tree,
++ const char *default_message,
++ const char *provided_message,
++ const char *action,
++ struct strbuf *out)
++{
++ if (!provided_message) {
++ const char *path = git_path_commit_editmsg();
++ const char *hint =
++ _("Please enter the commit message for the %s changes. Lines starting\n"
++ "with '%s' will be kept; you may remove them yourself if you want to.\n");
++ struct wt_status s;
++
++ strbuf_addstr(out, default_message);
++ strbuf_addch(out, '\n');
++ strbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);
++ write_file_buf(path, out->buf, out->len);
++
++ wt_status_prepare(repo, &s);
++ FREE_AND_NULL(s.branch);
++ s.ahead_behind_flags = AHEAD_BEHIND_QUICK;
++ s.commit_template = 1;
++ s.colopts = 0;
++ s.display_comment_prefix = 1;
++ s.hints = 0;
++ s.use_color = 0;
++ s.whence = FROM_COMMIT;
++ s.committable = 1;
++
++ s.fp = fopen(git_path_commit_editmsg(), "a");
++ if (!s.fp)
++ return error_errno(_("could not open '%s'"), git_path_commit_editmsg());
++
++ wt_status_collect_changes_trees(&s, old_tree, new_tree);
++ wt_status_print(&s);
++ wt_status_collect_free_buffers(&s);
++ string_list_clear_func(&s.change, change_data_free);
++
++ strbuf_reset(out);
++ if (launch_editor(path, out, NULL)) {
++ fprintf(stderr, _("Please supply the message using the -m option.\n"));
++ return -1;
++ }
++ strbuf_stripspace(out, comment_line_str);
++ } else {
++ strbuf_addstr(out, provided_message);
++ }
++
++ cleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);
++
++ if (!out->len) {
++ fprintf(stderr, _("Aborting commit due to empty commit message.\n"));
++ return -1;
++ }
++
++ return 0;
++}
++
+static int cmd_history_reword(int argc,
+ const char **argv,
+ const char *prefix,
@@ builtin/history.c: static int split_commit(struct repository *repo,
+ OPT_END(),
+ };
+ struct strbuf final_message = STRBUF_INIT;
-+ struct commit *original_commit, *head;
++ struct commit *original_commit, *parent, *head;
+ struct strvec commits = STRVEC_INIT;
+ struct object_id parent_tree_oid, original_commit_tree_oid;
+ struct object_id rewritten_commit;
+ const char *original_message, *original_body, *ptr;
-+ struct oidmap rewritten_commits = OIDMAP_INIT;
-+ struct replay_oid_mapping mapping = { 0 };
+ char *original_author = NULL;
+ size_t len;
+ int ret;
@@ builtin/history.c: static int split_commit(struct repository *repo,
+ ret = error(_("commit to be reworded cannot be found: %s"), argv[0]);
+ goto out;
+ }
++ if (repo_parse_commit(repo, original_commit)) {
++ ret = error(_("unable to parse commit %s"),
++ oid_to_hex(&original_commit->object.oid));
++ goto out;
++ }
++ original_commit_tree_oid = repo_get_commit_tree(repo, original_commit)->object.oid;
++
++ parent = original_commit->parents ? original_commit->parents->item : NULL;
++ if (parent) {
++ if (repo_parse_commit(repo, parent)) {
++ ret = error(_("unable to parse commit %s"),
++ oid_to_hex(&parent->object.oid));
++ goto out;
++ }
++ parent_tree_oid = repo_get_commit_tree(repo, parent)->object.oid;
++ } else {
++ oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
++ }
+
+ head = lookup_commit_reference_by_name("HEAD");
+ if (!head) {
@@ builtin/history.c: static int split_commit(struct repository *repo,
+ * This ensures that we'll abort early on in case the range of commits
+ * contains merges, which we do not yet handle.
+ */
-+ ret = collect_commits(repo, original_commit->parents ? original_commit->parents->item : NULL,
-+ head, &commits);
++ ret = collect_commits(repo, parent, head, &commits);
+ if (ret < 0)
+ goto out;
+
@@ builtin/history.c: static int split_commit(struct repository *repo,
+ original_author = xmemdupz(ptr, len);
+ find_commit_subject(original_message, &original_body);
+
-+ if (original_commit->parents)
-+ parent_tree_oid = *get_commit_tree_oid(original_commit->parents->item);
-+ else
-+ oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
-+ original_commit_tree_oid = *get_commit_tree_oid(original_commit);
-+
+ ret = fill_commit_message(repo, &parent_tree_oid, &original_commit_tree_oid,
+ original_body, commit_message, "reworded", &final_message);
+ if (ret < 0)
@@ builtin/history.c: static int split_commit(struct repository *repo,
+
+ replace_commits(&commits, &original_commit->object.oid, &rewritten_commit, 1);
+
-+ mapping.entry.oid = rewritten_commit;
-+ mapping.rewritten_oid = original_commit->object.oid;
-+ oidmap_put(&rewritten_commits, &mapping);
-+
-+ ret = apply_commits(repo, &commits, head, original_commit,
-+ &rewritten_commits, "reword");
++ ret = apply_commits(repo, &commits, parent, head, "reword");
+ if (ret < 0)
+ goto out;
+
+ ret = 0;
+
+out:
-+ oidmap_clear(&rewritten_commits, 0);
+ strbuf_release(&final_message);
+ strvec_clear(&commits);
+ free(original_author);
+ return ret;
+}
-+
- static int cmd_history_split(int argc,
- const char **argv,
- const char *prefix,
-@@ builtin/history.c: int cmd_history(int argc,
- N_("git history quit"),
- N_("git history drop <commit>"),
- N_("git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)"),
+
+ int cmd_history(int argc,
+ const char **argv,
+ const char *prefix,
+- struct repository *repo UNUSED)
++ struct repository *repo)
+ {
+ const char * const usage[] = {
+ N_("git history [<options>]"),
+ N_("git history reword [<options>] <commit>"),
- N_("git history split [<options>] <commit> [--] [<pathspec>...]"),
NULL,
};
-@@ builtin/history.c: int cmd_history(int argc,
- OPT_SUBCOMMAND("quit", &fn, cmd_history_quit),
- OPT_SUBCOMMAND("drop", &fn, cmd_history_drop),
- OPT_SUBCOMMAND("reorder", &fn, cmd_history_reorder),
++ parse_opt_subcommand_fn *fn = NULL;
+ struct option options[] = {
+ OPT_SUBCOMMAND("reword", &fn, cmd_history_reword),
- OPT_SUBCOMMAND("split", &fn, cmd_history_split),
OPT_END(),
};
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+- if (argc)
+- usagef("unrecognized argument: %s", argv[0]);
+- return 0;
++ return fn(argc, argv, prefix, repo);
+ }
## t/meson.build ##
@@ t/meson.build: integration_tests = [
- 't3451-history-drop.sh',
- 't3452-history-reorder.sh',
- 't3453-history-split.sh',
-+ 't3454-history-reword.sh',
+ 't3437-rebase-fixup-options.sh',
+ 't3438-rebase-broken-files.sh',
+ 't3450-history.sh',
++ 't3451-history-reword.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
- ## t/t3454-history-reword.sh (new) ##
+ ## t/t3450-history.sh ##
+@@ t/t3450-history.sh: test_description='tests for git-history command'
+ . ./test-lib.sh
+
+ test_expect_success 'does nothing without any arguments' '
+- git history >out 2>&1 &&
+- test_must_be_empty out
++ test_must_fail git history 2>err &&
++ test_grep "need a subcommand" err
+ '
+
+ test_expect_success 'raises an error with unknown argument' '
+ test_must_fail git history garbage 2>err &&
+- test_grep "unrecognized argument: garbage" err
++ test_grep "unknown subcommand: .garbage." err
+ '
+
+ test_done
+
+ ## t/t3451-history-reword.sh (new) ##
@@
+#!/bin/sh
+
@@ t/t3454-history-reword.sh (new)
+ )
+'
+
-+test_expect_success 'refuses to work with changes in the worktree or index' '
-+ test_when_finished "rm -rf repo" &&
-+ git init repo &&
-+ (
-+ cd repo &&
-+ test_commit base file &&
-+ echo foo >file &&
-+ test_must_fail git history reword HEAD 2>err &&
-+ test_grep "Your local changes to the following files would be overwritten" err &&
-+ git add file &&
-+ test_must_fail git history reword HEAD 2>err &&
-+ test_grep "Your local changes to the following files would be overwritten" err
-+ )
-+'
-+
+test_expect_success 'can reword tip of a branch' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
@@ t/t3454-history-reword.sh (new)
+ )
+'
+
++# For now, git-history(1) does not yet execute any hooks. This is subject to
++# change in the future, and if it does this test here is expected to start
++# failing. In other words, this test is not an endorsement of the current
++# status quo.
+test_expect_success 'hooks are executed for rewritten commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
@@ t/t3454-history-reword.sh (new)
+ test_commit third &&
+
+ write_script .git/hooks/prepare-commit-msg <<-EOF &&
-+ echo "prepare-commit-msg: \$@" >>"$(pwd)/hooks.log"
++ touch "$(pwd)/hooks.log
+ EOF
+ write_script .git/hooks/post-commit <<-EOF &&
-+ echo "post-commit" >>"$(pwd)/hooks.log"
++ touch "$(pwd)/hooks.log
+ EOF
+ write_script .git/hooks/post-rewrite <<-EOF &&
-+ {
-+ echo "post-rewrite: \$@"
-+ cat
-+ } >>"$(pwd)/hooks.log"
++ touch "$(pwd)/hooks.log
+ EOF
+
+ git history reword -m "second reworded" HEAD~ &&
@@ t/t3454-history-reword.sh (new)
+ git log --format=%s >actual &&
+ test_cmp expect actual &&
+
-+ cat >expect <<-EOF &&
-+ prepare-commit-msg: .git/COMMIT_EDITMSG message
-+ post-commit
-+ prepare-commit-msg: .git/COMMIT_EDITMSG message
-+ post-commit
-+ post-rewrite: history
-+ $(git rev-parse second) $(git rev-parse HEAD~)
-+ $(git rev-parse third) $(git rev-parse HEAD)
-+ EOF
-+ test_cmp expect hooks.log
++ test_path_is_missing hooks.log
+ )
+'
+
@@ t/t3454-history-reword.sh (new)
+ )
+'
+
++test_expect_success 'retains changes in the worktree and index' '
++ test_when_finished "rm -rf repo" &&
++ git init repo &&
++ (
++ cd repo &&
++ touch a b &&
++ git add . &&
++ git commit -m "initial commit" &&
++ echo foo >a &&
++ echo bar >b &&
++ git add b &&
++ git history reword HEAD -m message &&
++ cat >expect <<-\EOF &&
++ M a
++ M b
++ ?? actual
++ ?? expect
++ EOF
++ git status --porcelain >actual &&
++ test_cmp expect actual
++ )
++'
++
+test_done
11: 01cab3d8a5 = 7: 62e11acdfe add-patch: split out header from "add-interactive.h"
12: 6726e2ee02 ! 8: e0b65245fb add-patch: split out `struct interactive_options`
@@ add-interactive.c
#include "prompt.h"
#include "tree.h"
--static void init_color(struct repository *r, struct add_i_state *s,
+-static void init_color(struct repository *r, enum git_colorbool use_color,
- const char *section_and_slot, char *dst,
- const char *default_color)
-{
- char *key = xstrfmt("color.%s", section_and_slot);
- const char *value;
-
-- if (!s->use_color)
+- if (!want_color(use_color))
- dst[0] = '\0';
- else if (repo_config_get_value(r, key, &value) ||
- color_parse(value, dst))
@@ add-interactive.c
-
- free(key);
-}
+-
+-static enum git_colorbool check_color_config(struct repository *r, const char *var)
+-{
+- const char *value;
+- enum git_colorbool ret;
+-
+- if (repo_config_get_value(r, var, &value))
+- ret = GIT_COLOR_UNKNOWN;
+- else
+- ret = git_config_colorbool(var, value);
+-
+- /*
+- * Do not rely on want_color() to fall back to color.ui for us. It uses
+- * the value parsed by git_color_config(), which may not have been
+- * called by the main command.
+- */
+- if (ret == GIT_COLOR_UNKNOWN &&
+- !repo_config_get_value(r, "color.ui", &value))
+- ret = git_config_colorbool("color.ui", value);
+-
+- return ret;
+-}
-
void init_add_i_state(struct add_i_state *s, struct repository *r,
- struct add_p_opt *add_p_opt)
+ struct interactive_options *opts)
{
-- const char *value;
--
s->r = r;
- s->context = -1;
- s->interhunkcontext = -1;
-
-- if (repo_config_get_value(r, "color.interactive", &value))
-- s->use_color = -1;
-- else
-- s->use_color =
-- git_config_colorbool("color.interactive", value);
-- s->use_color = want_color(s->use_color);
+- s->use_color_interactive = check_color_config(r, "color.interactive");
-
-- init_color(r, s, "interactive.header", s->header_color, GIT_COLOR_BOLD);
-- init_color(r, s, "interactive.help", s->help_color, GIT_COLOR_BOLD_RED);
-- init_color(r, s, "interactive.prompt", s->prompt_color,
-- GIT_COLOR_BOLD_BLUE);
-- init_color(r, s, "interactive.error", s->error_color,
-- GIT_COLOR_BOLD_RED);
+- init_color(r, s->use_color_interactive, "interactive.header",
+- s->header_color, GIT_COLOR_BOLD);
+- init_color(r, s->use_color_interactive, "interactive.help",
+- s->help_color, GIT_COLOR_BOLD_RED);
+- init_color(r, s->use_color_interactive, "interactive.prompt",
+- s->prompt_color, GIT_COLOR_BOLD_BLUE);
+- init_color(r, s->use_color_interactive, "interactive.error",
+- s->error_color, GIT_COLOR_BOLD_RED);
+- strlcpy(s->reset_color_interactive,
+- want_color(s->use_color_interactive) ? GIT_COLOR_RESET : "", COLOR_MAXLEN);
-
-- init_color(r, s, "diff.frag", s->fraginfo_color,
-- diff_get_color(s->use_color, DIFF_FRAGINFO));
-- init_color(r, s, "diff.context", s->context_color, "fall back");
-- if (!strcmp(s->context_color, "fall back"))
-- init_color(r, s, "diff.plain", s->context_color,
-- diff_get_color(s->use_color, DIFF_CONTEXT));
-- init_color(r, s, "diff.old", s->file_old_color,
-- diff_get_color(s->use_color, DIFF_FILE_OLD));
-- init_color(r, s, "diff.new", s->file_new_color,
-- diff_get_color(s->use_color, DIFF_FILE_NEW));
+- s->use_color_diff = check_color_config(r, "color.diff");
-
-- strlcpy(s->reset_color,
-- s->use_color ? GIT_COLOR_RESET : "", COLOR_MAXLEN);
+- init_color(r, s->use_color_diff, "diff.frag", s->fraginfo_color,
+- diff_get_color(s->use_color_diff, DIFF_FRAGINFO));
+- init_color(r, s->use_color_diff, "diff.context", s->context_color,
+- "fall back");
+- if (!strcmp(s->context_color, "fall back"))
+- init_color(r, s->use_color_diff, "diff.plain",
+- s->context_color,
+- diff_get_color(s->use_color_diff, DIFF_CONTEXT));
+- init_color(r, s->use_color_diff, "diff.old", s->file_old_color,
+- diff_get_color(s->use_color_diff, DIFF_FILE_OLD));
+- init_color(r, s->use_color_diff, "diff.new", s->file_new_color,
+- diff_get_color(s->use_color_diff, DIFF_FILE_NEW));
+- strlcpy(s->reset_color_diff,
+- want_color(s->use_color_diff) ? GIT_COLOR_RESET : "", COLOR_MAXLEN);
-
- FREE_AND_NULL(s->interactive_diff_filter);
- repo_config_get_string(r, "interactive.difffilter",
@@ add-interactive.c
- FREE_AND_NULL(s->interactive_diff_algorithm);
+ interactive_config_clear(&s->cfg);
memset(s, 0, sizeof(*s));
-- s->use_color = -1;
+- s->use_color_interactive = GIT_COLOR_UNKNOWN;
+- s->use_color_diff = GIT_COLOR_UNKNOWN;
+ interactive_config_clear(&s->cfg);
}
@@ add-interactive.c: int run_add_i(struct repository *r, const struct pathspec *ps
* When color was asked for, use the prompt color for
* highlighting, otherwise use square brackets.
*/
-- if (s.use_color) {
+- if (want_color(s.use_color_interactive)) {
- data.color = s.prompt_color;
-- data.reset = s.reset_color;
-+ if (s.cfg.use_color) {
+- data.reset = s.reset_color_interactive;
++ if (want_color(s.cfg.use_color_interactive)) {
+ data.color = s.cfg.prompt_color;
-+ data.reset = s.cfg.reset_color;
++ data.reset = s.cfg.reset_color_interactive;
}
print_file_item_data.color = data.color;
print_file_item_data.reset = data.reset;
@@ add-interactive.h
struct add_i_state {
struct repository *r;
-- int use_color;
+- enum git_colorbool use_color_interactive;
+- enum git_colorbool use_color_diff;
- char header_color[COLOR_MAXLEN];
- char help_color[COLOR_MAXLEN];
- char prompt_color[COLOR_MAXLEN];
- char error_color[COLOR_MAXLEN];
-- char reset_color[COLOR_MAXLEN];
+- char reset_color_interactive[COLOR_MAXLEN];
+-
- char fraginfo_color[COLOR_MAXLEN];
- char context_color[COLOR_MAXLEN];
- char file_old_color[COLOR_MAXLEN];
- char file_new_color[COLOR_MAXLEN];
+- char reset_color_diff[COLOR_MAXLEN];
-
- int use_single_key;
- char *interactive_diff_filter, *interactive_diff_algorithm;
@@ add-patch.c: struct add_p_state {
};
+static void init_color(struct repository *r,
-+ struct interactive_config *cfg,
++ enum git_colorbool use_color,
+ const char *section_and_slot, char *dst,
+ const char *default_color)
+{
+ char *key = xstrfmt("color.%s", section_and_slot);
+ const char *value;
+
-+ if (!cfg->use_color)
++ if (!want_color(use_color))
+ dst[0] = '\0';
+ else if (repo_config_get_value(r, key, &value) ||
+ color_parse(value, dst))
@@ add-patch.c: struct add_p_state {
+ free(key);
+}
+
++static enum git_colorbool check_color_config(struct repository *r, const char *var)
++{
++ const char *value;
++ enum git_colorbool ret;
++
++ if (repo_config_get_value(r, var, &value))
++ ret = GIT_COLOR_UNKNOWN;
++ else
++ ret = git_config_colorbool(var, value);
++
++ /*
++ * Do not rely on want_color() to fall back to color.ui for us. It uses
++ * the value parsed by git_color_config(), which may not have been
++ * called by the main command.
++ */
++ if (ret == GIT_COLOR_UNKNOWN &&
++ !repo_config_get_value(r, "color.ui", &value))
++ ret = git_config_colorbool("color.ui", value);
++
++ return ret;
++}
++
+void interactive_config_init(struct interactive_config *cfg,
+ struct repository *r,
+ struct interactive_options *opts)
+{
-+ const char *value;
-+
+ cfg->context = -1;
+ cfg->interhunkcontext = -1;
+
-+ if (repo_config_get_value(r, "color.interactive", &value))
-+ cfg->use_color = -1;
-+ else
-+ cfg->use_color =
-+ git_config_colorbool("color.interactive", value);
-+ cfg->use_color = want_color(cfg->use_color);
++ cfg->use_color_interactive = check_color_config(r, "color.interactive");
+
-+ init_color(r, cfg, "interactive.header", cfg->header_color, GIT_COLOR_BOLD);
-+ init_color(r, cfg, "interactive.help", cfg->help_color, GIT_COLOR_BOLD_RED);
-+ init_color(r, cfg, "interactive.prompt", cfg->prompt_color,
-+ GIT_COLOR_BOLD_BLUE);
-+ init_color(r, cfg, "interactive.error", cfg->error_color,
-+ GIT_COLOR_BOLD_RED);
++ init_color(r, cfg->use_color_interactive, "interactive.header",
++ cfg->header_color, GIT_COLOR_BOLD);
++ init_color(r, cfg->use_color_interactive, "interactive.help",
++ cfg->help_color, GIT_COLOR_BOLD_RED);
++ init_color(r, cfg->use_color_interactive, "interactive.prompt",
++ cfg->prompt_color, GIT_COLOR_BOLD_BLUE);
++ init_color(r, cfg->use_color_interactive, "interactive.error",
++ cfg->error_color, GIT_COLOR_BOLD_RED);
++ strlcpy(cfg->reset_color_interactive,
++ want_color(cfg->use_color_interactive) ? GIT_COLOR_RESET : "", COLOR_MAXLEN);
+
-+ init_color(r, cfg, "diff.frag", cfg->fraginfo_color,
-+ diff_get_color(cfg->use_color, DIFF_FRAGINFO));
-+ init_color(r, cfg, "diff.context", cfg->context_color, "fall back");
-+ if (!strcmp(cfg->context_color, "fall back"))
-+ init_color(r, cfg, "diff.plain", cfg->context_color,
-+ diff_get_color(cfg->use_color, DIFF_CONTEXT));
-+ init_color(r, cfg, "diff.old", cfg->file_old_color,
-+ diff_get_color(cfg->use_color, DIFF_FILE_OLD));
-+ init_color(r, cfg, "diff.new", cfg->file_new_color,
-+ diff_get_color(cfg->use_color, DIFF_FILE_NEW));
++ cfg->use_color_diff = check_color_config(r, "color.diff");
+
-+ strlcpy(cfg->reset_color,
-+ cfg->use_color ? GIT_COLOR_RESET : "", COLOR_MAXLEN);
++ init_color(r, cfg->use_color_diff, "diff.frag", cfg->fraginfo_color,
++ diff_get_color(cfg->use_color_diff, DIFF_FRAGINFO));
++ init_color(r, cfg->use_color_diff, "diff.context", cfg->context_color,
++ "fall back");
++ if (!strcmp(cfg->context_color, "fall back"))
++ init_color(r, cfg->use_color_diff, "diff.plain",
++ cfg->context_color,
++ diff_get_color(cfg->use_color_diff, DIFF_CONTEXT));
++ init_color(r, cfg->use_color_diff, "diff.old", cfg->file_old_color,
++ diff_get_color(cfg->use_color_diff, DIFF_FILE_OLD));
++ init_color(r, cfg->use_color_diff, "diff.new", cfg->file_new_color,
++ diff_get_color(cfg->use_color_diff, DIFF_FILE_NEW));
++ strlcpy(cfg->reset_color_diff,
++ want_color(cfg->use_color_diff) ? GIT_COLOR_RESET : "", COLOR_MAXLEN);
+
+ FREE_AND_NULL(cfg->interactive_diff_filter);
+ repo_config_get_string(r, "interactive.difffilter",
@@ add-patch.c: struct add_p_state {
+ FREE_AND_NULL(cfg->interactive_diff_filter);
+ FREE_AND_NULL(cfg->interactive_diff_algorithm);
+ memset(cfg, 0, sizeof(*cfg));
-+ cfg->use_color = -1;
++ cfg->use_color_interactive = GIT_COLOR_UNKNOWN;
++ cfg->use_color_diff = GIT_COLOR_UNKNOWN;
+}
+
static void add_p_state_clear(struct add_p_state *s)
@@ add-patch.c: static void err(struct add_p_state *s, const char *fmt, ...)
- fputs(s->s.error_color, stdout);
+ fputs(s->s.cfg.error_color, stdout);
vprintf(fmt, args);
-- puts(s->s.reset_color);
-+ puts(s->s.cfg.reset_color);
+- puts(s->s.reset_color_interactive);
++ puts(s->s.cfg.reset_color_interactive);
va_end(args);
}
@@ add-patch.c: static int parse_diff(struct add_p_state *s, const struct pathspec
struct object_id oid;
strvec_push(&args,
@@ add-patch.c: static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
+ }
+ strbuf_complete_line(plain);
- if (want_color_fd(1, -1)) {
+- if (want_color_fd(1, s->s.use_color_diff)) {
++ if (want_color_fd(1, s->s.cfg.use_color_diff)) {
struct child_process colored_cp = CHILD_PROCESS_INIT;
- const char *diff_filter = s->s.interactive_diff_filter;
+ const char *diff_filter = s->s.cfg.interactive_diff_filter;
@@ add-patch.c: static void render_hunk(struct add_p_state *s, struct hunk *hunk,
if (len)
strbuf_add(out, p, len);
else if (colored)
-- strbuf_addf(out, "%s\n", s->s.reset_color);
-+ strbuf_addf(out, "%s\n", s->s.cfg.reset_color);
+- strbuf_addf(out, "%s\n", s->s.reset_color_diff);
++ strbuf_addf(out, "%s\n", s->s.cfg.reset_color_diff);
else
strbuf_addch(out, '\n');
}
@@ add-patch.c: static void recolor_hunk(struct add_p_state *s, struct hunk *hunk)
+ s->s.cfg.file_new_color :
+ s->s.cfg.context_color);
strbuf_add(&s->colored, plain + current, eol - current);
-- strbuf_addstr(&s->colored, s->s.reset_color);
-+ strbuf_addstr(&s->colored, s->s.cfg.reset_color);
+- strbuf_addstr(&s->colored, s->s.reset_color_diff);
++ strbuf_addstr(&s->colored, s->s.cfg.reset_color_diff);
if (next > eol)
strbuf_add(&s->colored, plain + eol, next - eol);
current = next;
@@ add-patch.c: static int patch_update_file(struct add_p_state *s,
: 1));
printf(_(s->mode->prompt_mode[prompt_mode_type]),
s->buf.buf);
-- if (*s->s.reset_color)
-- fputs(s->s.reset_color, stdout);
-+ if (*s->s.cfg.reset_color)
-+ fputs(s->s.cfg.reset_color, stdout);
+- if (*s->s.reset_color_interactive)
+- fputs(s->s.reset_color_interactive, stdout);
++ if (*s->s.cfg.reset_color_interactive)
++ fputs(s->s.cfg.reset_color_interactive, stdout);
fflush(stdout);
if (read_single_character(s) == EOF)
break;
@@ add-patch.h
+}
+
+struct interactive_config {
-+ int use_color;
++ enum git_colorbool use_color_interactive;
++ enum git_colorbool use_color_diff;
+ char header_color[COLOR_MAXLEN];
+ char help_color[COLOR_MAXLEN];
+ char prompt_color[COLOR_MAXLEN];
+ char error_color[COLOR_MAXLEN];
-+ char reset_color[COLOR_MAXLEN];
++ char reset_color_interactive[COLOR_MAXLEN];
++
+ char fraginfo_color[COLOR_MAXLEN];
+ char context_color[COLOR_MAXLEN];
+ char file_old_color[COLOR_MAXLEN];
+ char file_new_color[COLOR_MAXLEN];
++ char reset_color_diff[COLOR_MAXLEN];
+
+ int use_single_key;
+ char *interactive_diff_filter, *interactive_diff_algorithm;
13: fe853dff04 ! 9: 3fef9306d3 add-patch: remove dependency on "add-interactive" subsystem
@@ add-patch.c: static void err(struct add_p_state *s, const char *fmt, ...)
- fputs(s->s.cfg.error_color, stdout);
+ fputs(s->cfg.error_color, stdout);
vprintf(fmt, args);
-- puts(s->s.cfg.reset_color);
-+ puts(s->cfg.reset_color);
+- puts(s->s.cfg.reset_color_interactive);
++ puts(s->cfg.reset_color_interactive);
va_end(args);
}
@@ add-patch.c: static int parse_diff(struct add_p_state *s, const struct pathspec
struct object_id oid;
strvec_push(&args,
@@ add-patch.c: static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
+ }
+ strbuf_complete_line(plain);
- if (want_color_fd(1, -1)) {
+- if (want_color_fd(1, s->s.cfg.use_color_diff)) {
++ if (want_color_fd(1, s->cfg.use_color_diff)) {
struct child_process colored_cp = CHILD_PROCESS_INIT;
- const char *diff_filter = s->s.cfg.interactive_diff_filter;
+ const char *diff_filter = s->cfg.interactive_diff_filter;
@@ add-patch.c: static void render_hunk(struct add_p_state *s, struct hunk *hunk,
if (len)
strbuf_add(out, p, len);
else if (colored)
-- strbuf_addf(out, "%s\n", s->s.cfg.reset_color);
-+ strbuf_addf(out, "%s\n", s->cfg.reset_color);
+- strbuf_addf(out, "%s\n", s->s.cfg.reset_color_diff);
++ strbuf_addf(out, "%s\n", s->cfg.reset_color_diff);
else
strbuf_addch(out, '\n');
}
@@ add-patch.c: static void recolor_hunk(struct add_p_state *s, struct hunk *hunk)
+ s->cfg.file_new_color :
+ s->cfg.context_color);
strbuf_add(&s->colored, plain + current, eol - current);
-- strbuf_addstr(&s->colored, s->s.cfg.reset_color);
-+ strbuf_addstr(&s->colored, s->cfg.reset_color);
+- strbuf_addstr(&s->colored, s->s.cfg.reset_color_diff);
++ strbuf_addstr(&s->colored, s->cfg.reset_color_diff);
if (next > eol)
strbuf_add(&s->colored, plain + eol, next - eol);
current = next;
@@ add-patch.c: static int patch_update_file(struct add_p_state *s,
: 1));
printf(_(s->mode->prompt_mode[prompt_mode_type]),
s->buf.buf);
-- if (*s->s.cfg.reset_color)
-- fputs(s->s.cfg.reset_color, stdout);
-+ if (*s->cfg.reset_color)
-+ fputs(s->cfg.reset_color, stdout);
+- if (*s->s.cfg.reset_color_interactive)
+- fputs(s->s.cfg.reset_color_interactive, stdout);
++ if (*s->cfg.reset_color_interactive)
++ fputs(s->cfg.reset_color_interactive, stdout);
fflush(stdout);
if (read_single_character(s) == EOF)
break;
14: 37f6cc2a69 = 10: 9948b99601 add-patch: add support for in-memory index patching
6: e18b8b86c0 = 11: 19dfc229c9 cache-tree: allow writing in-memory index as tree
8: 8c6179ef85 < -: ---------- builtin/history: introduce subcommands to manage interrupted rewrites
9: f5c931a657 < -: ---------- builtin/history: implement "drop" subcommand
10: 3ab680968c < -: ---------- builtin/history: implement "reorder" subcommand
16: a66825ed24 < -: ---------- sequencer: allow callers to provide mappings for the old commit
17: 731b243497 ! 12: f72e364f92 builtin/history: implement "split" subcommand
@@ Commit message
Signed-off-by: Patrick Steinhardt <ps@pks.im>
## Documentation/git-history.adoc ##
-@@ Documentation/git-history.adoc: git history continue
- git history quit
- git history drop <commit>
- git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)
+@@ Documentation/git-history.adoc: SYNOPSIS
+ [synopsis]
+ git history [<options>]
+ git history reword [<options>] <commit>
+git history split [<options>] <commit> [--] [<pathspec>...]
DESCRIPTION
-----------
-@@ Documentation/git-history.adoc: child commits, as that would lead to an empty branch.
- be related to one another and must be reachable from the current `HEAD`
- commit.
+@@ Documentation/git-history.adoc: rewrite history in different ways:
+ provided, then this command will spawn an editor with the current
+ message of that commit.
+`split [--message=<message>] <commit> [--] [<pathspec>...]`::
+ Interactively split up <commit> into two commits by choosing
@@ Documentation/git-history.adoc: child commits, as that would lead to an empty br
+It is invalid to select either all or no hunks, as that would lead to
+one of the commits becoming empty.
+
- The following commands are used to manage an interrupted history-rewriting
- operation:
+ CONFIGURATION
+ -------------
-@@ Documentation/git-history.adoc: f44a46e third
- bf7438d first
- ----------
+@@ Documentation/git-history.adoc: include::includes/cmd-config-section-all.adoc[]
+ include::config/sequencer.adoc[]
+
++EXAMPLES
++--------
++
+Split a commit
+~~~~~~~~~~~~~~
+
@@ -0,0 +1 @@
+ 1 file changed, 1 insertion(+)
+----------
+
-+
- CONFIGURATION
- -------------
-
+ GIT
+ ---
+ Part of the linkgit:git[1] suite
## builtin/history.c ##
@@
-+/* Required for `comment_line_str`. */
-+#define USE_THE_REPOSITORY_VARIABLE
-+
+ #define USE_THE_REPOSITORY_VARIABLE
+
#include "builtin.h"
- #include "branch.h"
+#include "cache-tree.h"
- #include "commit.h"
#include "commit-reach.h"
+ #include "commit.h"
#include "config.h"
-+#include "editor.h"
- #include "environment.h"
- #include "gettext.h"
+@@
#include "hex.h"
- #include "object-name.h"
+ #include "oidmap.h"
#include "parse-options.h"
+#include "path.h"
-+#include "pathspec.h"
-+#include "read-cache-ll.h"
++#include "read-cache.h"
#include "refs.h"
+ #include "replay.h"
#include "reset.h"
#include "revision.h"
+#include "run-command.h"
#include "sequencer.h"
-+#include "sparse-index.h"
-
- static int cmd_history_abort(int argc,
- const char **argv,
-@@ builtin/history.c: static int apply_commits(struct repository *repo,
- const struct strvec *commits,
- struct commit *head,
- struct commit *base,
-+ const struct oidmap *rewritten_commits,
- const char *action)
- {
- struct setup_revision_opt revision_opts = {
-@@ builtin/history.c: static int apply_commits(struct repository *repo,
- replay_opts.strategy = replay_opts.default_strategy;
- replay_opts.default_strategy = NULL;
- }
-+ replay_opts.old_oid_mappings = rewritten_commits;
-
- strvec_push(&args, "");
- strvec_pushv(&args, commits->v);
-@@ builtin/history.c: static int cmd_history_drop(int argc,
- if (ret < 0)
- goto out;
-
-- ret = apply_commits(repo, &commits, head, commit_to_drop, "drop");
-+ ret = apply_commits(repo, &commits, head, commit_to_drop,
-+ NULL, "drop");
- if (ret < 0)
- goto out;
- }
-@@ builtin/history.c: static int cmd_history_reorder(int argc,
- replace_commits(&commits, &commit_to_reorder->object.oid, NULL, 0);
- replace_commits(&commits, &anchor->object.oid, replacement, ARRAY_SIZE(replacement));
-
-- ret = apply_commits(repo, &commits, head, old, "reorder");
-+ ret = apply_commits(repo, &commits, head, old, NULL, "reorder");
- if (ret < 0)
- goto out;
-
-@@ builtin/history.c: static int cmd_history_reorder(int argc,
+ #include "strvec.h"
+ #include "tree.h"
+@@ builtin/history.c: static int cmd_history_reword(int argc,
return ret;
}
-+static void change_data_free(void *util, const char *str UNUSED)
-+{
-+ struct wt_status_change_data *d = util;
-+ free(d->rename_source);
-+ free(d);
-+}
-+
-+static int fill_commit_message(struct repository *repo,
-+ const struct object_id *old_tree,
-+ const struct object_id *new_tree,
-+ const char *default_message,
-+ const char *provided_message,
-+ const char *action,
-+ struct strbuf *out)
-+{
-+ if (!provided_message) {
-+ const char *path = git_path_commit_editmsg();
-+ const char *hint =
-+ _("Please enter the commit message for the %s changes. Lines starting\n"
-+ "with '%s' will be kept; you may remove them yourself if you want to.\n");
-+ int verbose = 1;
-+
-+ strbuf_addstr(out, default_message);
-+ strbuf_addch(out, '\n');
-+ strbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);
-+ write_file_buf(path, out->buf, out->len);
-+
-+ repo_config_get_bool(repo, "commit.verbose", &verbose);
-+ if (verbose) {
-+ struct wt_status s;
-+
-+ wt_status_prepare(repo, &s);
-+ FREE_AND_NULL(s.branch);
-+ s.ahead_behind_flags = AHEAD_BEHIND_QUICK;
-+ s.commit_template = 1;
-+ s.colopts = 0;
-+ s.display_comment_prefix = 1;
-+ s.hints = 0;
-+ s.use_color = 0;
-+ s.whence = FROM_COMMIT;
-+ s.committable = 1;
-+
-+ s.fp = fopen(git_path_commit_editmsg(), "a");
-+ if (!s.fp)
-+ return error_errno(_("could not open '%s'"), git_path_commit_editmsg());
-+
-+ wt_status_collect_changes_trees(&s, old_tree, new_tree);
-+ wt_status_print(&s);
-+ wt_status_collect_free_buffers(&s);
-+ string_list_clear_func(&s.change, change_data_free);
-+ }
-+
-+ strbuf_reset(out);
-+ if (launch_editor(path, out, NULL)) {
-+ fprintf(stderr, _("Please supply the message using the -m option.\n"));
-+ return -1;
-+ }
-+ strbuf_stripspace(out, comment_line_str);
-+ } else {
-+ strbuf_addstr(out, provided_message);
-+ }
-+
-+ cleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);
-+
-+ if (!out->len) {
-+ fprintf(stderr, _("Aborting commit due to empty commit message.\n"));
-+ return -1;
-+ }
-+
-+ return 0;
-+}
-+
+static int split_commit(struct repository *repo,
+ struct commit *original_commit,
+ struct pathspec *pathspec,
@@ builtin/history.c: static int cmd_history_reorder(int argc,
+ original_commit_tree_oid = *get_commit_tree_oid(original_commit);
+
+ /*
-+ * Construct the first commit. This is done by taking the original
-+ * commit parent's tree and selectively patching changes from the diff
-+ * between that parent and its child.
-+ */
++ * Construct the first commit. This is done by taking the original
++ * commit parent's tree and selectively patching changes from the diff
++ * between that parent and its child.
++ */
+ repo_git_path_replace(repo, &index_file, "%s", "history-split.index");
+
+ read_tree_cmd.git_cmd = 1;
@@ builtin/history.c: static int cmd_history_reorder(int argc,
+ unlink(index_file.buf);
+
+ /*
-+ * We disallow the cases where either the split-out commit or the
-+ * original commit would become empty. Consequently, if we see that the
-+ * new tree ID matches either of those trees we abort.
-+ */
++ * We disallow the cases where either the split-out commit or the
++ * original commit would become empty. Consequently, if we see that the
++ * new tree ID matches either of those trees we abort.
++ */
+ if (oideq(&split_tree->object.oid, &parent_tree_oid)) {
+ ret = error(_("split commit is empty"));
+ goto out;
@@ builtin/history.c: static int cmd_history_reorder(int argc,
+ }
+
+ /*
-+ * The second commit is much simpler to construct, as we can simply use
-+ * the original commit details, except that we adjust its parent to be
-+ * the newly split-out commit.
-+ */
++ * The second commit is much simpler to construct, as we can simply use
++ * the original commit details, except that we adjust its parent to be
++ * the newly split-out commit.
++ */
+ find_commit_subject(original_message, &original_body);
+ first_commit = lookup_commit_reference(repo, &out[0]);
+ commit_list_append(first_commit, &parents);
@@ builtin/history.c: static int cmd_history_reorder(int argc,
+ OPT_END(),
+ };
+ struct oidmap rewritten_commits = OIDMAP_INIT;
-+ struct commit *original_commit, *head;
++ struct commit *original_commit, *parent, *head;
+ struct strvec commits = STRVEC_INIT;
+ struct commit_list *list = NULL;
+ struct object_id split_commits[2];
-+ struct replay_oid_mapping mapping[2] = { 0 };
+ struct pathspec pathspec = { 0 };
+ int ret;
+
@@ builtin/history.c: static int cmd_history_reorder(int argc,
+ goto out;
+ }
+
++ parent = original_commit->parents ? original_commit->parents->item : NULL;
++ if (parent && repo_parse_commit(repo, parent)) {
++ ret = error(_("unable to parse commit %s"),
++ oid_to_hex(&parent->object.oid));
++ goto out;
++ }
++
+ head = lookup_commit_reference_by_name("HEAD");
+ if (!head) {
+ ret = error(_("could not resolve HEAD to a commit"));
@@ builtin/history.c: static int cmd_history_reorder(int argc,
+ }
+
+ parse_pathspec(&pathspec, 0,
-+ PATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,
-+ prefix, argv + 1);
++ PATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,
++ prefix, argv + 1);
+
+ /*
-+ * Collect the list of commits that we'll have to reapply now already.
-+ * This ensures that we'll abort early on in case the range of commits
-+ * contains merges, which we do not yet handle.
-+ */
-+ ret = collect_commits(repo, original_commit->parents ? original_commit->parents->item : NULL,
-+ head, &commits);
++ * Collect the list of commits that we'll have to reapply now already.
++ * This ensures that we'll abort early on in case the range of commits
++ * contains merges, which we do not yet handle.
++ */
++ ret = collect_commits(repo, parent, head, &commits);
+ if (ret < 0)
+ goto out;
+
@@ builtin/history.c: static int cmd_history_reorder(int argc,
+ if (ret < 0)
+ goto out;
+
-+ mapping[0].entry.oid = split_commits[0];
-+ mapping[0].rewritten_oid = original_commit->object.oid;
-+ oidmap_put(&rewritten_commits, &mapping[0]);
-+ mapping[1].entry.oid = split_commits[1];
-+ mapping[1].rewritten_oid = original_commit->object.oid;
-+ oidmap_put(&rewritten_commits, &mapping[1]);
-+
+ replace_commits(&commits, &original_commit->object.oid,
+ split_commits, ARRAY_SIZE(split_commits));
+
-+ ret = apply_commits(repo, &commits, head, original_commit,
-+ &rewritten_commits, "split");
++ ret = apply_commits(repo, &commits, parent, head, "split");
+ if (ret < 0)
+ goto out;
+
@@ builtin/history.c: static int cmd_history_reorder(int argc,
const char **argv,
const char *prefix,
@@ builtin/history.c: int cmd_history(int argc,
- N_("git history quit"),
- N_("git history drop <commit>"),
- N_("git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)"),
+ const char * const usage[] = {
+ N_("git history [<options>]"),
+ N_("git history reword [<options>] <commit>"),
+ N_("git history split [<options>] <commit> [--] [<pathspec>...]"),
NULL,
};
parse_opt_subcommand_fn *fn = NULL;
-@@ builtin/history.c: int cmd_history(int argc,
- OPT_SUBCOMMAND("quit", &fn, cmd_history_quit),
- OPT_SUBCOMMAND("drop", &fn, cmd_history_drop),
- OPT_SUBCOMMAND("reorder", &fn, cmd_history_reorder),
+ struct option options[] = {
+ OPT_SUBCOMMAND("reword", &fn, cmd_history_reword),
+ OPT_SUBCOMMAND("split", &fn, cmd_history_split),
OPT_END(),
};
@@ builtin/history.c: int cmd_history(int argc,
## t/meson.build ##
@@ t/meson.build: integration_tests = [
+ 't3438-rebase-broken-files.sh',
't3450-history.sh',
- 't3451-history-drop.sh',
- 't3452-history-reorder.sh',
-+ 't3453-history-split.sh',
+ 't3451-history-reword.sh',
++ 't3452-history-split.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
- ## t/t3453-history-split.sh (new) ##
+ ## t/t3452-history-split.sh (new) ##
@@
+#!/bin/sh
+
@@ t/t3453-history-split.sh (new)
+ )
+'
+
-+test_expect_success 'refuses to work with changes in the worktree or index' '
-+ test_when_finished "rm -rf repo" &&
-+ git init repo &&
-+ (
-+ cd repo &&
-+ test_commit initial &&
-+ touch bar foo &&
-+ git add . &&
-+ git commit -m split-me &&
-+
-+ echo changed >bar &&
-+ test_must_fail git history split -m message HEAD 2>err <<-EOF &&
-+ y
-+ n
-+ EOF
-+ test_grep "Your local changes to the following files would be overwritten" err &&
-+
-+ git add bar &&
-+ test_must_fail git history split -m message HEAD 2>err <<-EOF &&
-+ y
-+ n
-+ EOF
-+ test_grep "Your local changes to the following files would be overwritten" err
-+ )
-+'
-+
+test_expect_success 'can split up tip commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
@@ t/t3453-history-split.sh (new)
+ )
+'
+
-+test_expect_success 'skips change summary with commit.verbose=false' '
-+ test_when_finished "rm -rf repo" &&
-+ git init repo &&
-+ (
-+ cd repo &&
-+ touch bar foo &&
-+ git add . &&
-+ git commit -m split-me &&
-+
-+ write_script fake-editor.sh <<-\EOF &&
-+ cp "$1" . &&
-+ echo "some commit message" >>"$1"
-+ EOF
-+ test_set_editor "$(pwd)"/fake-editor.sh &&
-+
-+ git -c commit.verbose=false history split HEAD <<-EOF &&
-+ y
-+ n
-+ EOF
-+
-+ cat >expect <<-EOF &&
-+
-+ # Please enter the commit message for the split-out changes. Lines starting
-+ # with ${SQ}#${SQ} will be kept; you may remove them yourself if you want to.
-+ EOF
-+ test_cmp expect COMMIT_EDITMSG &&
-+
-+ expect_log <<-EOF
-+ split-me
-+ some commit message
-+ EOF
-+ )
-+'
-+
+test_expect_success 'can use pathspec to limit what gets split' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
@@ t/t3453-history-split.sh (new)
+ old_head=$(git rev-parse HEAD) &&
+
+ write_script .git/hooks/prepare-commit-msg <<-EOF &&
-+ echo "prepare-commit-msg: \$@" >>"$(pwd)/hooks.log"
++ touch "$(pwd)/hooks.log"
+ EOF
+ write_script .git/hooks/post-commit <<-EOF &&
-+ echo "post-commit" >>"$(pwd)/hooks.log"
++ touch "$(pwd)/hooks.log"
+ EOF
+ write_script .git/hooks/post-rewrite <<-EOF &&
-+ {
-+ echo "post-rewrite: \$@"
-+ cat
-+ } >>"$(pwd)/hooks.log"
++ touch "$(pwd)/hooks.log"
+ EOF
+
+ set_fake_editor &&
@@ t/t3453-history-split.sh (new)
+ split-out commit
+ EOF
+
-+ cat >expect <<-EOF &&
-+ prepare-commit-msg: .git/COMMIT_EDITMSG message
-+ post-commit
-+ prepare-commit-msg: .git/COMMIT_EDITMSG message
-+ post-commit
-+ post-rewrite: history
-+ $old_head $(git rev-parse HEAD~)
-+ $old_head $(git rev-parse HEAD)
-+ EOF
-+ test_cmp expect hooks.log
++ test_path_is_missing hooks.log
+ )
+'
+
@@ t/t3453-history-split.sh (new)
+ )
+'
+
++test_expect_success 'retains changes in the worktree and index' '
++ test_when_finished "rm -rf repo" &&
++ git init repo &&
++ (
++ cd repo &&
++ echo a >a &&
++ echo b >b &&
++ git add . &&
++ git commit -m "initial commit" &&
++ echo a-modified >a &&
++ echo b-modified >b &&
++ git add b &&
++ git history split HEAD -m a-only <<-EOF &&
++ y
++ n
++ EOF
++
++ expect_tree_entries HEAD~ <<-EOF &&
++ a
++ EOF
++ expect_tree_entries HEAD <<-EOF &&
++ a
++ b
++ EOF
++
++ cat >expect <<-\EOF &&
++ M a
++ M b
++ ?? actual
++ ?? expect
++ EOF
++ git status --porcelain >actual &&
++ test_cmp expect actual
++ )
++'
++
+test_done
---
base-commit: 896ecf94ed941815749901ff6ad06d7141904cb2
change-id: 20250819-b4-pks-history-builtin-83398f9a05f0
^ permalink raw reply [flat|nested] 278+ messages in thread* [PATCH v4 01/12] wt-status: provide function to expose status for trees
2025-10-01 15:57 ` [PATCH v4 00/12] " Patrick Steinhardt
@ 2025-10-01 15:57 ` Patrick Steinhardt
2025-10-14 8:49 ` Karthik Nayak
2025-10-01 15:57 ` [PATCH v4 02/12] replay: extract logic to pick commits Patrick Steinhardt
` (12 subsequent siblings)
13 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-01 15:57 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren
The "wt-status" subsystem is responsible for printing status information
around the current state of the working tree. This most importantly
includes information around whether the working tree or the index have
any changes.
We're about to introduce a new command though where the changes in
neither of them are actually relevant to us. Instead, what we want is to
format the changes between two different trees. While it is a little bit
of a stretch to add this as functionality to _working tree_ status, it
doesn't make any sense to open-code this functionality, either.
Implement a new function `wt_status_collect_changes_trees()` that diffs
two trees and formats the status accordingly. This function is not yet
used, but will be in a subsequent commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
wt-status.c | 24 ++++++++++++++++++++++++
wt-status.h | 3 +++
2 files changed, 27 insertions(+)
diff --git a/wt-status.c b/wt-status.c
index 8ffe6d3988..b66edbfca6 100644
--- a/wt-status.c
+++ b/wt-status.c
@@ -612,6 +612,30 @@ static void wt_status_collect_updated_cb(struct diff_queue_struct *q,
}
}
+void wt_status_collect_changes_trees(struct wt_status *s,
+ const struct object_id *old_treeish,
+ const struct object_id *new_treeish)
+{
+ struct diff_options opts = { 0 };
+
+ repo_diff_setup(s->repo, &opts);
+ opts.output_format = DIFF_FORMAT_CALLBACK;
+ opts.format_callback = wt_status_collect_updated_cb;
+ opts.format_callback_data = s;
+ opts.detect_rename = s->detect_rename >= 0 ? s->detect_rename : opts.detect_rename;
+ opts.rename_limit = s->rename_limit >= 0 ? s->rename_limit : opts.rename_limit;
+ opts.rename_score = s->rename_score >= 0 ? s->rename_score : opts.rename_score;
+ opts.flags.recursive = 1;
+ diff_setup_done(&opts);
+
+ diff_tree_oid(old_treeish, new_treeish, "", &opts);
+ diffcore_std(&opts);
+ diff_flush(&opts);
+ wt_status_get_state(s->repo, &s->state, 0);
+
+ diff_free(&opts);
+}
+
static void wt_status_collect_changes_worktree(struct wt_status *s)
{
struct rev_info rev;
diff --git a/wt-status.h b/wt-status.h
index e40a27214a..924d7a5fa9 100644
--- a/wt-status.h
+++ b/wt-status.h
@@ -153,6 +153,9 @@ void wt_status_add_cut_line(struct wt_status *s);
void wt_status_prepare(struct repository *r, struct wt_status *s);
void wt_status_print(struct wt_status *s);
void wt_status_collect(struct wt_status *s);
+void wt_status_collect_changes_trees(struct wt_status *s,
+ const struct object_id *old_treeish,
+ const struct object_id *new_treeish);
/*
* Frees the buffers allocated by wt_status_collect.
*/
--
2.51.0.700.g236ee7b076.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH v4 01/12] wt-status: provide function to expose status for trees
2025-10-01 15:57 ` [PATCH v4 01/12] wt-status: provide function to expose status for trees Patrick Steinhardt
@ 2025-10-14 8:49 ` Karthik Nayak
2025-10-21 11:43 ` Patrick Steinhardt
0 siblings, 1 reply; 278+ messages in thread
From: Karthik Nayak @ 2025-10-14 8:49 UTC (permalink / raw)
To: Patrick Steinhardt, git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren
[-- Attachment #1: Type: text/plain, Size: 3438 bytes --]
Patrick Steinhardt <ps@pks.im> writes:
> The "wt-status" subsystem is responsible for printing status information
> around the current state of the working tree. This most importantly
> includes information around whether the working tree or the index have
> any changes.
>
> We're about to introduce a new command though where the changes in
Nit: s/though//
> neither of them are actually relevant to us. Instead, what we want is to
> format the changes between two different trees. While it is a little bit
> of a stretch to add this as functionality to _working tree_ status, it
> doesn't make any sense to open-code this functionality, either.
>
> Implement a new function `wt_status_collect_changes_trees()` that diffs
> two trees and formats the status accordingly. This function is not yet
> used, but will be in a subsequent commit.
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> wt-status.c | 24 ++++++++++++++++++++++++
> wt-status.h | 3 +++
> 2 files changed, 27 insertions(+)
>
> diff --git a/wt-status.c b/wt-status.c
> index 8ffe6d3988..b66edbfca6 100644
> --- a/wt-status.c
> +++ b/wt-status.c
> @@ -612,6 +612,30 @@ static void wt_status_collect_updated_cb(struct diff_queue_struct *q,
> }
> }
>
> +void wt_status_collect_changes_trees(struct wt_status *s,
> + const struct object_id *old_treeish,
> + const struct object_id *new_treeish)
> +{
So, my understanding here is that we want to diff two trees
`old_treeish` and `new_treeish` and then finally store the status change
in `wt_status`
> + struct diff_options opts = { 0 };
> +
> + repo_diff_setup(s->repo, &opts);
> + opts.output_format = DIFF_FORMAT_CALLBACK;
> + opts.format_callback = wt_status_collect_updated_cb;
> + opts.format_callback_data = s;
> + opts.detect_rename = s->detect_rename >= 0 ? s->detect_rename : opts.detect_rename;
> + opts.rename_limit = s->rename_limit >= 0 ? s->rename_limit : opts.rename_limit;
> + opts.rename_score = s->rename_score >= 0 ? s->rename_score : opts.rename_score;
Curious, why do we need a '>= 0' check here?
> + opts.flags.recursive = 1;
> + diff_setup_done(&opts);
> +
>
So first we setup the diff options, with the right callbacks so that the
relevant information is added to the `wt_status`.
> + diff_tree_oid(old_treeish, new_treeish, "", &opts);
> + diffcore_std(&opts);
> + diff_flush(&opts);
This is the part which calls the callback function with the relevant
information and callback data.
> + wt_status_get_state(s->repo, &s->state, 0);
> +
Based on the list of diff data in `s->change`, we add the status print
information. Okay makes sense.
> + diff_free(&opts);
> +}
> +
> static void wt_status_collect_changes_worktree(struct wt_status *s)
> {
> struct rev_info rev;
> diff --git a/wt-status.h b/wt-status.h
> index e40a27214a..924d7a5fa9 100644
> --- a/wt-status.h
> +++ b/wt-status.h
> @@ -153,6 +153,9 @@ void wt_status_add_cut_line(struct wt_status *s);
> void wt_status_prepare(struct repository *r, struct wt_status *s);
> void wt_status_print(struct wt_status *s);
> void wt_status_collect(struct wt_status *s);
> +void wt_status_collect_changes_trees(struct wt_status *s,
> + const struct object_id *old_treeish,
> + const struct object_id *new_treeish);
> /*
> * Frees the buffers allocated by wt_status_collect.
> */
>
> --
> 2.51.0.700.g236ee7b076.dirty
So this function will be used in an upcoming patch, looks good.
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 690 bytes --]
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH v4 01/12] wt-status: provide function to expose status for trees
2025-10-14 8:49 ` Karthik Nayak
@ 2025-10-21 11:43 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-21 11:43 UTC (permalink / raw)
To: Karthik Nayak
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren
On Tue, Oct 14, 2025 at 04:49:14AM -0400, Karthik Nayak wrote:
> Patrick Steinhardt <ps@pks.im> writes:
> > diff --git a/wt-status.c b/wt-status.c
> > index 8ffe6d3988..b66edbfca6 100644
> > --- a/wt-status.c
> > +++ b/wt-status.c
> > @@ -612,6 +612,30 @@ static void wt_status_collect_updated_cb(struct diff_queue_struct *q,
> > }
> > }
> >
> > +void wt_status_collect_changes_trees(struct wt_status *s,
> > + const struct object_id *old_treeish,
> > + const struct object_id *new_treeish)
> > +{
>
> So, my understanding here is that we want to diff two trees
> `old_treeish` and `new_treeish` and then finally store the status change
> in `wt_status`
Exactly.
> > + struct diff_options opts = { 0 };
> > +
> > + repo_diff_setup(s->repo, &opts);
> > + opts.output_format = DIFF_FORMAT_CALLBACK;
> > + opts.format_callback = wt_status_collect_updated_cb;
> > + opts.format_callback_data = s;
> > + opts.detect_rename = s->detect_rename >= 0 ? s->detect_rename : opts.detect_rename;
> > + opts.rename_limit = s->rename_limit >= 0 ? s->rename_limit : opts.rename_limit;
> > + opts.rename_score = s->rename_score >= 0 ? s->rename_score : opts.rename_score;
>
> Curious, why do we need a '>= 0' check here?
I'm mostly just mirroring the same behaviour that we already have in
`wt_status_collect_chanegs_worktree()`.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH v4 02/12] replay: extract logic to pick commits
2025-10-01 15:57 ` [PATCH v4 00/12] " Patrick Steinhardt
2025-10-01 15:57 ` [PATCH v4 01/12] wt-status: provide function to expose status for trees Patrick Steinhardt
@ 2025-10-01 15:57 ` Patrick Steinhardt
2025-10-01 15:57 ` [PATCH v4 03/12] replay: stop using `the_repository` Patrick Steinhardt
` (11 subsequent siblings)
13 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-01 15:57 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren
We're about to add a new git-history(1) command that will reuse some of
the same infrastructure as git-replay(1). To prepare for this, extract
the logic to pick a commit into a new "replay.c" file so that it can be
shared between both commands.
Rename the function to have a "replay_" prefix to clearly indicate its
subsystem.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Makefile | 1 +
builtin/replay.c | 110 ++--------------------------------------------------
meson.build | 1 +
replay.c | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
replay.h | 18 +++++++++
5 files changed, 138 insertions(+), 107 deletions(-)
diff --git a/Makefile b/Makefile
index 4c95affadb..5960c80736 100644
--- a/Makefile
+++ b/Makefile
@@ -1137,6 +1137,7 @@ LIB_OBJS += refs/ref-cache.o
LIB_OBJS += refspec.o
LIB_OBJS += remote.o
LIB_OBJS += replace-object.o
+LIB_OBJS += replay.o
LIB_OBJS += repo-settings.o
LIB_OBJS += repository.o
LIB_OBJS += rerere.o
diff --git a/builtin/replay.c b/builtin/replay.c
index b6f9d53560..e9d6559b47 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -2,7 +2,6 @@
* "git replay" builtin command
*/
-#define USE_THE_REPOSITORY_VARIABLE
#define DISABLE_SIGN_COMPARE_WARNINGS
#include "git-compat-util.h"
@@ -15,18 +14,12 @@
#include "object-name.h"
#include "parse-options.h"
#include "refs.h"
+#include "replay.h"
#include "revision.h"
#include "strmap.h"
#include <oidset.h>
#include <tree.h>
-static const char *short_commit_name(struct repository *repo,
- struct commit *commit)
-{
- return repo_find_unique_abbrev(repo, &commit->object.oid,
- DEFAULT_ABBREV);
-}
-
static struct commit *peel_committish(struct repository *repo, const char *name)
{
struct object *obj;
@@ -39,59 +32,6 @@ static struct commit *peel_committish(struct repository *repo, const char *name)
OBJ_COMMIT);
}
-static char *get_author(const char *message)
-{
- size_t len;
- const char *a;
-
- a = find_commit_header(message, "author", &len);
- if (a)
- return xmemdupz(a, len);
-
- return NULL;
-}
-
-static struct commit *create_commit(struct repository *repo,
- struct tree *tree,
- struct commit *based_on,
- struct commit *parent)
-{
- struct object_id ret;
- struct object *obj = NULL;
- struct commit_list *parents = NULL;
- char *author;
- char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
- struct commit_extra_header *extra = NULL;
- struct strbuf msg = STRBUF_INIT;
- const char *out_enc = get_commit_output_encoding();
- const char *message = repo_logmsg_reencode(repo, based_on,
- NULL, out_enc);
- const char *orig_message = NULL;
- const char *exclude_gpgsig[] = { "gpgsig", NULL };
-
- commit_list_insert(parent, &parents);
- extra = read_commit_extra_headers(based_on, exclude_gpgsig);
- find_commit_subject(message, &orig_message);
- strbuf_addstr(&msg, orig_message);
- author = get_author(message);
- reset_ident_date();
- if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,
- &ret, author, NULL, sign_commit, extra)) {
- error(_("failed to write commit object"));
- goto out;
- }
-
- obj = parse_object(repo, &ret);
-
-out:
- repo_unuse_commit_buffer(the_repository, based_on, message);
- free_commit_extra_headers(extra);
- free_commit_list(parents);
- strbuf_release(&msg);
- free(author);
- return (struct commit *)obj;
-}
-
struct ref_info {
struct commit *onto;
struct strset positive_refs;
@@ -240,50 +180,6 @@ static void determine_replay_mode(struct repository *repo,
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 repository *repo,
- struct commit *pickme,
- kh_oid_map_t *replayed_commits,
- struct commit *onto,
- struct merge_options *merge_opt,
- struct merge_result *result)
-{
- 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(repo, replayed_base);
- pickme_tree = repo_get_commit_tree(repo, pickme);
- base_tree = repo_get_commit_tree(repo, base);
-
- merge_opt->branch1 = short_commit_name(repo, replayed_base);
- merge_opt->branch2 = short_commit_name(repo, 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(repo, result->tree, pickme, replayed_base);
-}
-
static int add_ref_to_transaction(struct ref_transaction *transaction,
const char *refname,
const struct object_id *new_oid,
@@ -459,8 +355,8 @@ int cmd_replay(int argc,
if (commit->parents->next)
die(_("replaying merge commits is not supported yet!"));
- last_commit = pick_regular_commit(repo, commit, replayed_commits,
- onto, &merge_opt, &result);
+ last_commit = replay_pick_regular_commit(repo, commit, replayed_commits,
+ onto, &merge_opt, &result);
if (!last_commit)
break;
diff --git a/meson.build b/meson.build
index b3dfcc0497..c320bdba9f 100644
--- a/meson.build
+++ b/meson.build
@@ -463,6 +463,7 @@ libgit_sources = [
'reftable/writer.c',
'remote.c',
'replace-object.c',
+ 'replay.c',
'repo-settings.c',
'repository.c',
'rerere.c',
diff --git a/replay.c b/replay.c
new file mode 100644
index 0000000000..e22ce39940
--- /dev/null
+++ b/replay.c
@@ -0,0 +1,115 @@
+#define USE_THE_REPOSITORY_VARIABLE
+
+#include "git-compat-util.h"
+#include "commit.h"
+#include "environment.h"
+#include "gettext.h"
+#include "ident.h"
+#include "object.h"
+#include "object-name.h"
+#include "replay.h"
+#include "tree.h"
+
+static const char *short_commit_name(struct repository *repo,
+ struct commit *commit)
+{
+ return repo_find_unique_abbrev(repo, &commit->object.oid,
+ DEFAULT_ABBREV);
+}
+
+static char *get_author(const char *message)
+{
+ size_t len;
+ const char *a;
+
+ a = find_commit_header(message, "author", &len);
+ if (a)
+ return xmemdupz(a, len);
+
+ return NULL;
+}
+
+static struct commit *create_commit(struct repository *repo,
+ struct tree *tree,
+ struct commit *based_on,
+ struct commit *parent)
+{
+ struct object_id ret;
+ struct object *obj = NULL;
+ struct commit_list *parents = NULL;
+ char *author;
+ char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
+ struct commit_extra_header *extra = NULL;
+ struct strbuf msg = STRBUF_INIT;
+ const char *out_enc = get_commit_output_encoding();
+ const char *message = repo_logmsg_reencode(repo, based_on,
+ NULL, out_enc);
+ const char *orig_message = NULL;
+ const char *exclude_gpgsig[] = { "gpgsig", NULL };
+
+ commit_list_insert(parent, &parents);
+ extra = read_commit_extra_headers(based_on, exclude_gpgsig);
+ find_commit_subject(message, &orig_message);
+ strbuf_addstr(&msg, orig_message);
+ author = get_author(message);
+ reset_ident_date();
+ if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,
+ &ret, author, NULL, sign_commit, extra)) {
+ error(_("failed to write commit object"));
+ goto out;
+ }
+
+ obj = parse_object(repo, &ret);
+
+out:
+ repo_unuse_commit_buffer(the_repository, based_on, message);
+ free_commit_extra_headers(extra);
+ free_commit_list(parents);
+ strbuf_release(&msg);
+ free(author);
+ return (struct commit *)obj;
+}
+
+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);
+}
+
+struct commit *replay_pick_regular_commit(struct repository *repo,
+ struct commit *pickme,
+ kh_oid_map_t *replayed_commits,
+ struct commit *onto,
+ struct merge_options *merge_opt,
+ struct merge_result *result)
+{
+ 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(repo, replayed_base);
+ pickme_tree = repo_get_commit_tree(repo, pickme);
+ base_tree = repo_get_commit_tree(repo, base);
+
+ merge_opt->branch1 = short_commit_name(repo, replayed_base);
+ merge_opt->branch2 = short_commit_name(repo, 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(repo, result->tree, pickme, replayed_base);
+}
diff --git a/replay.h b/replay.h
new file mode 100644
index 0000000000..a461b5c234
--- /dev/null
+++ b/replay.h
@@ -0,0 +1,18 @@
+#ifndef REPLAY_H
+#define REPLAY_H
+
+#include "khash.h"
+#include "merge-ort.h"
+#include "repository.h"
+
+struct commit;
+struct tree;
+
+struct commit *replay_pick_regular_commit(struct repository *repo,
+ struct commit *pickme,
+ kh_oid_map_t *replayed_commits,
+ struct commit *onto,
+ struct merge_options *merge_opt,
+ struct merge_result *result);
+
+#endif
--
2.51.0.700.g236ee7b076.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH v4 03/12] replay: stop using `the_repository`
2025-10-01 15:57 ` [PATCH v4 00/12] " Patrick Steinhardt
2025-10-01 15:57 ` [PATCH v4 01/12] wt-status: provide function to expose status for trees Patrick Steinhardt
2025-10-01 15:57 ` [PATCH v4 02/12] replay: extract logic to pick commits Patrick Steinhardt
@ 2025-10-01 15:57 ` Patrick Steinhardt
2025-10-14 8:53 ` Karthik Nayak
2025-10-01 15:57 ` [PATCH v4 04/12] replay: parse commits before dereferencing them Patrick Steinhardt
` (10 subsequent siblings)
13 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-01 15:57 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren
In `create_commit()` we're using `the_repository` even though we already
have a repository passed to use as an argument. Fix this.
Note that we still cannot get rid of `USE_THE_REPOSITORY_VARIABLE`. This
is because we use `DEFAULT_ABBREV and `get_commit_output_encoding()`,
both of which are stored as global variables that can be modified via
the Git configuration.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
replay.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/replay.c b/replay.c
index e22ce39940..13d75d8054 100644
--- a/replay.c
+++ b/replay.c
@@ -62,7 +62,7 @@ static struct commit *create_commit(struct repository *repo,
obj = parse_object(repo, &ret);
out:
- repo_unuse_commit_buffer(the_repository, based_on, message);
+ repo_unuse_commit_buffer(repo, based_on, message);
free_commit_extra_headers(extra);
free_commit_list(parents);
strbuf_release(&msg);
--
2.51.0.700.g236ee7b076.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH v4 03/12] replay: stop using `the_repository`
2025-10-01 15:57 ` [PATCH v4 03/12] replay: stop using `the_repository` Patrick Steinhardt
@ 2025-10-14 8:53 ` Karthik Nayak
0 siblings, 0 replies; 278+ messages in thread
From: Karthik Nayak @ 2025-10-14 8:53 UTC (permalink / raw)
To: Patrick Steinhardt, git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren
[-- Attachment #1: Type: text/plain, Size: 1165 bytes --]
Patrick Steinhardt <ps@pks.im> writes:
> In `create_commit()` we're using `the_repository` even though we already
> have a repository passed to use as an argument. Fix this.
>
I was thinking about this as I read your previous patch. This makes
sense.
> Note that we still cannot get rid of `USE_THE_REPOSITORY_VARIABLE`. This
> is because we use `DEFAULT_ABBREV and `get_commit_output_encoding()`,
> both of which are stored as global variables that can be modified via
> the Git configuration.
>
That's a bummer. But one less step needed is a win.
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> replay.c | 2 +-
> 1 file changed, 1 insertion(+), 1 deletion(-)
>
> diff --git a/replay.c b/replay.c
> index e22ce39940..13d75d8054 100644
> --- a/replay.c
> +++ b/replay.c
> @@ -62,7 +62,7 @@ static struct commit *create_commit(struct repository *repo,
> obj = parse_object(repo, &ret);
>
> out:
> - repo_unuse_commit_buffer(the_repository, based_on, message);
> + repo_unuse_commit_buffer(repo, based_on, message);
> free_commit_extra_headers(extra);
> free_commit_list(parents);
> strbuf_release(&msg);
>
> --
> 2.51.0.700.g236ee7b076.dirty
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 690 bytes --]
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH v4 04/12] replay: parse commits before dereferencing them
2025-10-01 15:57 ` [PATCH v4 00/12] " Patrick Steinhardt
` (2 preceding siblings ...)
2025-10-01 15:57 ` [PATCH v4 03/12] replay: stop using `the_repository` Patrick Steinhardt
@ 2025-10-01 15:57 ` Patrick Steinhardt
2025-10-14 8:57 ` Karthik Nayak
2025-10-01 15:57 ` [PATCH v4 05/12] builtin: add new "history" command Patrick Steinhardt
` (9 subsequent siblings)
13 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-01 15:57 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren
When looking up a commit it may not be parsed yet. Callers that wish to
access the fields of `struct commit` have to call `repo_parse_commit()`
first so that it is guaranteed to be populated.
We didn't yet care about doing so, because code paths that lead to
`pick_regular_commit()` in "builtin/replay.c" already implicitly parsed
the commits. But now that the function is exposed to outside callers
it's quite easy to get this wrong.
Make the function easier to use by calling `repo_parse_commit()`.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
replay.c | 3 +++
1 file changed, 3 insertions(+)
diff --git a/replay.c b/replay.c
index 13d75d8054..c3628d2488 100644
--- a/replay.c
+++ b/replay.c
@@ -90,6 +90,9 @@ struct commit *replay_pick_regular_commit(struct repository *repo,
struct commit *base, *replayed_base;
struct tree *pickme_tree, *base_tree;
+ if (repo_parse_commit(repo, pickme))
+ return NULL;
+
base = pickme->parents->item;
replayed_base = mapped_commit(replayed_commits, base, onto);
--
2.51.0.700.g236ee7b076.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH v4 04/12] replay: parse commits before dereferencing them
2025-10-01 15:57 ` [PATCH v4 04/12] replay: parse commits before dereferencing them Patrick Steinhardt
@ 2025-10-14 8:57 ` Karthik Nayak
0 siblings, 0 replies; 278+ messages in thread
From: Karthik Nayak @ 2025-10-14 8:57 UTC (permalink / raw)
To: Patrick Steinhardt, git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren
[-- Attachment #1: Type: text/plain, Size: 1455 bytes --]
Patrick Steinhardt <ps@pks.im> writes:
> When looking up a commit it may not be parsed yet. Callers that wish to
> access the fields of `struct commit` have to call `repo_parse_commit()`
> first so that it is guaranteed to be populated.
>
> We didn't yet care about doing so, because code paths that lead to
> `pick_regular_commit()` in "builtin/replay.c" already implicitly parsed
> the commits. But now that the function is exposed to outside callers
> it's quite easy to get this wrong.
>
So I was wondering, wouldn't this duplicate the call made to
`pick_regular_commit()` and end up parsing the commit twice. But seems
like down the stack in `repo_parse_commit_internal()`, we check for
`item->object.parsed` and only parse if it hasn't been already parsed.
So this change is welcome.
> Make the function easier to use by calling `repo_parse_commit()`.
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> replay.c | 3 +++
> 1 file changed, 3 insertions(+)
>
> diff --git a/replay.c b/replay.c
> index 13d75d8054..c3628d2488 100644
> --- a/replay.c
> +++ b/replay.c
> @@ -90,6 +90,9 @@ struct commit *replay_pick_regular_commit(struct repository *repo,
> struct commit *base, *replayed_base;
> struct tree *pickme_tree, *base_tree;
>
> + if (repo_parse_commit(repo, pickme))
> + return NULL;
> +
> base = pickme->parents->item;
> replayed_base = mapped_commit(replayed_commits, base, onto);
>
>
> --
> 2.51.0.700.g236ee7b076.dirty
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 690 bytes --]
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH v4 05/12] builtin: add new "history" command
2025-10-01 15:57 ` [PATCH v4 00/12] " Patrick Steinhardt
` (3 preceding siblings ...)
2025-10-01 15:57 ` [PATCH v4 04/12] replay: parse commits before dereferencing them Patrick Steinhardt
@ 2025-10-01 15:57 ` Patrick Steinhardt
2025-10-02 9:26 ` Kristoffer Haugsbakk
2025-10-14 9:07 ` Karthik Nayak
2025-10-01 15:57 ` [PATCH v4 06/12] builtin/history: implement "reword" subcommand Patrick Steinhardt
` (8 subsequent siblings)
13 siblings, 2 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-01 15:57 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren
When rewriting history via git-rebase(1) there are a couple of very
common use cases:
- The ordering of two commits should be reversed.
- A commit should be split up into two commits.
- A commit should be dropped from the history completely.
- Multiple commits should be squashed into one.
While these operations are all doable, it often feels needlessly cludgy
to do so by doing an interactive rebase, using the editor to say what
one wants, and then perform the actions. Furthermore, some operations
like splitting up a commit into two are way more involved than that and
require a whole series of commands.
Add a new "history" command to plug this gap. This command will have
several different subcommands to imperatively rewrite history for common
use cases like the above. These commands will be implemented in
subsequent commits.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
.gitignore | 1 +
Documentation/git-history.adoc | 45 ++++++++++++++++++++++++++++++++++++++++++
Documentation/meson.build | 1 +
Makefile | 1 +
builtin.h | 1 +
builtin/history.c | 22 +++++++++++++++++++++
command-list.txt | 1 +
git.c | 1 +
meson.build | 1 +
t/meson.build | 1 +
t/t3450-history.sh | 17 ++++++++++++++++
11 files changed, 92 insertions(+)
diff --git a/.gitignore b/.gitignore
index 802ce70e48..3de9f9f16f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -77,6 +77,7 @@
/git-grep
/git-hash-object
/git-help
+/git-history
/git-hook
/git-http-backend
/git-http-fetch
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
new file mode 100644
index 0000000000..1537960374
--- /dev/null
+++ b/Documentation/git-history.adoc
@@ -0,0 +1,45 @@
+git-history(1)
+==============
+
+NAME
+----
+git-history - EXPERIMENTAL: Rewrite history of the current branch
+
+SYNOPSIS
+--------
+[synopsis]
+git history [<options>]
+
+DESCRIPTION
+-----------
+
+Rewrite history by rearranging or modifying specific commits in the
+history.
+
+This command is similar to linkgit:git-rebase[1] and uses the same
+underlying machinery. You should use rebases if you either want to
+reapply a range of commits onto a different base, or interactive rebases
+if you want to edit a range of commits.
+
+Note that this command does not (yet) work with histories that contain
+merges. You should use linkgit:git-rebase[1] with the `--rebase-merges`
+flag instead.
+
+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
+
+COMMANDS
+--------
+
+This command requires a subcommand. Several subcommands are available to
+rewrite history in different ways:
+
+CONFIGURATION
+-------------
+
+include::includes/cmd-config-section-all.adoc[]
+
+include::config/sequencer.adoc[]
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Documentation/meson.build b/Documentation/meson.build
index e34965c5b0..36500879e4 100644
--- a/Documentation/meson.build
+++ b/Documentation/meson.build
@@ -64,6 +64,7 @@ manpages = {
'git-gui.adoc' : 1,
'git-hash-object.adoc' : 1,
'git-help.adoc' : 1,
+ 'git-history.adoc' : 1,
'git-hook.adoc' : 1,
'git-http-backend.adoc' : 1,
'git-http-fetch.adoc' : 1,
diff --git a/Makefile b/Makefile
index 5960c80736..4e405509e9 100644
--- a/Makefile
+++ b/Makefile
@@ -1262,6 +1262,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o
BUILTIN_OBJS += builtin/grep.o
BUILTIN_OBJS += builtin/hash-object.o
BUILTIN_OBJS += builtin/help.o
+BUILTIN_OBJS += builtin/history.o
BUILTIN_OBJS += builtin/hook.o
BUILTIN_OBJS += builtin/index-pack.o
BUILTIN_OBJS += builtin/init-db.o
diff --git a/builtin.h b/builtin.h
index 1b35565fbd..93c91d07d4 100644
--- a/builtin.h
+++ b/builtin.h
@@ -172,6 +172,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix, struc
int cmd_grep(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_hash_object(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_help(int argc, const char **argv, const char *prefix, struct repository *repo);
+int cmd_history(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_hook(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_index_pack(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_init_db(int argc, const char **argv, const char *prefix, struct repository *repo);
diff --git a/builtin/history.c b/builtin/history.c
new file mode 100644
index 0000000000..f6fe32610b
--- /dev/null
+++ b/builtin/history.c
@@ -0,0 +1,22 @@
+#include "builtin.h"
+#include "gettext.h"
+#include "parse-options.h"
+
+int cmd_history(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo UNUSED)
+{
+ const char * const usage[] = {
+ N_("git history [<options>]"),
+ NULL,
+ };
+ struct option options[] = {
+ OPT_END(),
+ };
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc)
+ usagef("unrecognized argument: %s", argv[0]);
+ return 0;
+}
diff --git a/command-list.txt b/command-list.txt
index accd3d0c4b..f9005cf459 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -115,6 +115,7 @@ git-grep mainporcelain info
git-gui mainporcelain
git-hash-object plumbingmanipulators
git-help ancillaryinterrogators complete
+git-history mainporcelain history
git-hook purehelpers
git-http-backend synchingrepositories
git-http-fetch synchelpers
diff --git a/git.c b/git.c
index d020eef021..c7c13cea67 100644
--- a/git.c
+++ b/git.c
@@ -560,6 +560,7 @@ static struct cmd_struct commands[] = {
{ "grep", cmd_grep, RUN_SETUP_GENTLY },
{ "hash-object", cmd_hash_object },
{ "help", cmd_help },
+ { "history", cmd_history, RUN_SETUP },
{ "hook", cmd_hook, RUN_SETUP },
{ "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },
{ "init", cmd_init_db },
diff --git a/meson.build b/meson.build
index c320bdba9f..7630a8fb0a 100644
--- a/meson.build
+++ b/meson.build
@@ -604,6 +604,7 @@ builtin_sources = [
'builtin/grep.c',
'builtin/hash-object.c',
'builtin/help.c',
+ 'builtin/history.c',
'builtin/hook.c',
'builtin/index-pack.c',
'builtin/init-db.c',
diff --git a/t/meson.build b/t/meson.build
index 7974795fe4..8b31eb0858 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -382,6 +382,7 @@ integration_tests = [
't3436-rebase-more-options.sh',
't3437-rebase-fixup-options.sh',
't3438-rebase-broken-files.sh',
+ 't3450-history.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
diff --git a/t/t3450-history.sh b/t/t3450-history.sh
new file mode 100755
index 0000000000..417c343d43
--- /dev/null
+++ b/t/t3450-history.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+test_description='tests for git-history command'
+
+. ./test-lib.sh
+
+test_expect_success 'does nothing without any arguments' '
+ git history >out 2>&1 &&
+ test_must_be_empty out
+'
+
+test_expect_success 'raises an error with unknown argument' '
+ test_must_fail git history garbage 2>err &&
+ test_grep "unrecognized argument: garbage" err
+'
+
+test_done
--
2.51.0.700.g236ee7b076.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH v4 05/12] builtin: add new "history" command
2025-10-01 15:57 ` [PATCH v4 05/12] builtin: add new "history" command Patrick Steinhardt
@ 2025-10-02 9:26 ` Kristoffer Haugsbakk
2025-10-14 9:07 ` Karthik Nayak
1 sibling, 0 replies; 278+ messages in thread
From: Kristoffer Haugsbakk @ 2025-10-02 9:26 UTC (permalink / raw)
To: Patrick Steinhardt, git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Elijah Newren
On Wed, Oct 1, 2025, at 17:57, Patrick Steinhardt wrote:
>[snip]
>
> While these operations are all doable, it often feels needlessly cludgy
s/cludgy/kludgey/
> to do so by doing an interactive rebase, using the editor to say what
> one wants, and then perform the actions. Furthermore, some operations
> like splitting up a commit into two are way more involved than that and
> require a whole series of commands.
>
>[snip]
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v4 05/12] builtin: add new "history" command
2025-10-01 15:57 ` [PATCH v4 05/12] builtin: add new "history" command Patrick Steinhardt
2025-10-02 9:26 ` Kristoffer Haugsbakk
@ 2025-10-14 9:07 ` Karthik Nayak
2025-10-21 11:43 ` Patrick Steinhardt
2025-10-22 3:32 ` Junio C Hamano
1 sibling, 2 replies; 278+ messages in thread
From: Karthik Nayak @ 2025-10-14 9:07 UTC (permalink / raw)
To: Patrick Steinhardt, git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren
[-- Attachment #1: Type: text/plain, Size: 3335 bytes --]
Patrick Steinhardt <ps@pks.im> writes:
> When rewriting history via git-rebase(1) there are a couple of very
> common use cases:
>
> - The ordering of two commits should be reversed.
>
> - A commit should be split up into two commits.
>
> - A commit should be dropped from the history completely.
>
> - Multiple commits should be squashed into one.
>
> While these operations are all doable, it often feels needlessly cludgy
> to do so by doing an interactive rebase, using the editor to say what
> one wants, and then perform the actions. Furthermore, some operations
> like splitting up a commit into two are way more involved than that and
> require a whole series of commands.
>
> Add a new "history" command to plug this gap. This command will have
> several different subcommands to imperatively rewrite history for common
> use cases like the above. These commands will be implemented in
Nit: s/commands/subcommands
> subsequent commits.
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
[snip]
> new file mode 100644
> index 0000000000..1537960374
> --- /dev/null
> +++ b/Documentation/git-history.adoc
> @@ -0,0 +1,45 @@
> +git-history(1)
> +==============
> +
> +NAME
> +----
> +git-history - EXPERIMENTAL: Rewrite history of the current branch
> +
> +SYNOPSIS
> +--------
> +[synopsis]
> +git history [<options>]
> +
> +DESCRIPTION
> +-----------
> +
> +Rewrite history by rearranging or modifying specific commits in the
> +history.
> +
> +This command is similar to linkgit:git-rebase[1] and uses the same
> +underlying machinery. You should use rebases if you either want to
> +reapply a range of commits onto a different base, or interactive rebases
> +if you want to edit a range of commits.
> +
>
The either..or in the last sentence is a bit confusing; as it is not an
either between 'want to reapply a range of commit onto a different base'
& 'interactive rebases'.
Perhaps we can simply s/either//
> +Note that this command does not (yet) work with histories that contain
> +merges. You should use linkgit:git-rebase[1] with the `--rebase-merges`
> +flag instead.
> +
> +THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
> +
> +COMMANDS
> +--------
> +
> +This command requires a subcommand. Several subcommands are available to
> +rewrite history in different ways:
> +
> +CONFIGURATION
> +-------------
> +
> +include::includes/cmd-config-section-all.adoc[]
> +
> +include::config/sequencer.adoc[]
> +
> +GIT
> +---
> +Part of the linkgit:git[1] suite
[snip]
> diff --git a/builtin/history.c b/builtin/history.c
> new file mode 100644
> index 0000000000..f6fe32610b
> --- /dev/null
> +++ b/builtin/history.c
> @@ -0,0 +1,22 @@
> +#include "builtin.h"
> +#include "gettext.h"
> +#include "parse-options.h"
> +
> +int cmd_history(int argc,
> + const char **argv,
> + const char *prefix,
> + struct repository *repo UNUSED)
> +{
> + const char * const usage[] = {
> + N_("git history [<options>]"),
> + NULL,
> + };
Nit: We have pointer alignment set to 'Right' in our styling guide and
also mentioned in our 'Documentation/CodingGuidelines'
When declaring pointers, the star sides with the variable
name, i.e. "char *string", not "char* string" or
"char * string". This makes it easier to understand code
like "char *string, c;".
The rest of the patch looks good!
[snip]
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 690 bytes --]
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH v4 05/12] builtin: add new "history" command
2025-10-14 9:07 ` Karthik Nayak
@ 2025-10-21 11:43 ` Patrick Steinhardt
2025-10-22 3:32 ` Junio C Hamano
1 sibling, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-21 11:43 UTC (permalink / raw)
To: Karthik Nayak
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren
On Tue, Oct 14, 2025 at 05:07:03AM -0400, Karthik Nayak wrote:
> Patrick Steinhardt <ps@pks.im> writes:
> > new file mode 100644
> > index 0000000000..1537960374
> > --- /dev/null
> > +++ b/Documentation/git-history.adoc
> > @@ -0,0 +1,45 @@
> > +git-history(1)
> > +==============
> > +
> > +NAME
> > +----
> > +git-history - EXPERIMENTAL: Rewrite history of the current branch
> > +
> > +SYNOPSIS
> > +--------
> > +[synopsis]
> > +git history [<options>]
> > +
> > +DESCRIPTION
> > +-----------
> > +
> > +Rewrite history by rearranging or modifying specific commits in the
> > +history.
> > +
> > +This command is similar to linkgit:git-rebase[1] and uses the same
> > +underlying machinery. You should use rebases if you either want to
> > +reapply a range of commits onto a different base, or interactive rebases
> > +if you want to edit a range of commits.
> > +
> >
>
> The either..or in the last sentence is a bit confusing; as it is not an
> either between 'want to reapply a range of commit onto a different base'
> & 'interactive rebases'.
>
> Perhaps we can simply s/either//
Fair.
> > diff --git a/builtin/history.c b/builtin/history.c
> > new file mode 100644
> > index 0000000000..f6fe32610b
> > --- /dev/null
> > +++ b/builtin/history.c
> > @@ -0,0 +1,22 @@
> > +#include "builtin.h"
> > +#include "gettext.h"
> > +#include "parse-options.h"
> > +
> > +int cmd_history(int argc,
> > + const char **argv,
> > + const char *prefix,
> > + struct repository *repo UNUSED)
> > +{
> > + const char * const usage[] = {
> > + N_("git history [<options>]"),
> > + NULL,
> > + };
>
> Nit: We have pointer alignment set to 'Right' in our styling guide and
> also mentioned in our 'Documentation/CodingGuidelines'
>
> When declaring pointers, the star sides with the variable
> name, i.e. "char *string", not "char* string" or
> "char * string". This makes it easier to understand code
> like "char *string, c;".
>
> The rest of the patch looks good!
This is one of the common exceptions though:
$ git grep 'const char \* const' | wc -l
186
$ git grep 'const char \*const' | wc -l
108
So when there is another keyword following the asterisk we tend to have
an additional space inbetween. We tend to only drop the space when the
next token is the variable name.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH v4 05/12] builtin: add new "history" command
2025-10-14 9:07 ` Karthik Nayak
2025-10-21 11:43 ` Patrick Steinhardt
@ 2025-10-22 3:32 ` Junio C Hamano
2025-10-22 12:12 ` Karthik Nayak
1 sibling, 1 reply; 278+ messages in thread
From: Junio C Hamano @ 2025-10-22 3:32 UTC (permalink / raw)
To: Karthik Nayak
Cc: Patrick Steinhardt, git, D. Ben Knoble, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren
Karthik Nayak <karthik.188@gmail.com> writes:
>> + const char * const usage[] = {
>> + N_("git history [<options>]"),
>> + NULL,
>> + };
>
> Nit: We have pointer alignment set to 'Right' in our styling guide and
> also mentioned in our 'Documentation/CodingGuidelines'
>
> When declaring pointers, the star sides with the variable
> name, i.e. "char *string", not "char* string" or
> "char * string". This makes it easier to understand code
> like "char *string, c;".
But there is nothing specified for an asterisk that cannot side with
variable name, like the one we see above. I _think_ the "space on
both sides" is the prevalent style, but I do not know (although I
suspect you do---as the person with most changes in it) what (y)our
clang format configuration wants to do. Can you make sure the tool
suggests the style that matches the prevailing style?
Thanks.
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH v4 05/12] builtin: add new "history" command
2025-10-22 3:32 ` Junio C Hamano
@ 2025-10-22 12:12 ` Karthik Nayak
0 siblings, 0 replies; 278+ messages in thread
From: Karthik Nayak @ 2025-10-22 12:12 UTC (permalink / raw)
To: Junio C Hamano
Cc: Patrick Steinhardt, git, D. Ben Knoble, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren
[-- Attachment #1: Type: text/plain, Size: 1415 bytes --]
Junio C Hamano <gitster@pobox.com> writes:
> Karthik Nayak <karthik.188@gmail.com> writes:
>
>>> + const char * const usage[] = {
>>> + N_("git history [<options>]"),
>>> + NULL,
>>> + };
>>
>> Nit: We have pointer alignment set to 'Right' in our styling guide and
>> also mentioned in our 'Documentation/CodingGuidelines'
>>
>> When declaring pointers, the star sides with the variable
>> name, i.e. "char *string", not "char* string" or
>> "char * string". This makes it easier to understand code
>> like "char *string, c;".
>
> But there is nothing specified for an asterisk that cannot side with
> variable name, like the one we see above. I _think_ the "space on
> both sides" is the prevalent style, but I do not know (although I
> suspect you do---as the person with most changes in it) what (y)our
> clang format configuration wants to do. Can you make sure the tool
> suggests the style that matches the prevailing style?
>
> Thanks.
I looked into this, and unfortunately it [1] doesn't support such
granularity.
So for something like `const char * const usage`, it only cares about
the alignment of the pointer with respect to the tokens surrounding it.
With our current setting of `PointerAlignment: Right`, this means it
would expect to have `const char *const usage` which is not the
prevalent style.
[1]: https://clang.llvm.org/docs/ClangFormatStyleOptions.html#pointeralignment
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 690 bytes --]
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH v4 06/12] builtin/history: implement "reword" subcommand
2025-10-01 15:57 ` [PATCH v4 00/12] " Patrick Steinhardt
` (4 preceding siblings ...)
2025-10-01 15:57 ` [PATCH v4 05/12] builtin: add new "history" command Patrick Steinhardt
@ 2025-10-01 15:57 ` Patrick Steinhardt
2025-10-14 11:04 ` Karthik Nayak
2025-10-01 15:57 ` [PATCH v4 07/12] add-patch: split out header from "add-interactive.h" Patrick Steinhardt
` (7 subsequent siblings)
13 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-01 15:57 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren
Implement a new "reword" subcommand for git-history(1). This subcommand
is essentially the same as if a user performed an interactive rebase
with a single commit changed to use the "reword" verb.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-history.adoc | 7 +
builtin/history.c | 375 ++++++++++++++++++++++++++++++++++++++++-
t/meson.build | 1 +
t/t3450-history.sh | 6 +-
t/t3451-history-reword.sh | 202 ++++++++++++++++++++++
5 files changed, 584 insertions(+), 7 deletions(-)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index 1537960374..b55babe206 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -9,6 +9,7 @@ SYNOPSIS
--------
[synopsis]
git history [<options>]
+git history reword [<options>] <commit>
DESCRIPTION
-----------
@@ -33,6 +34,12 @@ COMMANDS
This command requires a subcommand. Several subcommands are available to
rewrite history in different ways:
+`reword <commit> [--message=<message>]`::
+ Rewrite the commit message of the specified commit. All the other
+ details of this commit remain unchanged. If no commit message is
+ provided, then this command will spawn an editor with the current
+ message of that commit.
+
CONFIGURATION
-------------
diff --git a/builtin/history.c b/builtin/history.c
index f6fe32610b..7b2a0023e8 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -1,22 +1,389 @@
+#define USE_THE_REPOSITORY_VARIABLE
+
#include "builtin.h"
+#include "commit-reach.h"
+#include "commit.h"
+#include "config.h"
+#include "editor.h"
+#include "environment.h"
#include "gettext.h"
+#include "hex.h"
+#include "oidmap.h"
#include "parse-options.h"
+#include "refs.h"
+#include "replay.h"
+#include "reset.h"
+#include "revision.h"
+#include "sequencer.h"
+#include "strvec.h"
+#include "tree.h"
+#include "wt-status.h"
+
+static int collect_commits(struct repository *repo,
+ struct commit *old_commit,
+ struct commit *new_commit,
+ struct strvec *out)
+{
+ struct setup_revision_opt revision_opts = {
+ .assume_dashdash = 1,
+ };
+ struct strvec revisions = STRVEC_INIT;
+ struct commit_list *from_list = NULL;
+ struct commit *child;
+ struct rev_info rev = { 0 };
+ int ret;
+
+ /*
+ * Check that the old commit actually is an ancestor of HEAD. If not
+ * the whole request becomes nonsensical.
+ */
+ if (old_commit) {
+ commit_list_insert(old_commit, &from_list);
+ if (!repo_is_descendant_of(repo, new_commit, from_list)) {
+ ret = error(_("commit must be reachable from current HEAD commit"));
+ goto out;
+ }
+ }
+
+ repo_init_revisions(repo, &rev, NULL);
+ strvec_push(&revisions, "");
+ strvec_push(&revisions, oid_to_hex(&new_commit->object.oid));
+ if (old_commit)
+ strvec_pushf(&revisions, "^%s", oid_to_hex(&old_commit->object.oid));
+
+ setup_revisions_from_strvec(&revisions, &rev, &revision_opts);
+ if (revisions.nr != 1 || prepare_revision_walk(&rev)) {
+ ret = error(_("revision walk setup failed"));
+ goto out;
+ }
+
+ while ((child = get_revision(&rev))) {
+ if (old_commit && !child->parents)
+ BUG("revision walk did not find child commit");
+ if (child->parents && child->parents->next) {
+ ret = error(_("cannot rearrange commit history with merges"));
+ goto out;
+ }
+
+ strvec_push(out, oid_to_hex(&child->object.oid));
+
+ if (child->parents && old_commit &&
+ commit_list_contains(old_commit, child->parents))
+ break;
+ }
+
+ /*
+ * Revisions are in newest-order-first. We have to reverse the
+ * array though so that we pick the oldest commits first.
+ */
+ for (size_t i = 0, j = out->nr - 1; i < j; i++, j--)
+ SWAP(out->v[i], out->v[j]);
+
+ ret = 0;
+
+out:
+ free_commit_list(from_list);
+ strvec_clear(&revisions);
+ release_revisions(&rev);
+ reset_revision_walk();
+ return ret;
+}
+
+static void replace_commits(struct strvec *commits,
+ const struct object_id *commit_to_replace,
+ const struct object_id *replacements,
+ size_t replacements_nr)
+{
+ char commit_to_replace_oid[GIT_MAX_HEXSZ + 1];
+ struct strvec replacement_oids = STRVEC_INIT;
+ bool found = false;
+
+ oid_to_hex_r(commit_to_replace_oid, commit_to_replace);
+ for (size_t i = 0; i < replacements_nr; i++)
+ strvec_push(&replacement_oids, oid_to_hex(&replacements[i]));
+
+ for (size_t i = 0; i < commits->nr; i++) {
+ if (strcmp(commits->v[i], commit_to_replace_oid))
+ continue;
+ strvec_splice(commits, i, 1, replacement_oids.v, replacement_oids.nr);
+ found = true;
+ break;
+ }
+ if (!found)
+ BUG("could not find commit to replace");
+
+ strvec_clear(&replacement_oids);
+}
+
+static int apply_commits(struct repository *repo,
+ const struct strvec *commits,
+ struct commit *onto,
+ struct commit *orig_head,
+ const char *action)
+{
+ struct reset_head_opts reset_opts = { 0 };
+ struct merge_options merge_opts = { 0 };
+ struct merge_result result = { 0 };
+ struct strbuf buf = STRBUF_INIT;
+ kh_oid_map_t *replayed_commits;
+ int ret;
+
+ replayed_commits = kh_init_oid_map();
+
+ init_basic_merge_options(&merge_opts, repo);
+ merge_opts.show_rename_progress = 0;
+
+ for (size_t i = 0; i < commits->nr; i++) {
+ struct object_id commit_id;
+ struct commit *commit;
+ const char *end;
+ int hash_result;
+ khint_t pos;
+
+ if (parse_oid_hex_algop(commits->v[i], &commit_id, &end,
+ repo->hash_algo)) {
+ ret = error(_("invalid object ID: %s"), commits->v[i]);
+ goto out;
+ }
+
+ commit = lookup_commit(repo, &commit_id);
+ if (!commit || repo_parse_commit(repo, commit)) {
+ ret = error(_("failed to look up commit: %s"), oid_to_hex(&commit_id));
+ goto out;
+ }
+
+ if (!onto) {
+ onto = commit;
+ result.clean = 1;
+ result.tree = repo_get_commit_tree(repo, commit);
+ } else {
+ onto = replay_pick_regular_commit(repo, commit, replayed_commits,
+ onto, &merge_opts, &result);
+ if (!onto)
+ break;
+ }
+
+ pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hash_result);
+ if (hash_result == 0) {
+ ret = error(_("duplicate rewritten commit: %s\n"),
+ oid_to_hex(&commit->object.oid));
+ goto out;
+ }
+ kh_value(replayed_commits, pos) = onto;
+ }
+
+ if (!result.clean) {
+ ret = error(_("could not merge"));
+ goto out;
+ }
+
+ reset_opts.oid = &onto->object.oid;
+ strbuf_addf(&buf, "%s: switch to rewritten %s", action, oid_to_hex(reset_opts.oid));
+ reset_opts.flags = RESET_HEAD_REFS_ONLY | RESET_ORIG_HEAD;
+ reset_opts.orig_head = &orig_head->object.oid;
+ reset_opts.default_reflog_action = action;
+ if (reset_head(repo, &reset_opts) < 0) {
+ ret = error(_("could not switch to %s"), oid_to_hex(reset_opts.oid));
+ goto out;
+ }
+
+ ret = 0;
+
+out:
+ kh_destroy_oid_map(replayed_commits);
+ merge_finalize(&merge_opts, &result);
+ strbuf_release(&buf);
+ return ret;
+}
+
+static void change_data_free(void *util, const char *str UNUSED)
+{
+ struct wt_status_change_data *d = util;
+ free(d->rename_source);
+ free(d);
+}
+
+static int fill_commit_message(struct repository *repo,
+ const struct object_id *old_tree,
+ const struct object_id *new_tree,
+ const char *default_message,
+ const char *provided_message,
+ const char *action,
+ struct strbuf *out)
+{
+ if (!provided_message) {
+ const char *path = git_path_commit_editmsg();
+ const char *hint =
+ _("Please enter the commit message for the %s changes. Lines starting\n"
+ "with '%s' will be kept; you may remove them yourself if you want to.\n");
+ struct wt_status s;
+
+ strbuf_addstr(out, default_message);
+ strbuf_addch(out, '\n');
+ strbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);
+ write_file_buf(path, out->buf, out->len);
+
+ wt_status_prepare(repo, &s);
+ FREE_AND_NULL(s.branch);
+ s.ahead_behind_flags = AHEAD_BEHIND_QUICK;
+ s.commit_template = 1;
+ s.colopts = 0;
+ s.display_comment_prefix = 1;
+ s.hints = 0;
+ s.use_color = 0;
+ s.whence = FROM_COMMIT;
+ s.committable = 1;
+
+ s.fp = fopen(git_path_commit_editmsg(), "a");
+ if (!s.fp)
+ return error_errno(_("could not open '%s'"), git_path_commit_editmsg());
+
+ wt_status_collect_changes_trees(&s, old_tree, new_tree);
+ wt_status_print(&s);
+ wt_status_collect_free_buffers(&s);
+ string_list_clear_func(&s.change, change_data_free);
+
+ strbuf_reset(out);
+ if (launch_editor(path, out, NULL)) {
+ fprintf(stderr, _("Please supply the message using the -m option.\n"));
+ return -1;
+ }
+ strbuf_stripspace(out, comment_line_str);
+ } else {
+ strbuf_addstr(out, provided_message);
+ }
+
+ cleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);
+
+ if (!out->len) {
+ fprintf(stderr, _("Aborting commit due to empty commit message.\n"));
+ return -1;
+ }
+
+ return 0;
+}
+
+static int cmd_history_reword(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo)
+{
+ const char * const usage[] = {
+ N_("git history reword [<options>] <commit>"),
+ NULL,
+ };
+ const char *commit_message = NULL;
+ struct option options[] = {
+ OPT_STRING('m', "message", &commit_message, N_("message"), N_("commit message")),
+ OPT_END(),
+ };
+ struct strbuf final_message = STRBUF_INIT;
+ struct commit *original_commit, *parent, *head;
+ struct strvec commits = STRVEC_INIT;
+ struct object_id parent_tree_oid, original_commit_tree_oid;
+ struct object_id rewritten_commit;
+ const char *original_message, *original_body, *ptr;
+ char *original_author = NULL;
+ size_t len;
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc != 1) {
+ ret = error(_("command expects a single revision"));
+ goto out;
+ }
+ repo_config(repo, git_default_config, NULL);
+
+ original_commit = lookup_commit_reference_by_name(argv[0]);
+ if (!original_commit) {
+ ret = error(_("commit to be reworded cannot be found: %s"), argv[0]);
+ goto out;
+ }
+ if (repo_parse_commit(repo, original_commit)) {
+ ret = error(_("unable to parse commit %s"),
+ oid_to_hex(&original_commit->object.oid));
+ goto out;
+ }
+ original_commit_tree_oid = repo_get_commit_tree(repo, original_commit)->object.oid;
+
+ parent = original_commit->parents ? original_commit->parents->item : NULL;
+ if (parent) {
+ if (repo_parse_commit(repo, parent)) {
+ ret = error(_("unable to parse commit %s"),
+ oid_to_hex(&parent->object.oid));
+ goto out;
+ }
+ parent_tree_oid = repo_get_commit_tree(repo, parent)->object.oid;
+ } else {
+ oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
+ }
+
+ head = lookup_commit_reference_by_name("HEAD");
+ if (!head) {
+ ret = error(_("could not resolve HEAD to a commit"));
+ goto out;
+ }
+
+ /*
+ * Collect the list of commits that we'll have to reapply now already.
+ * This ensures that we'll abort early on in case the range of commits
+ * contains merges, which we do not yet handle.
+ */
+ ret = collect_commits(repo, parent, head, &commits);
+ if (ret < 0)
+ goto out;
+
+ /* We retain authorship of the original commit. */
+ original_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);
+ ptr = find_commit_header(original_message, "author", &len);
+ if (ptr)
+ original_author = xmemdupz(ptr, len);
+ find_commit_subject(original_message, &original_body);
+
+ ret = fill_commit_message(repo, &parent_tree_oid, &original_commit_tree_oid,
+ original_body, commit_message, "reworded", &final_message);
+ if (ret < 0)
+ goto out;
+
+ ret = commit_tree(final_message.buf, final_message.len,
+ &repo_get_commit_tree(repo, original_commit)->object.oid,
+ original_commit->parents, &rewritten_commit, original_author, NULL);
+ if (ret < 0) {
+ ret = error(_("failed writing reworded commit"));
+ goto out;
+ }
+
+ replace_commits(&commits, &original_commit->object.oid, &rewritten_commit, 1);
+
+ ret = apply_commits(repo, &commits, parent, head, "reword");
+ if (ret < 0)
+ goto out;
+
+ ret = 0;
+
+out:
+ strbuf_release(&final_message);
+ strvec_clear(&commits);
+ free(original_author);
+ return ret;
+}
int cmd_history(int argc,
const char **argv,
const char *prefix,
- struct repository *repo UNUSED)
+ struct repository *repo)
{
const char * const usage[] = {
N_("git history [<options>]"),
+ N_("git history reword [<options>] <commit>"),
NULL,
};
+ parse_opt_subcommand_fn *fn = NULL;
struct option options[] = {
+ OPT_SUBCOMMAND("reword", &fn, cmd_history_reword),
OPT_END(),
};
argc = parse_options(argc, argv, prefix, options, usage, 0);
- if (argc)
- usagef("unrecognized argument: %s", argv[0]);
- return 0;
+ return fn(argc, argv, prefix, repo);
}
diff --git a/t/meson.build b/t/meson.build
index 8b31eb0858..2a74243202 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -383,6 +383,7 @@ integration_tests = [
't3437-rebase-fixup-options.sh',
't3438-rebase-broken-files.sh',
't3450-history.sh',
+ 't3451-history-reword.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
diff --git a/t/t3450-history.sh b/t/t3450-history.sh
index 417c343d43..f513463b92 100755
--- a/t/t3450-history.sh
+++ b/t/t3450-history.sh
@@ -5,13 +5,13 @@ test_description='tests for git-history command'
. ./test-lib.sh
test_expect_success 'does nothing without any arguments' '
- git history >out 2>&1 &&
- test_must_be_empty out
+ test_must_fail git history 2>err &&
+ test_grep "need a subcommand" err
'
test_expect_success 'raises an error with unknown argument' '
test_must_fail git history garbage 2>err &&
- test_grep "unrecognized argument: garbage" err
+ test_grep "unknown subcommand: .garbage." err
'
test_done
diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
new file mode 100755
index 0000000000..84d203643e
--- /dev/null
+++ b/t/t3451-history-reword.sh
@@ -0,0 +1,202 @@
+#!/bin/sh
+
+test_description='tests for git-history reword subcommand'
+
+. ./test-lib.sh
+
+test_expect_success 'refuses to work with merge commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ git branch branch &&
+ test_commit ours &&
+ git switch branch &&
+ test_commit theirs &&
+ git switch - &&
+ git merge theirs &&
+ test_must_fail git history reword HEAD~ 2>err &&
+ test_grep "cannot rearrange commit history with merges" err &&
+ test_must_fail git history reword HEAD 2>err &&
+ test_grep "cannot rearrange commit history with merges" err
+ )
+'
+
+test_expect_success 'can reword tip of a branch' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ git symbolic-ref HEAD >expect &&
+ git history reword -m "third reworded" HEAD &&
+ git symbolic-ref HEAD >actual &&
+ test_cmp expect actual &&
+
+ cat >expect <<-EOF &&
+ third reworded
+ second
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'can reword commit in the middle' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ git symbolic-ref HEAD >expect &&
+ git history reword -m "second reworded" HEAD~ &&
+ git symbolic-ref HEAD >actual &&
+ test_cmp expect actual &&
+
+ cat >expect <<-EOF &&
+ third
+ second reworded
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'can reword root commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ git history reword -m "first reworded" HEAD~2 &&
+
+ cat >expect <<-EOF &&
+ third
+ second
+ first reworded
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'can use editor to rewrite commit message' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+
+ write_script fake-editor.sh <<-\EOF &&
+ cp "$1" . &&
+ printf "\namend a comment\n" >>"$1"
+ EOF
+ test_set_editor "$(pwd)"/fake-editor.sh &&
+ git history reword HEAD &&
+
+ cat >expect <<-EOF &&
+ first
+
+ # Please enter the commit message for the reworded changes. Lines starting
+ # with ${SQ}#${SQ} will be kept; you may remove them yourself if you want to.
+ # Changes to be committed:
+ # new file: first.t
+ #
+ EOF
+ test_cmp expect COMMIT_EDITMSG &&
+
+ cat >expect <<-EOF &&
+ first
+
+ amend a comment
+
+ EOF
+ git log --format=%B >actual &&
+ test_cmp expect actual
+ )
+'
+
+# For now, git-history(1) does not yet execute any hooks. This is subject to
+# change in the future, and if it does this test here is expected to start
+# failing. In other words, this test is not an endorsement of the current
+# status quo.
+test_expect_success 'hooks are executed for rewritten commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ write_script .git/hooks/prepare-commit-msg <<-EOF &&
+ touch "$(pwd)/hooks.log
+ EOF
+ write_script .git/hooks/post-commit <<-EOF &&
+ touch "$(pwd)/hooks.log
+ EOF
+ write_script .git/hooks/post-rewrite <<-EOF &&
+ touch "$(pwd)/hooks.log
+ EOF
+
+ git history reword -m "second reworded" HEAD~ &&
+
+ cat >expect <<-EOF &&
+ third
+ second reworded
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual &&
+
+ test_path_is_missing hooks.log
+ )
+'
+
+test_expect_success 'aborts with empty commit message' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+
+ test_must_fail git history reword -m "" HEAD 2>err &&
+ test_grep "Aborting commit due to empty commit message." err
+ )
+'
+
+test_expect_success 'retains changes in the worktree and index' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch a b &&
+ git add . &&
+ git commit -m "initial commit" &&
+ echo foo >a &&
+ echo bar >b &&
+ git add b &&
+ git history reword HEAD -m message &&
+ cat >expect <<-\EOF &&
+ M a
+ M b
+ ?? actual
+ ?? expect
+ EOF
+ git status --porcelain >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_done
--
2.51.0.700.g236ee7b076.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH v4 06/12] builtin/history: implement "reword" subcommand
2025-10-01 15:57 ` [PATCH v4 06/12] builtin/history: implement "reword" subcommand Patrick Steinhardt
@ 2025-10-14 11:04 ` Karthik Nayak
2025-10-21 11:43 ` Patrick Steinhardt
0 siblings, 1 reply; 278+ messages in thread
From: Karthik Nayak @ 2025-10-14 11:04 UTC (permalink / raw)
To: Patrick Steinhardt, git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren
[-- Attachment #1: Type: text/plain, Size: 14806 bytes --]
Patrick Steinhardt <ps@pks.im> writes:
> Implement a new "reword" subcommand for git-history(1). This subcommand
> is essentially the same as if a user performed an interactive rebase
> with a single commit changed to use the "reword" verb.
>
[snip]
> diff --git a/builtin/history.c b/builtin/history.c
> index f6fe32610b..7b2a0023e8 100644
> --- a/builtin/history.c
> +++ b/builtin/history.c
> @@ -1,22 +1,389 @@
> +#define USE_THE_REPOSITORY_VARIABLE
> +
> #include "builtin.h"
> +#include "commit-reach.h"
> +#include "commit.h"
> +#include "config.h"
> +#include "editor.h"
> +#include "environment.h"
> #include "gettext.h"
> +#include "hex.h"
> +#include "oidmap.h"
Nit: This can be dropped, perhaps needed in a future patch?
> #include "parse-options.h"
> +#include "refs.h"
> +#include "replay.h"
> +#include "reset.h"
> +#include "revision.h"
> +#include "sequencer.h"
> +#include "strvec.h"
> +#include "tree.h"
> +#include "wt-status.h"
> +
> +static int collect_commits(struct repository *repo,
> + struct commit *old_commit,
> + struct commit *new_commit,
> + struct strvec *out)
> +{
> + struct setup_revision_opt revision_opts = {
> + .assume_dashdash = 1,
> + };
> + struct strvec revisions = STRVEC_INIT;
> + struct commit_list *from_list = NULL;
> + struct commit *child;
> + struct rev_info rev = { 0 };
> + int ret;
> +
> + /*
> + * Check that the old commit actually is an ancestor of HEAD. If not
> + * the whole request becomes nonsensical.
> + */
Missing space here
> + if (old_commit) {
> + commit_list_insert(old_commit, &from_list);
> + if (!repo_is_descendant_of(repo, new_commit, from_list)) {
> + ret = error(_("commit must be reachable from current HEAD commit"));
> + goto out;
> + }
> + }
Makes sense. There is an inherent assumption using the 'git history'
command that you want to modify the history of the current reference.
One question, wouldn't it make sense to parse and check that the commit
to be reworded should be checked to be a descendant of HEAD earlier on
in `cmd_history_reword()`?
This would ensure this function `collect_commits()` doesn't worry about
how it is meant to be used, and simply worries about collecting commits.
> + repo_init_revisions(repo, &rev, NULL);
> + strvec_push(&revisions, "");
> + strvec_push(&revisions, oid_to_hex(&new_commit->object.oid));
> + if (old_commit)
> + strvec_pushf(&revisions, "^%s", oid_to_hex(&old_commit->object.oid));
> +
> + setup_revisions_from_strvec(&revisions, &rev, &revision_opts);
> + if (revisions.nr != 1 || prepare_revision_walk(&rev)) {
> + ret = error(_("revision walk setup failed"));
> + goto out;
> + }
> +
> + while ((child = get_revision(&rev))) {
> + if (old_commit && !child->parents)
> + BUG("revision walk did not find child commit");
> + if (child->parents && child->parents->next) {
> + ret = error(_("cannot rearrange commit history with merges"));
> + goto out;
> + }
> +
> + strvec_push(out, oid_to_hex(&child->object.oid));
> +
> + if (child->parents && old_commit &&
> + commit_list_contains(old_commit, child->parents))
> + break;
> + }
> +
Okay makes sense here, we collect all the commits we break as soon as we
reach old_commit. Since we check for merges at the start of the loop,
the history should be linear.
[snip]
> +static void replace_commits(struct strvec *commits,
> + const struct object_id *commit_to_replace,
> + const struct object_id *replacements,
> + size_t replacements_nr)
> +{
> + char commit_to_replace_oid[GIT_MAX_HEXSZ + 1];
> + struct strvec replacement_oids = STRVEC_INIT;
> + bool found = false;
> +
> + oid_to_hex_r(commit_to_replace_oid, commit_to_replace);
> + for (size_t i = 0; i < replacements_nr; i++)
> + strvec_push(&replacement_oids, oid_to_hex(&replacements[i]));
> +
> + for (size_t i = 0; i < commits->nr; i++) {
> + if (strcmp(commits->v[i], commit_to_replace_oid))
> + continue;
> + strvec_splice(commits, i, 1, replacement_oids.v, replacement_oids.nr);
> + found = true;
> + break;
> + }
> + if (!found)
> + BUG("could not find commit to replace");
> +
> + strvec_clear(&replacement_oids);
> +}
So this basically goes over the commits that we pass and replaces a
single commit with a set of commits. In our case that would be
C0 C1 C2 ... CN
└─(reword) └─(HEAD)
↓ (rewrites to)
R0 R1 R2 ... RN C1 C2 ... CN
└────────────┘ └─(HEAD)
(new commits)
Makes sense. I assume we use a list of replacements here for future
commands.
> +static int apply_commits(struct repository *repo,
> + const struct strvec *commits,
> + struct commit *onto,
> + struct commit *orig_head,
> + const char *action)
> +{
> + struct reset_head_opts reset_opts = { 0 };
> + struct merge_options merge_opts = { 0 };
> + struct merge_result result = { 0 };
> + struct strbuf buf = STRBUF_INIT;
> + kh_oid_map_t *replayed_commits;
> + int ret;
> +
> + replayed_commits = kh_init_oid_map();
> +
So this is used to the pass the replayed_commits list to the replay
mechanism so that the appropriate base commit is selected.
> + init_basic_merge_options(&merge_opts, repo);
> + merge_opts.show_rename_progress = 0;
> +
> + for (size_t i = 0; i < commits->nr; i++) {
> + struct object_id commit_id;
> + struct commit *commit;
> + const char *end;
> + int hash_result;
> + khint_t pos;
> +
> + if (parse_oid_hex_algop(commits->v[i], &commit_id, &end,
> + repo->hash_algo)) {
> + ret = error(_("invalid object ID: %s"), commits->v[i]);
> + goto out;
> + }
> +
> + commit = lookup_commit(repo, &commit_id);
> + if (!commit || repo_parse_commit(repo, commit)) {
> + ret = error(_("failed to look up commit: %s"), oid_to_hex(&commit_id));
> + goto out;
> + }
> +
> + if (!onto) {
> + onto = commit;
> + result.clean = 1;
> + result.tree = repo_get_commit_tree(repo, commit);
So if there is no onto, we're starting at the root commit and expect a
clean merge.
> + } else {
> + onto = replay_pick_regular_commit(repo, commit, replayed_commits,
> + onto, &merge_opts, &result);
> + if (!onto)
> + break;
Else we will replay the current commit onto the prev 'onto' commit.
> + }
> +
> + pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hash_result);
> + if (hash_result == 0) {
> + ret = error(_("duplicate rewritten commit: %s\n"),
> + oid_to_hex(&commit->object.oid));
> + goto out;
> + }
> + kh_value(replayed_commits, pos) = onto;
> + }
> +
> + if (!result.clean) {
> + ret = error(_("could not merge"));
> + goto out;
> + }
> +
> + reset_opts.oid = &onto->object.oid;
> + strbuf_addf(&buf, "%s: switch to rewritten %s", action, oid_to_hex(reset_opts.oid));
> + reset_opts.flags = RESET_HEAD_REFS_ONLY | RESET_ORIG_HEAD;
> + reset_opts.orig_head = &orig_head->object.oid;
> + reset_opts.default_reflog_action = action;
> + if (reset_head(repo, &reset_opts) < 0) {
> + ret = error(_("could not switch to %s"), oid_to_hex(reset_opts.oid));
> + goto out;
> + }
> +
We finally update the HEAD reference also.
> + ret = 0;
> +
> +out:
> + kh_destroy_oid_map(replayed_commits);
> + merge_finalize(&merge_opts, &result);
> + strbuf_release(&buf);
> + return ret;
> +}
> +
> +static void change_data_free(void *util, const char *str UNUSED)
> +{
> + struct wt_status_change_data *d = util;
> + free(d->rename_source);
> + free(d);
> +}
> +
> +static int fill_commit_message(struct repository *repo,
> + const struct object_id *old_tree,
> + const struct object_id *new_tree,
> + const char *default_message,
> + const char *provided_message,
> + const char *action,
> + struct strbuf *out)
> +{
> + if (!provided_message) {
> + const char *path = git_path_commit_editmsg();
> + const char *hint =
> + _("Please enter the commit message for the %s changes. Lines starting\n"
> + "with '%s' will be kept; you may remove them yourself if you want to.\n");
Shouldn't this be s/kept/removed? Also this line needs to be aligned.
> + struct wt_status s;
> +
> + strbuf_addstr(out, default_message);
> + strbuf_addch(out, '\n');
> + strbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);
> + write_file_buf(path, out->buf, out->len);
> +
> + wt_status_prepare(repo, &s);
> + FREE_AND_NULL(s.branch);
> + s.ahead_behind_flags = AHEAD_BEHIND_QUICK;
> + s.commit_template = 1;
> + s.colopts = 0;
> + s.display_comment_prefix = 1;
> + s.hints = 0;
> + s.use_color = 0;
> + s.whence = FROM_COMMIT;
> + s.committable = 1;
> +
> + s.fp = fopen(git_path_commit_editmsg(), "a");
> + if (!s.fp)
> + return error_errno(_("could not open '%s'"), git_path_commit_editmsg());
> +
> + wt_status_collect_changes_trees(&s, old_tree, new_tree);
> + wt_status_print(&s);
> + wt_status_collect_free_buffers(&s);
> + string_list_clear_func(&s.change, change_data_free);
> +
Nice, so we show the user the diff tree and then ask them to modify the
existing message as they want.
> + strbuf_reset(out);
> + if (launch_editor(path, out, NULL)) {
> + fprintf(stderr, _("Please supply the message using the -m option.\n"));
> + return -1;
> + }
> + strbuf_stripspace(out, comment_line_str);
> + } else {
> + strbuf_addstr(out, provided_message);
> + }
> +
> + cleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);
> +
> + if (!out->len) {
> + fprintf(stderr, _("Aborting commit due to empty commit message.\n"));
> + return -1;
> + }
> +
> + return 0;
> +}
> +
> +static int cmd_history_reword(int argc,
> + const char **argv,
> + const char *prefix,
> + struct repository *repo)
> +{
> + const char * const usage[] = {
Same as the prev commit, this should be '*const'.
> + N_("git history reword [<options>] <commit>"),
> + NULL,
> + };
> + const char *commit_message = NULL;
> + struct option options[] = {
> + OPT_STRING('m', "message", &commit_message, N_("message"), N_("commit message")),
> + OPT_END(),
> + };
> + struct strbuf final_message = STRBUF_INIT;
> + struct commit *original_commit, *parent, *head;
> + struct strvec commits = STRVEC_INIT;
> + struct object_id parent_tree_oid, original_commit_tree_oid;
> + struct object_id rewritten_commit;
> + const char *original_message, *original_body, *ptr;
> + char *original_author = NULL;
> + size_t len;
> + int ret;
> +
> + argc = parse_options(argc, argv, prefix, options, usage, 0);
> + if (argc != 1) {
> + ret = error(_("command expects a single revision"));
> + goto out;
> + }
> + repo_config(repo, git_default_config, NULL);
> +
Right. We only expect something like 'git history reword @~10', so we
allow only one commit to be re-worded at a given time.
> + original_commit = lookup_commit_reference_by_name(argv[0]);
> + if (!original_commit) {
> + ret = error(_("commit to be reworded cannot be found: %s"), argv[0]);
> + goto out;
> + }
So here we want to parse the commit-ish name to find the commit.
> + if (repo_parse_commit(repo, original_commit)) {
> + ret = error(_("unable to parse commit %s"),
> + oid_to_hex(&original_commit->object.oid));
> + goto out;
> + }
Isn't this already done as part of
`lookup_commit_reference_by_name_gently()` which is called by
`lookup_commit_reference_by_name()` ?
> + original_commit_tree_oid = repo_get_commit_tree(repo, original_commit)->object.oid;
> +
Okay we get the tree of the commit, I assume this is used to create the
new commit and replay the children on top of it.
> + parent = original_commit->parents ? original_commit->parents->item : NULL;
> + if (parent) {
> + if (repo_parse_commit(repo, parent)) {
> + ret = error(_("unable to parse commit %s"),
> + oid_to_hex(&parent->object.oid));
> + goto out;
> + }
> + parent_tree_oid = repo_get_commit_tree(repo, parent)->object.oid;
> + } else {
> + oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
> + }
> +
> + head = lookup_commit_reference_by_name("HEAD");
> + if (!head) {
> + ret = error(_("could not resolve HEAD to a commit"));
> + goto out;
> + }
> +
> + /*
> + * Collect the list of commits that we'll have to reapply now already.
> + * This ensures that we'll abort early on in case the range of commits
> + * contains merges, which we do not yet handle.
> + */
> + ret = collect_commits(repo, parent, head, &commits);
> + if (ret < 0)
> + goto out;
> +
The user is currently at HEAD and wishes to reword a commit.
PN ... P2 P1 C0 C1 ... CN
└─(reword) └─(HEAD)
So we want to re-word C0, so we need to collect P1..CN.
> + /* We retain authorship of the original commit. */
> + original_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);
> + ptr = find_commit_header(original_message, "author", &len);
> + if (ptr)
> + original_author = xmemdupz(ptr, len);
> + find_commit_subject(original_message, &original_body);
> +
> + ret = fill_commit_message(repo, &parent_tree_oid, &original_commit_tree_oid,
> + original_body, commit_message, "reworded", &final_message);
> + if (ret < 0)
> + goto out;
We obtain the new message from the user.
> +
> + ret = commit_tree(final_message.buf, final_message.len,
> + &repo_get_commit_tree(repo, original_commit)->object.oid,
Can't we use original_commit_tree_oid here?
> + original_commit->parents, &rewritten_commit, original_author, NULL);
> + if (ret < 0) {
> + ret = error(_("failed writing reworded commit"));
> + goto out;
> + }
> +
> + replace_commits(&commits, &original_commit->object.oid, &rewritten_commit, 1);
> +
Okay we replace the commits we obtained with the new rewritten commit.
> + ret = apply_commits(repo, &commits, parent, head, "reword");
> + if (ret < 0)
> + goto out;
> +
> + ret = 0;
> +
So we're now asking to replay the commits onto parent and update HEAD.
Looks good.
> +out:
> + strbuf_release(&final_message);
> + strvec_clear(&commits);
> + free(original_author);
> + return ret;
> +}
>
> int cmd_history(int argc,
> const char **argv,
> const char *prefix,
> - struct repository *repo UNUSED)
> + struct repository *repo)
> {
> const char * const usage[] = {
> N_("git history [<options>]"),
> + N_("git history reword [<options>] <commit>"),
This string is used twice, perhaps we move it to a macro?
[snip]
The tests looked good too!
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 690 bytes --]
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH v4 06/12] builtin/history: implement "reword" subcommand
2025-10-14 11:04 ` Karthik Nayak
@ 2025-10-21 11:43 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-21 11:43 UTC (permalink / raw)
To: Karthik Nayak
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren
On Tue, Oct 14, 2025 at 07:04:06AM -0400, Karthik Nayak wrote:
> Patrick Steinhardt <ps@pks.im> writes:
> > diff --git a/builtin/history.c b/builtin/history.c
> > index f6fe32610b..7b2a0023e8 100644
> > --- a/builtin/history.c
> > +++ b/builtin/history.c
> > @@ -1,22 +1,389 @@
> > +#define USE_THE_REPOSITORY_VARIABLE
> > +
> > #include "builtin.h"
> > +#include "commit-reach.h"
> > +#include "commit.h"
> > +#include "config.h"
> > +#include "editor.h"
> > +#include "environment.h"
> > #include "gettext.h"
> > +#include "hex.h"
> > +#include "oidmap.h"
>
> Nit: This can be dropped, perhaps needed in a future patch?
Yeah, it's indeed needed in a subsequent patch. Let me move the import
around.
> > #include "parse-options.h"
> > +#include "refs.h"
> > +#include "replay.h"
> > +#include "reset.h"
> > +#include "revision.h"
> > +#include "sequencer.h"
> > +#include "strvec.h"
> > +#include "tree.h"
> > +#include "wt-status.h"
> > +
> > +static int collect_commits(struct repository *repo,
> > + struct commit *old_commit,
> > + struct commit *new_commit,
> > + struct strvec *out)
> > +{
> > + struct setup_revision_opt revision_opts = {
> > + .assume_dashdash = 1,
> > + };
> > + struct strvec revisions = STRVEC_INIT;
> > + struct commit_list *from_list = NULL;
> > + struct commit *child;
> > + struct rev_info rev = { 0 };
> > + int ret;
> > +
> > + /*
> > + * Check that the old commit actually is an ancestor of HEAD. If not
> > + * the whole request becomes nonsensical.
> > + */
>
> Missing space here
Good eyes.
> > + if (old_commit) {
> > + commit_list_insert(old_commit, &from_list);
> > + if (!repo_is_descendant_of(repo, new_commit, from_list)) {
> > + ret = error(_("commit must be reachable from current HEAD commit"));
> > + goto out;
> > + }
> > + }
>
> Makes sense. There is an inherent assumption using the 'git history'
> command that you want to modify the history of the current reference.
>
> One question, wouldn't it make sense to parse and check that the commit
> to be reworded should be checked to be a descendant of HEAD earlier on
> in `cmd_history_reword()`?
>
> This would ensure this function `collect_commits()` doesn't worry about
> how it is meant to be used, and simply worries about collecting commits.
The reason why I opted to move this into `collect_commits()` is so that
we don't have to reimplement that check for every single subcommand, as
they also have the same restriction.
[snip]
> > +static int fill_commit_message(struct repository *repo,
> > + const struct object_id *old_tree,
> > + const struct object_id *new_tree,
> > + const char *default_message,
> > + const char *provided_message,
> > + const char *action,
> > + struct strbuf *out)
> > +{
> > + if (!provided_message) {
> > + const char *path = git_path_commit_editmsg();
> > + const char *hint =
> > + _("Please enter the commit message for the %s changes. Lines starting\n"
> > + "with '%s' will be kept; you may remove them yourself if you want to.\n");
>
> Shouldn't this be s/kept/removed? Also this line needs to be aligned.
Huh, yes, indeed.
[snip]
> > + if (repo_parse_commit(repo, original_commit)) {
> > + ret = error(_("unable to parse commit %s"),
> > + oid_to_hex(&original_commit->object.oid));
> > + goto out;
> > + }
>
> Isn't this already done as part of
> `lookup_commit_reference_by_name_gently()` which is called by
> `lookup_commit_reference_by_name()` ?
Yes, you're right.
[snip]
> > + ret = commit_tree(final_message.buf, final_message.len,
> > + &repo_get_commit_tree(repo, original_commit)->object.oid,
>
> Can't we use original_commit_tree_oid here?
Yup, indeed.
> > const char **argv,
> > const char *prefix,
> > - struct repository *repo UNUSED)
> > + struct repository *repo)
> > {
> > const char * const usage[] = {
> > N_("git history [<options>]"),
> > + N_("git history reword [<options>] <commit>"),
>
> This string is used twice, perhaps we move it to a macro?
Yeah, why not.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH v4 07/12] add-patch: split out header from "add-interactive.h"
2025-10-01 15:57 ` [PATCH v4 00/12] " Patrick Steinhardt
` (5 preceding siblings ...)
2025-10-01 15:57 ` [PATCH v4 06/12] builtin/history: implement "reword" subcommand Patrick Steinhardt
@ 2025-10-01 15:57 ` Patrick Steinhardt
2025-10-01 15:57 ` [PATCH v4 08/12] add-patch: split out `struct interactive_options` Patrick Steinhardt
` (6 subsequent siblings)
13 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-01 15:57 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren
While we have a "add-patch.c" code file, its declarations are part of
"add-interactive.h". This makes it somewhat harder than necessary to
find relevant code and to identify clear boundaries between the two
subsystems.
Split up concerns and move declarations that relate to "add-patch.c"
into a new "add-patch.h" header.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
add-interactive.h | 23 +++--------------------
add-patch.c | 1 +
add-patch.h | 26 ++++++++++++++++++++++++++
3 files changed, 30 insertions(+), 20 deletions(-)
diff --git a/add-interactive.h b/add-interactive.h
index da49502b76..2e3d1d871d 100644
--- a/add-interactive.h
+++ b/add-interactive.h
@@ -1,14 +1,11 @@
#ifndef ADD_INTERACTIVE_H
#define ADD_INTERACTIVE_H
+#include "add-patch.h"
#include "color.h"
-struct add_p_opt {
- int context;
- int interhunkcontext;
-};
-
-#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }
+struct pathspec;
+struct repository;
struct add_i_state {
struct repository *r;
@@ -35,21 +32,7 @@ void init_add_i_state(struct add_i_state *s, struct repository *r,
struct add_p_opt *add_p_opt);
void clear_add_i_state(struct add_i_state *s);
-struct repository;
-struct pathspec;
int run_add_i(struct repository *r, const struct pathspec *ps,
struct add_p_opt *add_p_opt);
-enum add_p_mode {
- ADD_P_ADD,
- ADD_P_STASH,
- ADD_P_RESET,
- ADD_P_CHECKOUT,
- ADD_P_WORKTREE,
-};
-
-int run_add_p(struct repository *r, enum add_p_mode mode,
- struct add_p_opt *o, const char *revision,
- const struct pathspec *ps);
-
#endif
diff --git a/add-patch.c b/add-patch.c
index b0389c5d5b..9d0890fc49 100644
--- a/add-patch.c
+++ b/add-patch.c
@@ -3,6 +3,7 @@
#include "git-compat-util.h"
#include "add-interactive.h"
+#include "add-patch.h"
#include "advice.h"
#include "editor.h"
#include "environment.h"
diff --git a/add-patch.h b/add-patch.h
new file mode 100644
index 0000000000..4394c74107
--- /dev/null
+++ b/add-patch.h
@@ -0,0 +1,26 @@
+#ifndef ADD_PATCH_H
+#define ADD_PATCH_H
+
+struct pathspec;
+struct repository;
+
+struct add_p_opt {
+ int context;
+ int interhunkcontext;
+};
+
+#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }
+
+enum add_p_mode {
+ ADD_P_ADD,
+ ADD_P_STASH,
+ ADD_P_RESET,
+ ADD_P_CHECKOUT,
+ ADD_P_WORKTREE,
+};
+
+int run_add_p(struct repository *r, enum add_p_mode mode,
+ struct add_p_opt *o, const char *revision,
+ const struct pathspec *ps);
+
+#endif
--
2.51.0.700.g236ee7b076.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH v4 08/12] add-patch: split out `struct interactive_options`
2025-10-01 15:57 ` [PATCH v4 00/12] " Patrick Steinhardt
` (6 preceding siblings ...)
2025-10-01 15:57 ` [PATCH v4 07/12] add-patch: split out header from "add-interactive.h" Patrick Steinhardt
@ 2025-10-01 15:57 ` Patrick Steinhardt
2025-10-02 9:25 ` Kristoffer Haugsbakk
2025-10-14 12:35 ` Karthik Nayak
2025-10-01 15:57 ` [PATCH v4 09/12] add-patch: remove dependency on "add-interactive" subsystem Patrick Steinhardt
` (5 subsequent siblings)
13 siblings, 2 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-01 15:57 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren
The `struct add_p_opt` is reused both by our the infra for "git add -p"
and "git add -i". Users of `run_add_i()` for example are expected to
pass `struct add_p_opt`. This is somewhat confusing and raises the
question which options apply to what part of the stack.
But things are even more confusing than that: while callers are expected
to pass in `struct add_p_opt`, these options ultimately get used to
initialize a `struct add_i_state` that is used by both subsystems. So we
are basically going full circle here.
Refactor the code and split out a new `struct interactive_options` that
hosts common options used by both. These options are then applied to a
`struct interactive_config` that hosts common configuration.
This refactoring doesn't yet fully detangle the two subsystems from one
another, as we still end up calling `init_add_i_state()` in the "git add
-p" subsystem. This will be fixed in a subsequent commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
add-interactive.c | 174 +++++++++++------------------------------------------
add-interactive.h | 23 +------
add-patch.c | 170 +++++++++++++++++++++++++++++++++++++++++++--------
add-patch.h | 36 ++++++++++-
builtin/add.c | 22 +++----
builtin/checkout.c | 4 +-
builtin/commit.c | 16 ++---
builtin/reset.c | 16 ++---
builtin/stash.c | 46 +++++++-------
commit.h | 2 +-
10 files changed, 270 insertions(+), 239 deletions(-)
diff --git a/add-interactive.c b/add-interactive.c
index 6ffe64c38d..f2d971818e 100644
--- a/add-interactive.c
+++ b/add-interactive.c
@@ -3,7 +3,6 @@
#include "git-compat-util.h"
#include "add-interactive.h"
#include "color.h"
-#include "config.h"
#include "diffcore.h"
#include "gettext.h"
#include "hash.h"
@@ -20,119 +19,18 @@
#include "prompt.h"
#include "tree.h"
-static void init_color(struct repository *r, enum git_colorbool use_color,
- const char *section_and_slot, char *dst,
- const char *default_color)
-{
- char *key = xstrfmt("color.%s", section_and_slot);
- const char *value;
-
- if (!want_color(use_color))
- dst[0] = '\0';
- else if (repo_config_get_value(r, key, &value) ||
- color_parse(value, dst))
- strlcpy(dst, default_color, COLOR_MAXLEN);
-
- free(key);
-}
-
-static enum git_colorbool check_color_config(struct repository *r, const char *var)
-{
- const char *value;
- enum git_colorbool ret;
-
- if (repo_config_get_value(r, var, &value))
- ret = GIT_COLOR_UNKNOWN;
- else
- ret = git_config_colorbool(var, value);
-
- /*
- * Do not rely on want_color() to fall back to color.ui for us. It uses
- * the value parsed by git_color_config(), which may not have been
- * called by the main command.
- */
- if (ret == GIT_COLOR_UNKNOWN &&
- !repo_config_get_value(r, "color.ui", &value))
- ret = git_config_colorbool("color.ui", value);
-
- return ret;
-}
-
void init_add_i_state(struct add_i_state *s, struct repository *r,
- struct add_p_opt *add_p_opt)
+ struct interactive_options *opts)
{
s->r = r;
- s->context = -1;
- s->interhunkcontext = -1;
-
- s->use_color_interactive = check_color_config(r, "color.interactive");
-
- init_color(r, s->use_color_interactive, "interactive.header",
- s->header_color, GIT_COLOR_BOLD);
- init_color(r, s->use_color_interactive, "interactive.help",
- s->help_color, GIT_COLOR_BOLD_RED);
- init_color(r, s->use_color_interactive, "interactive.prompt",
- s->prompt_color, GIT_COLOR_BOLD_BLUE);
- init_color(r, s->use_color_interactive, "interactive.error",
- s->error_color, GIT_COLOR_BOLD_RED);
- strlcpy(s->reset_color_interactive,
- want_color(s->use_color_interactive) ? GIT_COLOR_RESET : "", COLOR_MAXLEN);
-
- s->use_color_diff = check_color_config(r, "color.diff");
-
- init_color(r, s->use_color_diff, "diff.frag", s->fraginfo_color,
- diff_get_color(s->use_color_diff, DIFF_FRAGINFO));
- init_color(r, s->use_color_diff, "diff.context", s->context_color,
- "fall back");
- if (!strcmp(s->context_color, "fall back"))
- init_color(r, s->use_color_diff, "diff.plain",
- s->context_color,
- diff_get_color(s->use_color_diff, DIFF_CONTEXT));
- init_color(r, s->use_color_diff, "diff.old", s->file_old_color,
- diff_get_color(s->use_color_diff, DIFF_FILE_OLD));
- init_color(r, s->use_color_diff, "diff.new", s->file_new_color,
- diff_get_color(s->use_color_diff, DIFF_FILE_NEW));
- strlcpy(s->reset_color_diff,
- want_color(s->use_color_diff) ? GIT_COLOR_RESET : "", COLOR_MAXLEN);
-
- FREE_AND_NULL(s->interactive_diff_filter);
- repo_config_get_string(r, "interactive.difffilter",
- &s->interactive_diff_filter);
-
- FREE_AND_NULL(s->interactive_diff_algorithm);
- repo_config_get_string(r, "diff.algorithm",
- &s->interactive_diff_algorithm);
-
- if (!repo_config_get_int(r, "diff.context", &s->context))
- if (s->context < 0)
- die(_("%s cannot be negative"), "diff.context");
- if (!repo_config_get_int(r, "diff.interHunkContext", &s->interhunkcontext))
- if (s->interhunkcontext < 0)
- die(_("%s cannot be negative"), "diff.interHunkContext");
-
- repo_config_get_bool(r, "interactive.singlekey", &s->use_single_key);
- if (s->use_single_key)
- setbuf(stdin, NULL);
-
- if (add_p_opt->context != -1) {
- if (add_p_opt->context < 0)
- die(_("%s cannot be negative"), "--unified");
- s->context = add_p_opt->context;
- }
- if (add_p_opt->interhunkcontext != -1) {
- if (add_p_opt->interhunkcontext < 0)
- die(_("%s cannot be negative"), "--inter-hunk-context");
- s->interhunkcontext = add_p_opt->interhunkcontext;
- }
+ interactive_config_init(&s->cfg, r, opts);
}
void clear_add_i_state(struct add_i_state *s)
{
- FREE_AND_NULL(s->interactive_diff_filter);
- FREE_AND_NULL(s->interactive_diff_algorithm);
+ interactive_config_clear(&s->cfg);
memset(s, 0, sizeof(*s));
- s->use_color_interactive = GIT_COLOR_UNKNOWN;
- s->use_color_diff = GIT_COLOR_UNKNOWN;
+ interactive_config_clear(&s->cfg);
}
/*
@@ -285,7 +183,7 @@ static void list(struct add_i_state *s, struct string_list *list, int *selected,
return;
if (opts->header)
- color_fprintf_ln(stdout, s->header_color,
+ color_fprintf_ln(stdout, s->cfg.header_color,
"%s", opts->header);
for (i = 0; i < list->nr; i++) {
@@ -353,7 +251,7 @@ static ssize_t list_and_choose(struct add_i_state *s,
list(s, &items->items, items->selected, &opts->list_opts);
- color_fprintf(stdout, s->prompt_color, "%s", opts->prompt);
+ color_fprintf(stdout, s->cfg.prompt_color, "%s", opts->prompt);
fputs(singleton ? "> " : ">> ", stdout);
fflush(stdout);
@@ -431,7 +329,7 @@ static ssize_t list_and_choose(struct add_i_state *s,
if (from < 0 || from >= items->items.nr ||
(singleton && from + 1 != to)) {
- color_fprintf_ln(stderr, s->error_color,
+ color_fprintf_ln(stderr, s->cfg.error_color,
_("Huh (%s)?"), p);
break;
} else if (singleton) {
@@ -991,7 +889,7 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps,
free(files->items.items[i].string);
} else if (item->index.unmerged ||
item->worktree.unmerged) {
- color_fprintf_ln(stderr, s->error_color,
+ color_fprintf_ln(stderr, s->cfg.error_color,
_("ignoring unmerged: %s"),
files->items.items[i].string);
free(item);
@@ -1013,9 +911,9 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps,
opts->prompt = N_("Patch update");
count = list_and_choose(s, files, opts);
if (count > 0) {
- struct add_p_opt add_p_opt = {
- .context = s->context,
- .interhunkcontext = s->interhunkcontext,
+ struct interactive_options opts = {
+ .context = s->cfg.context,
+ .interhunkcontext = s->cfg.interhunkcontext,
};
struct strvec args = STRVEC_INIT;
struct pathspec ps_selected = { 0 };
@@ -1027,7 +925,7 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps,
parse_pathspec(&ps_selected,
PATHSPEC_ALL_MAGIC & ~PATHSPEC_LITERAL,
PATHSPEC_LITERAL_PATH, "", args.v);
- res = run_add_p(s->r, ADD_P_ADD, &add_p_opt, NULL, &ps_selected);
+ res = run_add_p(s->r, ADD_P_ADD, &opts, NULL, &ps_selected);
strvec_clear(&args);
clear_pathspec(&ps_selected);
}
@@ -1063,10 +961,10 @@ static int run_diff(struct add_i_state *s, const struct pathspec *ps,
struct child_process cmd = CHILD_PROCESS_INIT;
strvec_pushl(&cmd.args, "git", "diff", "-p", "--cached", NULL);
- if (s->context != -1)
- strvec_pushf(&cmd.args, "--unified=%i", s->context);
- if (s->interhunkcontext != -1)
- strvec_pushf(&cmd.args, "--inter-hunk-context=%i", s->interhunkcontext);
+ if (s->cfg.context != -1)
+ strvec_pushf(&cmd.args, "--unified=%i", s->cfg.context);
+ if (s->cfg.interhunkcontext != -1)
+ strvec_pushf(&cmd.args, "--inter-hunk-context=%i", s->cfg.interhunkcontext);
strvec_pushl(&cmd.args, oid_to_hex(!is_initial ? &oid :
s->r->hash_algo->empty_tree), "--", NULL);
for (i = 0; i < files->items.nr; i++)
@@ -1084,17 +982,17 @@ static int run_help(struct add_i_state *s, const struct pathspec *ps UNUSED,
struct prefix_item_list *files UNUSED,
struct list_and_choose_options *opts UNUSED)
{
- color_fprintf_ln(stdout, s->help_color, "status - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "status - %s",
_("show paths with changes"));
- color_fprintf_ln(stdout, s->help_color, "update - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "update - %s",
_("add working tree state to the staged set of changes"));
- color_fprintf_ln(stdout, s->help_color, "revert - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "revert - %s",
_("revert staged set of changes back to the HEAD version"));
- color_fprintf_ln(stdout, s->help_color, "patch - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "patch - %s",
_("pick hunks and update selectively"));
- color_fprintf_ln(stdout, s->help_color, "diff - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "diff - %s",
_("view diff between HEAD and index"));
- color_fprintf_ln(stdout, s->help_color, "add untracked - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "add untracked - %s",
_("add contents of untracked files to the staged set of changes"));
return 0;
@@ -1102,21 +1000,21 @@ static int run_help(struct add_i_state *s, const struct pathspec *ps UNUSED,
static void choose_prompt_help(struct add_i_state *s)
{
- color_fprintf_ln(stdout, s->help_color, "%s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "%s",
_("Prompt help:"));
- color_fprintf_ln(stdout, s->help_color, "1 - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "1 - %s",
_("select a single item"));
- color_fprintf_ln(stdout, s->help_color, "3-5 - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "3-5 - %s",
_("select a range of items"));
- color_fprintf_ln(stdout, s->help_color, "2-3,6-9 - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "2-3,6-9 - %s",
_("select multiple ranges"));
- color_fprintf_ln(stdout, s->help_color, "foo - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "foo - %s",
_("select item based on unique prefix"));
- color_fprintf_ln(stdout, s->help_color, "-... - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "-... - %s",
_("unselect specified items"));
- color_fprintf_ln(stdout, s->help_color, "* - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "* - %s",
_("choose all items"));
- color_fprintf_ln(stdout, s->help_color, " - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, " - %s",
_("(empty) finish selecting"));
}
@@ -1151,7 +1049,7 @@ static void print_command_item(int i, int selected UNUSED,
static void command_prompt_help(struct add_i_state *s)
{
- const char *help_color = s->help_color;
+ const char *help_color = s->cfg.help_color;
color_fprintf_ln(stdout, help_color, "%s", _("Prompt help:"));
color_fprintf_ln(stdout, help_color, "1 - %s",
_("select a numbered item"));
@@ -1162,7 +1060,7 @@ static void command_prompt_help(struct add_i_state *s)
}
int run_add_i(struct repository *r, const struct pathspec *ps,
- struct add_p_opt *add_p_opt)
+ struct interactive_options *interactive_opts)
{
struct add_i_state s = { NULL };
struct print_command_item_data data = { "[", "]" };
@@ -1205,15 +1103,15 @@ int run_add_i(struct repository *r, const struct pathspec *ps,
->util = util;
}
- init_add_i_state(&s, r, add_p_opt);
+ init_add_i_state(&s, r, interactive_opts);
/*
* When color was asked for, use the prompt color for
* highlighting, otherwise use square brackets.
*/
- if (want_color(s.use_color_interactive)) {
- data.color = s.prompt_color;
- data.reset = s.reset_color_interactive;
+ if (want_color(s.cfg.use_color_interactive)) {
+ data.color = s.cfg.prompt_color;
+ data.reset = s.cfg.reset_color_interactive;
}
print_file_item_data.color = data.color;
print_file_item_data.reset = data.reset;
diff --git a/add-interactive.h b/add-interactive.h
index 2e3d1d871d..eefa2edc7c 100644
--- a/add-interactive.h
+++ b/add-interactive.h
@@ -2,37 +2,20 @@
#define ADD_INTERACTIVE_H
#include "add-patch.h"
-#include "color.h"
struct pathspec;
struct repository;
struct add_i_state {
struct repository *r;
- enum git_colorbool use_color_interactive;
- enum git_colorbool use_color_diff;
- char header_color[COLOR_MAXLEN];
- char help_color[COLOR_MAXLEN];
- char prompt_color[COLOR_MAXLEN];
- char error_color[COLOR_MAXLEN];
- char reset_color_interactive[COLOR_MAXLEN];
-
- char fraginfo_color[COLOR_MAXLEN];
- char context_color[COLOR_MAXLEN];
- char file_old_color[COLOR_MAXLEN];
- char file_new_color[COLOR_MAXLEN];
- char reset_color_diff[COLOR_MAXLEN];
-
- int use_single_key;
- char *interactive_diff_filter, *interactive_diff_algorithm;
- int context, interhunkcontext;
+ struct interactive_config cfg;
};
void init_add_i_state(struct add_i_state *s, struct repository *r,
- struct add_p_opt *add_p_opt);
+ struct interactive_options *opts);
void clear_add_i_state(struct add_i_state *s);
int run_add_i(struct repository *r, const struct pathspec *ps,
- struct add_p_opt *add_p_opt);
+ struct interactive_options *opts);
#endif
diff --git a/add-patch.c b/add-patch.c
index 9d0890fc49..29c15695dd 100644
--- a/add-patch.c
+++ b/add-patch.c
@@ -5,6 +5,8 @@
#include "add-interactive.h"
#include "add-patch.h"
#include "advice.h"
+#include "config.h"
+#include "diff.h"
#include "editor.h"
#include "environment.h"
#include "gettext.h"
@@ -279,6 +281,122 @@ struct add_p_state {
const char *revision;
};
+static void init_color(struct repository *r,
+ enum git_colorbool use_color,
+ const char *section_and_slot, char *dst,
+ const char *default_color)
+{
+ char *key = xstrfmt("color.%s", section_and_slot);
+ const char *value;
+
+ if (!want_color(use_color))
+ dst[0] = '\0';
+ else if (repo_config_get_value(r, key, &value) ||
+ color_parse(value, dst))
+ strlcpy(dst, default_color, COLOR_MAXLEN);
+
+ free(key);
+}
+
+static enum git_colorbool check_color_config(struct repository *r, const char *var)
+{
+ const char *value;
+ enum git_colorbool ret;
+
+ if (repo_config_get_value(r, var, &value))
+ ret = GIT_COLOR_UNKNOWN;
+ else
+ ret = git_config_colorbool(var, value);
+
+ /*
+ * Do not rely on want_color() to fall back to color.ui for us. It uses
+ * the value parsed by git_color_config(), which may not have been
+ * called by the main command.
+ */
+ if (ret == GIT_COLOR_UNKNOWN &&
+ !repo_config_get_value(r, "color.ui", &value))
+ ret = git_config_colorbool("color.ui", value);
+
+ return ret;
+}
+
+void interactive_config_init(struct interactive_config *cfg,
+ struct repository *r,
+ struct interactive_options *opts)
+{
+ cfg->context = -1;
+ cfg->interhunkcontext = -1;
+
+ cfg->use_color_interactive = check_color_config(r, "color.interactive");
+
+ init_color(r, cfg->use_color_interactive, "interactive.header",
+ cfg->header_color, GIT_COLOR_BOLD);
+ init_color(r, cfg->use_color_interactive, "interactive.help",
+ cfg->help_color, GIT_COLOR_BOLD_RED);
+ init_color(r, cfg->use_color_interactive, "interactive.prompt",
+ cfg->prompt_color, GIT_COLOR_BOLD_BLUE);
+ init_color(r, cfg->use_color_interactive, "interactive.error",
+ cfg->error_color, GIT_COLOR_BOLD_RED);
+ strlcpy(cfg->reset_color_interactive,
+ want_color(cfg->use_color_interactive) ? GIT_COLOR_RESET : "", COLOR_MAXLEN);
+
+ cfg->use_color_diff = check_color_config(r, "color.diff");
+
+ init_color(r, cfg->use_color_diff, "diff.frag", cfg->fraginfo_color,
+ diff_get_color(cfg->use_color_diff, DIFF_FRAGINFO));
+ init_color(r, cfg->use_color_diff, "diff.context", cfg->context_color,
+ "fall back");
+ if (!strcmp(cfg->context_color, "fall back"))
+ init_color(r, cfg->use_color_diff, "diff.plain",
+ cfg->context_color,
+ diff_get_color(cfg->use_color_diff, DIFF_CONTEXT));
+ init_color(r, cfg->use_color_diff, "diff.old", cfg->file_old_color,
+ diff_get_color(cfg->use_color_diff, DIFF_FILE_OLD));
+ init_color(r, cfg->use_color_diff, "diff.new", cfg->file_new_color,
+ diff_get_color(cfg->use_color_diff, DIFF_FILE_NEW));
+ strlcpy(cfg->reset_color_diff,
+ want_color(cfg->use_color_diff) ? GIT_COLOR_RESET : "", COLOR_MAXLEN);
+
+ FREE_AND_NULL(cfg->interactive_diff_filter);
+ repo_config_get_string(r, "interactive.difffilter",
+ &cfg->interactive_diff_filter);
+
+ FREE_AND_NULL(cfg->interactive_diff_algorithm);
+ repo_config_get_string(r, "diff.algorithm",
+ &cfg->interactive_diff_algorithm);
+
+ if (!repo_config_get_int(r, "diff.context", &cfg->context))
+ if (cfg->context < 0)
+ die(_("%s cannot be negative"), "diff.context");
+ if (!repo_config_get_int(r, "diff.interHunkContext", &cfg->interhunkcontext))
+ if (cfg->interhunkcontext < 0)
+ die(_("%s cannot be negative"), "diff.interHunkContext");
+
+ repo_config_get_bool(r, "interactive.singlekey", &cfg->use_single_key);
+ if (cfg->use_single_key)
+ setbuf(stdin, NULL);
+
+ if (opts->context != -1) {
+ if (opts->context < 0)
+ die(_("%s cannot be negative"), "--unified");
+ cfg->context = opts->context;
+ }
+ if (opts->interhunkcontext != -1) {
+ if (opts->interhunkcontext < 0)
+ die(_("%s cannot be negative"), "--inter-hunk-context");
+ cfg->interhunkcontext = opts->interhunkcontext;
+ }
+}
+
+void interactive_config_clear(struct interactive_config *cfg)
+{
+ FREE_AND_NULL(cfg->interactive_diff_filter);
+ FREE_AND_NULL(cfg->interactive_diff_algorithm);
+ memset(cfg, 0, sizeof(*cfg));
+ cfg->use_color_interactive = GIT_COLOR_UNKNOWN;
+ cfg->use_color_diff = GIT_COLOR_UNKNOWN;
+}
+
static void add_p_state_clear(struct add_p_state *s)
{
size_t i;
@@ -299,9 +417,9 @@ static void err(struct add_p_state *s, const char *fmt, ...)
va_list args;
va_start(args, fmt);
- fputs(s->s.error_color, stdout);
+ fputs(s->s.cfg.error_color, stdout);
vprintf(fmt, args);
- puts(s->s.reset_color_interactive);
+ puts(s->s.cfg.reset_color_interactive);
va_end(args);
}
@@ -424,12 +542,12 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
int res;
strvec_pushv(&args, s->mode->diff_cmd);
- if (s->s.context != -1)
- strvec_pushf(&args, "--unified=%i", s->s.context);
- if (s->s.interhunkcontext != -1)
- strvec_pushf(&args, "--inter-hunk-context=%i", s->s.interhunkcontext);
- if (s->s.interactive_diff_algorithm)
- strvec_pushf(&args, "--diff-algorithm=%s", s->s.interactive_diff_algorithm);
+ if (s->s.cfg.context != -1)
+ strvec_pushf(&args, "--unified=%i", s->s.cfg.context);
+ if (s->s.cfg.interhunkcontext != -1)
+ strvec_pushf(&args, "--inter-hunk-context=%i", s->s.cfg.interhunkcontext);
+ if (s->s.cfg.interactive_diff_algorithm)
+ strvec_pushf(&args, "--diff-algorithm=%s", s->s.cfg.interactive_diff_algorithm);
if (s->revision) {
struct object_id oid;
strvec_push(&args,
@@ -458,9 +576,9 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
}
strbuf_complete_line(plain);
- if (want_color_fd(1, s->s.use_color_diff)) {
+ if (want_color_fd(1, s->s.cfg.use_color_diff)) {
struct child_process colored_cp = CHILD_PROCESS_INIT;
- const char *diff_filter = s->s.interactive_diff_filter;
+ const char *diff_filter = s->s.cfg.interactive_diff_filter;
setup_child_process(s, &colored_cp, NULL);
xsnprintf((char *)args.v[color_arg_index], 8, "--color");
@@ -693,7 +811,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
hunk->colored_end - hunk->colored_start);
return;
} else {
- strbuf_addstr(out, s->s.fraginfo_color);
+ strbuf_addstr(out, s->s.cfg.fraginfo_color);
p = s->colored.buf + header->colored_extra_start;
len = header->colored_extra_end
- header->colored_extra_start;
@@ -715,7 +833,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
if (len)
strbuf_add(out, p, len);
else if (colored)
- strbuf_addf(out, "%s\n", s->s.reset_color_diff);
+ strbuf_addf(out, "%s\n", s->s.cfg.reset_color_diff);
else
strbuf_addch(out, '\n');
}
@@ -1103,12 +1221,12 @@ static void recolor_hunk(struct add_p_state *s, struct hunk *hunk)
strbuf_addstr(&s->colored,
plain[current] == '-' ?
- s->s.file_old_color :
+ s->s.cfg.file_old_color :
plain[current] == '+' ?
- s->s.file_new_color :
- s->s.context_color);
+ s->s.cfg.file_new_color :
+ s->s.cfg.context_color);
strbuf_add(&s->colored, plain + current, eol - current);
- strbuf_addstr(&s->colored, s->s.reset_color_diff);
+ strbuf_addstr(&s->colored, s->s.cfg.reset_color_diff);
if (next > eol)
strbuf_add(&s->colored, plain + eol, next - eol);
current = next;
@@ -1227,7 +1345,7 @@ static int run_apply_check(struct add_p_state *s,
static int read_single_character(struct add_p_state *s)
{
- if (s->s.use_single_key) {
+ if (s->s.cfg.use_single_key) {
int res = read_key_without_echo(&s->answer);
printf("%s\n", res == EOF ? "" : s->answer.buf);
return res;
@@ -1241,7 +1359,7 @@ static int read_single_character(struct add_p_state *s)
static int prompt_yesno(struct add_p_state *s, const char *prompt)
{
for (;;) {
- color_fprintf(stdout, s->s.prompt_color, "%s", _(prompt));
+ color_fprintf(stdout, s->s.cfg.prompt_color, "%s", _(prompt));
fflush(stdout);
if (read_single_character(s) == EOF)
return -1;
@@ -1522,15 +1640,15 @@ static int patch_update_file(struct add_p_state *s,
else
prompt_mode_type = PROMPT_HUNK;
- printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->s.prompt_color,
+ printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->s.cfg.prompt_color,
(uintmax_t)hunk_index + 1,
(uintmax_t)(file_diff->hunk_nr
? file_diff->hunk_nr
: 1));
printf(_(s->mode->prompt_mode[prompt_mode_type]),
s->buf.buf);
- if (*s->s.reset_color_interactive)
- fputs(s->s.reset_color_interactive, stdout);
+ if (*s->s.cfg.reset_color_interactive)
+ fputs(s->s.cfg.reset_color_interactive, stdout);
fflush(stdout);
if (read_single_character(s) == EOF)
break;
@@ -1687,7 +1805,7 @@ static int patch_update_file(struct add_p_state *s,
err(s, _("Sorry, cannot split this hunk"));
} else if (!split_hunk(s, file_diff,
hunk - file_diff->hunk)) {
- color_fprintf_ln(stdout, s->s.header_color,
+ color_fprintf_ln(stdout, s->s.cfg.header_color,
_("Split into %d hunks."),
(int)splittable_into);
rendered_hunk_index = -1;
@@ -1705,7 +1823,7 @@ static int patch_update_file(struct add_p_state *s,
} else if (s->answer.buf[0] == '?') {
const char *p = _(help_patch_remainder), *eol = p;
- color_fprintf(stdout, s->s.help_color, "%s",
+ color_fprintf(stdout, s->s.cfg.help_color, "%s",
_(s->mode->help_patch_text));
/*
@@ -1723,7 +1841,7 @@ static int patch_update_file(struct add_p_state *s,
if (*p != '?' && !strchr(s->buf.buf, *p))
continue;
- color_fprintf_ln(stdout, s->s.help_color,
+ color_fprintf_ln(stdout, s->s.cfg.help_color,
"%.*s", (int)(eol - p), p);
}
} else {
@@ -1764,7 +1882,7 @@ static int patch_update_file(struct add_p_state *s,
}
int run_add_p(struct repository *r, enum add_p_mode mode,
- struct add_p_opt *o, const char *revision,
+ struct interactive_options *opts, const char *revision,
const struct pathspec *ps)
{
struct add_p_state s = {
@@ -1772,7 +1890,7 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
};
size_t i, binary_count = 0;
- init_add_i_state(&s.s, r, o);
+ init_add_i_state(&s.s, r, opts);
if (mode == ADD_P_STASH)
s.mode = &patch_mode_stash;
diff --git a/add-patch.h b/add-patch.h
index 4394c74107..a4a05d9d14 100644
--- a/add-patch.h
+++ b/add-patch.h
@@ -1,15 +1,45 @@
#ifndef ADD_PATCH_H
#define ADD_PATCH_H
+#include "color.h"
+
struct pathspec;
struct repository;
-struct add_p_opt {
+struct interactive_options {
int context;
int interhunkcontext;
};
-#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }
+#define INTERACTIVE_OPTIONS_INIT { \
+ .context = -1, \
+ .interhunkcontext = -1, \
+}
+
+struct interactive_config {
+ enum git_colorbool use_color_interactive;
+ enum git_colorbool use_color_diff;
+ char header_color[COLOR_MAXLEN];
+ char help_color[COLOR_MAXLEN];
+ char prompt_color[COLOR_MAXLEN];
+ char error_color[COLOR_MAXLEN];
+ char reset_color_interactive[COLOR_MAXLEN];
+
+ char fraginfo_color[COLOR_MAXLEN];
+ char context_color[COLOR_MAXLEN];
+ char file_old_color[COLOR_MAXLEN];
+ char file_new_color[COLOR_MAXLEN];
+ char reset_color_diff[COLOR_MAXLEN];
+
+ int use_single_key;
+ char *interactive_diff_filter, *interactive_diff_algorithm;
+ int context, interhunkcontext;
+};
+
+void interactive_config_init(struct interactive_config *cfg,
+ struct repository *r,
+ struct interactive_options *opts);
+void interactive_config_clear(struct interactive_config *cfg);
enum add_p_mode {
ADD_P_ADD,
@@ -20,7 +50,7 @@ enum add_p_mode {
};
int run_add_p(struct repository *r, enum add_p_mode mode,
- struct add_p_opt *o, const char *revision,
+ struct interactive_options *opts, const char *revision,
const struct pathspec *ps);
#endif
diff --git a/builtin/add.c b/builtin/add.c
index 4cd3d183f9..08e2ef98b0 100644
--- a/builtin/add.c
+++ b/builtin/add.c
@@ -30,7 +30,7 @@ static const char * const builtin_add_usage[] = {
NULL
};
static int patch_interactive, add_interactive, edit_interactive;
-static struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+static struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
static int take_worktree_changes;
static int add_renormalize;
static int pathspec_file_nul;
@@ -159,7 +159,7 @@ static int refresh(struct repository *repo, int verbose, const struct pathspec *
int interactive_add(struct repository *repo,
const char **argv,
const char *prefix,
- int patch, struct add_p_opt *add_p_opt)
+ int patch, struct interactive_options *interactive_opts)
{
struct pathspec pathspec;
int ret;
@@ -171,9 +171,9 @@ int interactive_add(struct repository *repo,
prefix, argv);
if (patch)
- ret = !!run_add_p(repo, ADD_P_ADD, add_p_opt, NULL, &pathspec);
+ ret = !!run_add_p(repo, ADD_P_ADD, interactive_opts, NULL, &pathspec);
else
- ret = !!run_add_i(repo, &pathspec, add_p_opt);
+ ret = !!run_add_i(repo, &pathspec, interactive_opts);
clear_pathspec(&pathspec);
return ret;
@@ -255,8 +255,8 @@ static struct option builtin_add_options[] = {
OPT_GROUP(""),
OPT_BOOL('i', "interactive", &add_interactive, N_("interactive picking")),
OPT_BOOL('p', "patch", &patch_interactive, N_("select hunks interactively")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT_BOOL('e', "edit", &edit_interactive, N_("edit current diff and apply")),
OPT__FORCE(&ignored_too, N_("allow adding otherwise ignored files"), 0),
OPT_BOOL('u', "update", &take_worktree_changes, N_("update tracked files")),
@@ -399,9 +399,9 @@ int cmd_add(int argc,
prepare_repo_settings(repo);
repo->settings.command_requires_full_index = 0;
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
if (patch_interactive)
@@ -411,11 +411,11 @@ int cmd_add(int argc,
die(_("options '%s' and '%s' cannot be used together"), "--dry-run", "--interactive/--patch");
if (pathspec_from_file)
die(_("options '%s' and '%s' cannot be used together"), "--pathspec-from-file", "--interactive/--patch");
- exit(interactive_add(repo, argv + 1, prefix, patch_interactive, &add_p_opt));
+ exit(interactive_add(repo, argv + 1, prefix, patch_interactive, &interactive_opts));
} else {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--interactive/--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--interactive/--patch");
}
diff --git a/builtin/checkout.c b/builtin/checkout.c
index f9453473fe..d230b1f899 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -546,7 +546,7 @@ static int checkout_paths(const struct checkout_opts *opts,
if (opts->patch_mode) {
enum add_p_mode patch_mode;
- struct add_p_opt add_p_opt = {
+ struct interactive_options interactive_opts = {
.context = opts->patch_context,
.interhunkcontext = opts->patch_interhunk_context,
};
@@ -575,7 +575,7 @@ static int checkout_paths(const struct checkout_opts *opts,
else
BUG("either flag must have been set, worktree=%d, index=%d",
opts->checkout_worktree, opts->checkout_index);
- return !!run_add_p(the_repository, patch_mode, &add_p_opt,
+ return !!run_add_p(the_repository, patch_mode, &interactive_opts,
rev, &opts->pathspec);
}
diff --git a/builtin/commit.c b/builtin/commit.c
index 0243f17d53..640495cc57 100644
--- a/builtin/commit.c
+++ b/builtin/commit.c
@@ -123,7 +123,7 @@ static const char *edit_message, *use_message;
static char *fixup_message, *fixup_commit, *squash_message;
static const char *fixup_prefix;
static int all, also, interactive, patch_interactive, only, amend, signoff;
-static struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+static struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
static int edit_flag = -1; /* unspecified */
static int quiet, verbose, no_verify, allow_empty, dry_run, renew_authorship;
static int config_commit_verbose = -1; /* unspecified */
@@ -356,9 +356,9 @@ static const char *prepare_index(const char **argv, const char *prefix,
const char *ret;
char *path = NULL;
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
if (is_status)
@@ -407,7 +407,7 @@ static const char *prepare_index(const char **argv, const char *prefix,
old_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT));
setenv(INDEX_ENVIRONMENT, the_repository->index_file, 1);
- if (interactive_add(the_repository, argv, prefix, patch_interactive, &add_p_opt) != 0)
+ if (interactive_add(the_repository, argv, prefix, patch_interactive, &interactive_opts) != 0)
die(_("interactive add failed"));
the_repository->index_file = old_repo_index_file;
@@ -432,9 +432,9 @@ static const char *prepare_index(const char **argv, const char *prefix,
ret = get_lock_file_path(&index_lock);
goto out;
} else {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--interactive/--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--interactive/--patch");
}
@@ -1742,8 +1742,8 @@ int cmd_commit(int argc,
OPT_BOOL('i', "include", &also, N_("add specified files to index for commit")),
OPT_BOOL(0, "interactive", &interactive, N_("interactively add files")),
OPT_BOOL('p', "patch", &patch_interactive, N_("interactively add changes")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT_BOOL('o', "only", &only, N_("commit only specified files")),
OPT_BOOL('n', "no-verify", &no_verify, N_("bypass pre-commit and commit-msg hooks")),
OPT_BOOL(0, "dry-run", &dry_run, N_("show what would be committed")),
diff --git a/builtin/reset.c b/builtin/reset.c
index ed35802af1..088449e120 100644
--- a/builtin/reset.c
+++ b/builtin/reset.c
@@ -346,7 +346,7 @@ int cmd_reset(int argc,
struct object_id oid;
struct pathspec pathspec;
int intent_to_add = 0;
- struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
const struct option options[] = {
OPT__QUIET(&quiet, N_("be quiet, only report errors")),
OPT_BOOL(0, "no-refresh", &no_refresh,
@@ -371,8 +371,8 @@ int cmd_reset(int argc,
PARSE_OPT_OPTARG,
option_parse_recurse_submodules_worktree_updater),
OPT_BOOL('p', "patch", &patch_mode, N_("select hunks interactively")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT_BOOL('N', "intent-to-add", &intent_to_add,
N_("record only the fact that removed paths will be added later")),
OPT_PATHSPEC_FROM_FILE(&pathspec_from_file),
@@ -423,9 +423,9 @@ int cmd_reset(int argc,
oidcpy(&oid, &tree->object.oid);
}
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
prepare_repo_settings(the_repository);
@@ -436,12 +436,12 @@ int cmd_reset(int argc,
die(_("options '%s' and '%s' cannot be used together"), "--patch", "--{hard,mixed,soft}");
trace2_cmd_mode("patch-interactive");
update_ref_status = !!run_add_p(the_repository, ADD_P_RESET,
- &add_p_opt, rev, &pathspec);
+ &interactive_opts, rev, &pathspec);
goto cleanup;
} else {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--patch");
}
diff --git a/builtin/stash.c b/builtin/stash.c
index 948eba06fb..3b50905233 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -1306,7 +1306,7 @@ static int stash_staged(struct stash_info *info, struct strbuf *out_patch,
static int stash_patch(struct stash_info *info, const struct pathspec *ps,
struct strbuf *out_patch, int quiet,
- struct add_p_opt *add_p_opt)
+ struct interactive_options *interactive_opts)
{
int ret = 0;
struct child_process cp_read_tree = CHILD_PROCESS_INIT;
@@ -1331,7 +1331,7 @@ static int stash_patch(struct stash_info *info, const struct pathspec *ps,
old_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT));
setenv(INDEX_ENVIRONMENT, the_repository->index_file, 1);
- ret = !!run_add_p(the_repository, ADD_P_STASH, add_p_opt, NULL, ps);
+ ret = !!run_add_p(the_repository, ADD_P_STASH, interactive_opts, NULL, ps);
the_repository->index_file = old_repo_index_file;
if (old_index_env && *old_index_env)
@@ -1427,7 +1427,8 @@ static int stash_working_tree(struct stash_info *info, const struct pathspec *ps
}
static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_buf,
- int include_untracked, int patch_mode, struct add_p_opt *add_p_opt,
+ int include_untracked, int patch_mode,
+ struct interactive_options *interactive_opts,
int only_staged, struct stash_info *info, struct strbuf *patch,
int quiet)
{
@@ -1509,7 +1510,7 @@ static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_b
untracked_commit_option = 1;
}
if (patch_mode) {
- ret = stash_patch(info, ps, patch, quiet, add_p_opt);
+ ret = stash_patch(info, ps, patch, quiet, interactive_opts);
if (ret < 0) {
if (!quiet)
fprintf_ln(stderr, _("Cannot save the current "
@@ -1595,7 +1596,8 @@ static int create_stash(int argc, const char **argv, const char *prefix UNUSED,
}
static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int quiet,
- int keep_index, int patch_mode, struct add_p_opt *add_p_opt,
+ int keep_index, int patch_mode,
+ struct interactive_options *interactive_opts,
int include_untracked, int only_staged)
{
int ret = 0;
@@ -1667,7 +1669,7 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q
if (stash_msg)
strbuf_addstr(&stash_msg_buf, stash_msg);
if (do_create_stash(ps, &stash_msg_buf, include_untracked, patch_mode,
- add_p_opt, only_staged, &info, &patch, quiet)) {
+ interactive_opts, only_staged, &info, &patch, quiet)) {
ret = -1;
goto done;
}
@@ -1841,7 +1843,7 @@ static int push_stash(int argc, const char **argv, const char *prefix,
const char *stash_msg = NULL;
char *pathspec_from_file = NULL;
struct pathspec ps;
- struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
struct option options[] = {
OPT_BOOL('k', "keep-index", &keep_index,
N_("keep index")),
@@ -1849,8 +1851,8 @@ static int push_stash(int argc, const char **argv, const char *prefix,
N_("stash staged changes only")),
OPT_BOOL('p', "patch", &patch_mode,
N_("stash in patch mode")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT__QUIET(&quiet, N_("quiet mode")),
OPT_BOOL('u', "include-untracked", &include_untracked,
N_("include untracked files in stash")),
@@ -1907,19 +1909,19 @@ static int push_stash(int argc, const char **argv, const char *prefix,
}
if (!patch_mode) {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--patch");
}
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
ret = do_push_stash(&ps, stash_msg, quiet, keep_index, patch_mode,
- &add_p_opt, include_untracked, only_staged);
+ &interactive_opts, include_untracked, only_staged);
clear_pathspec(&ps);
free(pathspec_from_file);
@@ -1944,7 +1946,7 @@ static int save_stash(int argc, const char **argv, const char *prefix,
const char *stash_msg = NULL;
struct pathspec ps;
struct strbuf stash_msg_buf = STRBUF_INIT;
- struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
struct option options[] = {
OPT_BOOL('k', "keep-index", &keep_index,
N_("keep index")),
@@ -1952,8 +1954,8 @@ static int save_stash(int argc, const char **argv, const char *prefix,
N_("stash staged changes only")),
OPT_BOOL('p', "patch", &patch_mode,
N_("stash in patch mode")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT__QUIET(&quiet, N_("quiet mode")),
OPT_BOOL('u', "include-untracked", &include_untracked,
N_("include untracked files in stash")),
@@ -1973,20 +1975,20 @@ static int save_stash(int argc, const char **argv, const char *prefix,
memset(&ps, 0, sizeof(ps));
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
if (!patch_mode) {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--patch");
}
ret = do_push_stash(&ps, stash_msg, quiet, keep_index,
- patch_mode, &add_p_opt, include_untracked,
+ patch_mode, &interactive_opts, include_untracked,
only_staged);
strbuf_release(&stash_msg_buf);
diff --git a/commit.h b/commit.h
index 1d6e0c7518..7b6e59d6c1 100644
--- a/commit.h
+++ b/commit.h
@@ -258,7 +258,7 @@ int for_each_commit_graft(each_commit_graft_fn, void *);
int interactive_add(struct repository *repo,
const char **argv,
const char *prefix,
- int patch, struct add_p_opt *add_p_opt);
+ int patch, struct interactive_options *opts);
struct commit_extra_header {
struct commit_extra_header *next;
--
2.51.0.700.g236ee7b076.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH v4 08/12] add-patch: split out `struct interactive_options`
2025-10-01 15:57 ` [PATCH v4 08/12] add-patch: split out `struct interactive_options` Patrick Steinhardt
@ 2025-10-02 9:25 ` Kristoffer Haugsbakk
2025-10-14 12:35 ` Karthik Nayak
1 sibling, 0 replies; 278+ messages in thread
From: Kristoffer Haugsbakk @ 2025-10-02 9:25 UTC (permalink / raw)
To: Patrick Steinhardt, git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Elijah Newren
On Wed, Oct 1, 2025, at 17:57, Patrick Steinhardt wrote:
> The `struct add_p_opt` is reused both by our the infra for "git add -p"
“both by our the infra”
I think an edit to this sentence got mixed up/jumbled with
the old version.
> and "git add -i". Users of `run_add_i()` for example are expected to
> pass `struct add_p_opt`. This is somewhat confusing and raises the
> question which options apply to what part of the stack.
s/question which/question of which/ ?
Or: s/question which options apply to what/question of what options apply to what/
>
>[snip]
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v4 08/12] add-patch: split out `struct interactive_options`
2025-10-01 15:57 ` [PATCH v4 08/12] add-patch: split out `struct interactive_options` Patrick Steinhardt
2025-10-02 9:25 ` Kristoffer Haugsbakk
@ 2025-10-14 12:35 ` Karthik Nayak
2025-10-21 11:44 ` Patrick Steinhardt
1 sibling, 1 reply; 278+ messages in thread
From: Karthik Nayak @ 2025-10-14 12:35 UTC (permalink / raw)
To: Patrick Steinhardt, git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren
[-- Attachment #1: Type: text/plain, Size: 3087 bytes --]
Patrick Steinhardt <ps@pks.im> writes:
> The `struct add_p_opt` is reused both by our the infra for "git add -p"
> and "git add -i". Users of `run_add_i()` for example are expected to
> pass `struct add_p_opt`. This is somewhat confusing and raises the
> question which options apply to what part of the stack.
>
Okay. So seems like `struct add_p_opt` is defined in 'add-patch.h' and
`struct add_i_state` in 'add-interactive.h'.
> But things are even more confusing than that: while callers are expected
> to pass in `struct add_p_opt`, these options ultimately get used to
> initialize a `struct add_i_state` that is used by both subsystems. So we
> are basically going full circle here.
>
> Refactor the code and split out a new `struct interactive_options` that
> hosts common options used by both. These options are then applied to a
> `struct interactive_config` that hosts common configuration.
>
> This refactoring doesn't yet fully detangle the two subsystems from one
> another, as we still end up calling `init_add_i_state()` in the "git add
> -p" subsystem. This will be fixed in a subsequent commit.
>
[snip]
> diff --git a/add-patch.h b/add-patch.h
> index 4394c74107..a4a05d9d14 100644
> --- a/add-patch.h
> +++ b/add-patch.h
> @@ -1,15 +1,45 @@
> #ifndef ADD_PATCH_H
> #define ADD_PATCH_H
>
> +#include "color.h"
> +
> struct pathspec;
> struct repository;
>
> -struct add_p_opt {
> +struct interactive_options {
> int context;
> int interhunkcontext;
> };
>
> -#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }
> +#define INTERACTIVE_OPTIONS_INIT { \
> + .context = -1, \
> + .interhunkcontext = -1, \
> +}
> +
> +struct interactive_config {
> + enum git_colorbool use_color_interactive;
> + enum git_colorbool use_color_diff;
> + char header_color[COLOR_MAXLEN];
> + char help_color[COLOR_MAXLEN];
> + char prompt_color[COLOR_MAXLEN];
> + char error_color[COLOR_MAXLEN];
> + char reset_color_interactive[COLOR_MAXLEN];
> +
> + char fraginfo_color[COLOR_MAXLEN];
> + char context_color[COLOR_MAXLEN];
> + char file_old_color[COLOR_MAXLEN];
> + char file_new_color[COLOR_MAXLEN];
> + char reset_color_diff[COLOR_MAXLEN];
> +
> + int use_single_key;
> + char *interactive_diff_filter, *interactive_diff_algorithm;
> + int context, interhunkcontext;
> +};
> +
> +void interactive_config_init(struct interactive_config *cfg,
> + struct repository *r,
> + struct interactive_options *opts);
> +void interactive_config_clear(struct interactive_config *cfg);
>
It feels a little odd that the `interactive_*` code lies in the
'add-patch.h' and not in the 'add-interactive.h'.
Should we also consider moving this or renaming the structs?
Nit: might be nice to make add the 'add_' prefix to them while we're
here.
> enum add_p_mode {
> ADD_P_ADD,
> @@ -20,7 +50,7 @@ enum add_p_mode {
> };
>
> int run_add_p(struct repository *r, enum add_p_mode mode,
> - struct add_p_opt *o, const char *revision,
> + struct interactive_options *opts, const char *revision,
> const struct pathspec *ps);
>
> #endif
[snip]
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 690 bytes --]
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH v4 08/12] add-patch: split out `struct interactive_options`
2025-10-14 12:35 ` Karthik Nayak
@ 2025-10-21 11:44 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-21 11:44 UTC (permalink / raw)
To: Karthik Nayak
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren
On Tue, Oct 14, 2025 at 08:35:39AM -0400, Karthik Nayak wrote:
> Patrick Steinhardt <ps@pks.im> writes:
> > diff --git a/add-patch.h b/add-patch.h
> > index 4394c74107..a4a05d9d14 100644
> > --- a/add-patch.h
> > +++ b/add-patch.h
> > @@ -1,15 +1,45 @@
> > #ifndef ADD_PATCH_H
> > #define ADD_PATCH_H
> >
> > +#include "color.h"
> > +
> > struct pathspec;
> > struct repository;
> >
> > -struct add_p_opt {
> > +struct interactive_options {
> > int context;
> > int interhunkcontext;
> > };
> >
> > -#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }
> > +#define INTERACTIVE_OPTIONS_INIT { \
> > + .context = -1, \
> > + .interhunkcontext = -1, \
> > +}
> > +
> > +struct interactive_config {
> > + enum git_colorbool use_color_interactive;
> > + enum git_colorbool use_color_diff;
> > + char header_color[COLOR_MAXLEN];
> > + char help_color[COLOR_MAXLEN];
> > + char prompt_color[COLOR_MAXLEN];
> > + char error_color[COLOR_MAXLEN];
> > + char reset_color_interactive[COLOR_MAXLEN];
> > +
> > + char fraginfo_color[COLOR_MAXLEN];
> > + char context_color[COLOR_MAXLEN];
> > + char file_old_color[COLOR_MAXLEN];
> > + char file_new_color[COLOR_MAXLEN];
> > + char reset_color_diff[COLOR_MAXLEN];
> > +
> > + int use_single_key;
> > + char *interactive_diff_filter, *interactive_diff_algorithm;
> > + int context, interhunkcontext;
> > +};
> > +
> > +void interactive_config_init(struct interactive_config *cfg,
> > + struct repository *r,
> > + struct interactive_options *opts);
> > +void interactive_config_clear(struct interactive_config *cfg);
> >
>
> It feels a little odd that the `interactive_*` code lies in the
> 'add-patch.h' and not in the 'add-interactive.h'.
>
> Should we also consider moving this or renaming the structs?
>
> Nit: might be nice to make add the 'add_' prefix to them while we're
> here.
The proper name for this struct would be `add_patch_interactive_config`,
but I decided against that name for now as it feels like a mouthful. I
think these two patches improve the status quo regardless of that, so
I'd prefer to just keep those as-is if you don't mind?
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH v4 09/12] add-patch: remove dependency on "add-interactive" subsystem
2025-10-01 15:57 ` [PATCH v4 00/12] " Patrick Steinhardt
` (7 preceding siblings ...)
2025-10-01 15:57 ` [PATCH v4 08/12] add-patch: split out `struct interactive_options` Patrick Steinhardt
@ 2025-10-01 15:57 ` Patrick Steinhardt
2025-10-01 15:57 ` [PATCH v4 10/12] add-patch: add support for in-memory index patching Patrick Steinhardt
` (4 subsequent siblings)
13 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-01 15:57 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren
With the preceding commit we have split out interactive configuration
that is used by both "git add -p" and "git add -i". But we still
initialize that configuration in the "add -p" subsystem by calling
`init_add_i_state()`, even though we only do so to initialize the
interactive configuration as well as a repository pointer.
Stop doing so and instead store and initialize the interactive
configuration in `struct add_p_state` directly.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
add-patch.c | 70 ++++++++++++++++++++++++++++++++-----------------------------
1 file changed, 37 insertions(+), 33 deletions(-)
diff --git a/add-patch.c b/add-patch.c
index 29c15695dd..9c0f3b23ef 100644
--- a/add-patch.c
+++ b/add-patch.c
@@ -2,7 +2,6 @@
#define DISABLE_SIGN_COMPARE_WARNINGS
#include "git-compat-util.h"
-#include "add-interactive.h"
#include "add-patch.h"
#include "advice.h"
#include "config.h"
@@ -263,7 +262,8 @@ struct hunk {
};
struct add_p_state {
- struct add_i_state s;
+ struct repository *r;
+ struct interactive_config cfg;
struct strbuf answer, buf;
/* parsed diff */
@@ -408,7 +408,7 @@ static void add_p_state_clear(struct add_p_state *s)
for (i = 0; i < s->file_diff_nr; i++)
free(s->file_diff[i].hunk);
free(s->file_diff);
- clear_add_i_state(&s->s);
+ interactive_config_clear(&s->cfg);
}
__attribute__((format (printf, 2, 3)))
@@ -417,9 +417,9 @@ static void err(struct add_p_state *s, const char *fmt, ...)
va_list args;
va_start(args, fmt);
- fputs(s->s.cfg.error_color, stdout);
+ fputs(s->cfg.error_color, stdout);
vprintf(fmt, args);
- puts(s->s.cfg.reset_color_interactive);
+ puts(s->cfg.reset_color_interactive);
va_end(args);
}
@@ -437,7 +437,7 @@ static void setup_child_process(struct add_p_state *s,
cp->git_cmd = 1;
strvec_pushf(&cp->env,
- INDEX_ENVIRONMENT "=%s", s->s.r->index_file);
+ INDEX_ENVIRONMENT "=%s", s->r->index_file);
}
static int parse_range(const char **p,
@@ -542,12 +542,12 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
int res;
strvec_pushv(&args, s->mode->diff_cmd);
- if (s->s.cfg.context != -1)
- strvec_pushf(&args, "--unified=%i", s->s.cfg.context);
- if (s->s.cfg.interhunkcontext != -1)
- strvec_pushf(&args, "--inter-hunk-context=%i", s->s.cfg.interhunkcontext);
- if (s->s.cfg.interactive_diff_algorithm)
- strvec_pushf(&args, "--diff-algorithm=%s", s->s.cfg.interactive_diff_algorithm);
+ if (s->cfg.context != -1)
+ strvec_pushf(&args, "--unified=%i", s->cfg.context);
+ if (s->cfg.interhunkcontext != -1)
+ strvec_pushf(&args, "--inter-hunk-context=%i", s->cfg.interhunkcontext);
+ if (s->cfg.interactive_diff_algorithm)
+ strvec_pushf(&args, "--diff-algorithm=%s", s->cfg.interactive_diff_algorithm);
if (s->revision) {
struct object_id oid;
strvec_push(&args,
@@ -576,9 +576,9 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
}
strbuf_complete_line(plain);
- if (want_color_fd(1, s->s.cfg.use_color_diff)) {
+ if (want_color_fd(1, s->cfg.use_color_diff)) {
struct child_process colored_cp = CHILD_PROCESS_INIT;
- const char *diff_filter = s->s.cfg.interactive_diff_filter;
+ const char *diff_filter = s->cfg.interactive_diff_filter;
setup_child_process(s, &colored_cp, NULL);
xsnprintf((char *)args.v[color_arg_index], 8, "--color");
@@ -811,7 +811,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
hunk->colored_end - hunk->colored_start);
return;
} else {
- strbuf_addstr(out, s->s.cfg.fraginfo_color);
+ strbuf_addstr(out, s->cfg.fraginfo_color);
p = s->colored.buf + header->colored_extra_start;
len = header->colored_extra_end
- header->colored_extra_start;
@@ -833,7 +833,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
if (len)
strbuf_add(out, p, len);
else if (colored)
- strbuf_addf(out, "%s\n", s->s.cfg.reset_color_diff);
+ strbuf_addf(out, "%s\n", s->cfg.reset_color_diff);
else
strbuf_addch(out, '\n');
}
@@ -1221,12 +1221,12 @@ static void recolor_hunk(struct add_p_state *s, struct hunk *hunk)
strbuf_addstr(&s->colored,
plain[current] == '-' ?
- s->s.cfg.file_old_color :
+ s->cfg.file_old_color :
plain[current] == '+' ?
- s->s.cfg.file_new_color :
- s->s.cfg.context_color);
+ s->cfg.file_new_color :
+ s->cfg.context_color);
strbuf_add(&s->colored, plain + current, eol - current);
- strbuf_addstr(&s->colored, s->s.cfg.reset_color_diff);
+ strbuf_addstr(&s->colored, s->cfg.reset_color_diff);
if (next > eol)
strbuf_add(&s->colored, plain + eol, next - eol);
current = next;
@@ -1345,7 +1345,7 @@ static int run_apply_check(struct add_p_state *s,
static int read_single_character(struct add_p_state *s)
{
- if (s->s.cfg.use_single_key) {
+ if (s->cfg.use_single_key) {
int res = read_key_without_echo(&s->answer);
printf("%s\n", res == EOF ? "" : s->answer.buf);
return res;
@@ -1359,7 +1359,7 @@ static int read_single_character(struct add_p_state *s)
static int prompt_yesno(struct add_p_state *s, const char *prompt)
{
for (;;) {
- color_fprintf(stdout, s->s.cfg.prompt_color, "%s", _(prompt));
+ color_fprintf(stdout, s->cfg.prompt_color, "%s", _(prompt));
fflush(stdout);
if (read_single_character(s) == EOF)
return -1;
@@ -1640,15 +1640,15 @@ static int patch_update_file(struct add_p_state *s,
else
prompt_mode_type = PROMPT_HUNK;
- printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->s.cfg.prompt_color,
+ printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->cfg.prompt_color,
(uintmax_t)hunk_index + 1,
(uintmax_t)(file_diff->hunk_nr
? file_diff->hunk_nr
: 1));
printf(_(s->mode->prompt_mode[prompt_mode_type]),
s->buf.buf);
- if (*s->s.cfg.reset_color_interactive)
- fputs(s->s.cfg.reset_color_interactive, stdout);
+ if (*s->cfg.reset_color_interactive)
+ fputs(s->cfg.reset_color_interactive, stdout);
fflush(stdout);
if (read_single_character(s) == EOF)
break;
@@ -1805,7 +1805,7 @@ static int patch_update_file(struct add_p_state *s,
err(s, _("Sorry, cannot split this hunk"));
} else if (!split_hunk(s, file_diff,
hunk - file_diff->hunk)) {
- color_fprintf_ln(stdout, s->s.cfg.header_color,
+ color_fprintf_ln(stdout, s->cfg.header_color,
_("Split into %d hunks."),
(int)splittable_into);
rendered_hunk_index = -1;
@@ -1823,7 +1823,7 @@ static int patch_update_file(struct add_p_state *s,
} else if (s->answer.buf[0] == '?') {
const char *p = _(help_patch_remainder), *eol = p;
- color_fprintf(stdout, s->s.cfg.help_color, "%s",
+ color_fprintf(stdout, s->cfg.help_color, "%s",
_(s->mode->help_patch_text));
/*
@@ -1841,7 +1841,7 @@ static int patch_update_file(struct add_p_state *s,
if (*p != '?' && !strchr(s->buf.buf, *p))
continue;
- color_fprintf_ln(stdout, s->s.cfg.help_color,
+ color_fprintf_ln(stdout, s->cfg.help_color,
"%.*s", (int)(eol - p), p);
}
} else {
@@ -1861,7 +1861,7 @@ static int patch_update_file(struct add_p_state *s,
strbuf_reset(&s->buf);
reassemble_patch(s, file_diff, 0, &s->buf);
- discard_index(s->s.r->index);
+ discard_index(s->r->index);
if (s->mode->apply_for_checkout)
apply_for_checkout(s, &s->buf,
s->mode->is_reverse);
@@ -1872,8 +1872,8 @@ static int patch_update_file(struct add_p_state *s,
NULL, 0, NULL, 0))
error(_("'git apply' failed"));
}
- if (repo_read_index(s->s.r) >= 0)
- repo_refresh_and_write_index(s->s.r, REFRESH_QUIET, 0,
+ if (repo_read_index(s->r) >= 0)
+ repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,
1, NULL, NULL, NULL);
}
@@ -1886,11 +1886,15 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
const struct pathspec *ps)
{
struct add_p_state s = {
- { r }, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
+ .r = r,
+ .answer = STRBUF_INIT,
+ .buf = STRBUF_INIT,
+ .plain = STRBUF_INIT,
+ .colored = STRBUF_INIT,
};
size_t i, binary_count = 0;
- init_add_i_state(&s.s, r, opts);
+ interactive_config_init(&s.cfg, r, opts);
if (mode == ADD_P_STASH)
s.mode = &patch_mode_stash;
--
2.51.0.700.g236ee7b076.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH v4 10/12] add-patch: add support for in-memory index patching
2025-10-01 15:57 ` [PATCH v4 00/12] " Patrick Steinhardt
` (8 preceding siblings ...)
2025-10-01 15:57 ` [PATCH v4 09/12] add-patch: remove dependency on "add-interactive" subsystem Patrick Steinhardt
@ 2025-10-01 15:57 ` Patrick Steinhardt
2025-10-02 9:28 ` Kristoffer Haugsbakk
2025-10-14 13:08 ` Karthik Nayak
2025-10-01 15:57 ` [PATCH v4 11/12] cache-tree: allow writing in-memory index as tree Patrick Steinhardt
` (3 subsequent siblings)
13 siblings, 2 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-01 15:57 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren
With `run_add_p()` callers have the ability to apply changes from a
specific revision to a repository's index. This infra supports several
different modes, like for example applying changes to the index,
worktree or both.
One feature that is missing though is the ability to apply changes to an
in-memory index different from the repository's index. Add a new
function `run_add_p_index()` to plug this gap.
This new function will be used in a subsequent commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
add-patch.c | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
add-patch.h | 8 +++++
2 files changed, 115 insertions(+), 3 deletions(-)
diff --git a/add-patch.c b/add-patch.c
index 9c0f3b23ef..fac82c3886 100644
--- a/add-patch.c
+++ b/add-patch.c
@@ -4,11 +4,13 @@
#include "git-compat-util.h"
#include "add-patch.h"
#include "advice.h"
+#include "commit.h"
#include "config.h"
#include "diff.h"
#include "editor.h"
#include "environment.h"
#include "gettext.h"
+#include "hex.h"
#include "object-name.h"
#include "pager.h"
#include "read-cache-ll.h"
@@ -263,6 +265,8 @@ struct hunk {
struct add_p_state {
struct repository *r;
+ struct index_state *index;
+ const char *index_file;
struct interactive_config cfg;
struct strbuf answer, buf;
@@ -437,7 +441,7 @@ static void setup_child_process(struct add_p_state *s,
cp->git_cmd = 1;
strvec_pushf(&cp->env,
- INDEX_ENVIRONMENT "=%s", s->r->index_file);
+ INDEX_ENVIRONMENT "=%s", s->index_file);
}
static int parse_range(const char **p,
@@ -1861,7 +1865,7 @@ static int patch_update_file(struct add_p_state *s,
strbuf_reset(&s->buf);
reassemble_patch(s, file_diff, 0, &s->buf);
- discard_index(s->r->index);
+ discard_index(s->index);
if (s->mode->apply_for_checkout)
apply_for_checkout(s, &s->buf,
s->mode->is_reverse);
@@ -1872,9 +1876,11 @@ static int patch_update_file(struct add_p_state *s,
NULL, 0, NULL, 0))
error(_("'git apply' failed"));
}
- if (repo_read_index(s->r) >= 0)
+ if (read_index_from(s->index, s->index_file, s->r->gitdir) >= 0 &&
+ s->index == s->r->index) {
repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,
1, NULL, NULL, NULL);
+ }
}
putchar('\n');
@@ -1887,6 +1893,8 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
{
struct add_p_state s = {
.r = r,
+ .index = r->index,
+ .index_file = r->index_file,
.answer = STRBUF_INIT,
.buf = STRBUF_INIT,
.plain = STRBUF_INIT,
@@ -1945,3 +1953,99 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
add_p_state_clear(&s);
return 0;
}
+
+int run_add_p_index(struct repository *r,
+ struct index_state *index,
+ const char *index_file,
+ struct interactive_options *opts,
+ const char *revision,
+ const struct pathspec *ps)
+{
+ struct patch_mode mode = {
+ .apply_args = { "--cached", NULL },
+ .apply_check_args = { "--cached", NULL },
+ .prompt_mode = {
+ N_("Stage mode change [y,n,q,a,d%s,?]? "),
+ N_("Stage deletion [y,n,q,a,d%s,?]? "),
+ N_("Stage addition [y,n,q,a,d%s,?]? "),
+ N_("Stage this hunk [y,n,q,a,d%s,?]? ")
+ },
+ .edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk "
+ "will immediately be marked for staging."),
+ .help_patch_text =
+ N_("y - stage this hunk\n"
+ "n - do not stage this hunk\n"
+ "q - quit; do not stage this hunk or any of the remaining "
+ "ones\n"
+ "a - stage this hunk and all later hunks in the file\n"
+ "d - do not stage this hunk or any of the later hunks in "
+ "the file\n"),
+ .index_only = 1,
+ };
+ struct add_p_state s = {
+ .r = r,
+ .index = index,
+ .index_file = index_file,
+ .answer = STRBUF_INIT,
+ .buf = STRBUF_INIT,
+ .plain = STRBUF_INIT,
+ .colored = STRBUF_INIT,
+ .mode = &mode,
+ .revision = revision,
+ };
+ struct strbuf parent_revision = STRBUF_INIT;
+ char parent_tree_oid[GIT_MAX_HEXSZ + 1];
+ size_t binary_count = 0;
+ struct commit *commit;
+ int ret;
+
+ commit = lookup_commit_reference_by_name(revision);
+ if (!commit) {
+ err(&s, _("Revision does not refer to a commit"));
+ ret = -1;
+ goto out;
+ }
+
+ if (commit->parents)
+ oid_to_hex_r(parent_tree_oid, get_commit_tree_oid(commit->parents->item));
+ else
+ oid_to_hex_r(parent_tree_oid, r->hash_algo->empty_tree);
+
+ strbuf_addf(&parent_revision, "%s~", revision);
+ mode.diff_cmd[0] = "diff-tree";
+ mode.diff_cmd[1] = "-r";
+ mode.diff_cmd[2] = parent_tree_oid;
+
+ interactive_config_init(&s.cfg, r, opts);
+
+ if (parse_diff(&s, ps) < 0) {
+ ret = -1;
+ goto out;
+ }
+
+ for (size_t i = 0; i < s.file_diff_nr; i++) {
+ if (s.file_diff[i].binary && !s.file_diff[i].hunk_nr)
+ binary_count++;
+ else if (patch_update_file(&s, s.file_diff + i))
+ break;
+ }
+
+ if (s.file_diff_nr == 0) {
+ err(&s, _("No changes."));
+ ret = -1;
+ goto out;
+ }
+
+ if (binary_count == s.file_diff_nr) {
+ err(&s, _("Only binary files changed."));
+ ret = -1;
+ goto out;
+ }
+
+ ret = 0;
+
+out:
+ strbuf_release(&parent_revision);
+ add_p_state_clear(&s);
+ return ret;
+}
diff --git a/add-patch.h b/add-patch.h
index a4a05d9d14..901c42fd7b 100644
--- a/add-patch.h
+++ b/add-patch.h
@@ -3,6 +3,7 @@
#include "color.h"
+struct index_state;
struct pathspec;
struct repository;
@@ -53,4 +54,11 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
struct interactive_options *opts, const char *revision,
const struct pathspec *ps);
+int run_add_p_index(struct repository *r,
+ struct index_state *index,
+ const char *index_file,
+ struct interactive_options *opts,
+ const char *revision,
+ const struct pathspec *ps);
+
#endif
--
2.51.0.700.g236ee7b076.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH v4 10/12] add-patch: add support for in-memory index patching
2025-10-01 15:57 ` [PATCH v4 10/12] add-patch: add support for in-memory index patching Patrick Steinhardt
@ 2025-10-02 9:28 ` Kristoffer Haugsbakk
2025-10-02 10:24 ` Patrick Steinhardt
2025-10-14 13:08 ` Karthik Nayak
1 sibling, 1 reply; 278+ messages in thread
From: Kristoffer Haugsbakk @ 2025-10-02 9:28 UTC (permalink / raw)
To: Patrick Steinhardt, git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Elijah Newren
On Wed, Oct 1, 2025, at 17:57, Patrick Steinhardt wrote:
> With `run_add_p()` callers have the ability to apply changes from a
> specific revision to a repository's index. This infra supports several
> different modes, like for example applying changes to the index,
> worktree or both.
s/worktree/working tree/ ?
>
>[snip]
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v4 10/12] add-patch: add support for in-memory index patching
2025-10-02 9:28 ` Kristoffer Haugsbakk
@ 2025-10-02 10:24 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-02 10:24 UTC (permalink / raw)
To: Kristoffer Haugsbakk
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Elijah Newren
On Thu, Oct 02, 2025 at 11:28:23AM +0200, Kristoffer Haugsbakk wrote:
> On Wed, Oct 1, 2025, at 17:57, Patrick Steinhardt wrote:
> > With `run_add_p()` callers have the ability to apply changes from a
> > specific revision to a repository's index. This infra supports several
> > different modes, like for example applying changes to the index,
> > worktree or both.
>
> s/worktree/working tree/ ?
Yup. I've applied all of your feedback locally and will send it out with
the next version. Thanks!
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v4 10/12] add-patch: add support for in-memory index patching
2025-10-01 15:57 ` [PATCH v4 10/12] add-patch: add support for in-memory index patching Patrick Steinhardt
2025-10-02 9:28 ` Kristoffer Haugsbakk
@ 2025-10-14 13:08 ` Karthik Nayak
1 sibling, 0 replies; 278+ messages in thread
From: Karthik Nayak @ 2025-10-14 13:08 UTC (permalink / raw)
To: Patrick Steinhardt, git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren
[-- Attachment #1: Type: text/plain, Size: 803 bytes --]
Patrick Steinhardt <ps@pks.im> writes:
[snip]
> @@ -1945,3 +1953,99 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
> add_p_state_clear(&s);
> return 0;
> }
> +
> +int run_add_p_index(struct repository *r,
> + struct index_state *index,
> + const char *index_file,
> + struct interactive_options *opts,
> + const char *revision,
> + const struct pathspec *ps)
> +{
> + struct patch_mode mode = {
> + .apply_args = { "--cached", NULL },
> + .apply_check_args = { "--cached", NULL },
> + .prompt_mode = {
> + N_("Stage mode change [y,n,q,a,d%s,?]? "),
> + N_("Stage deletion [y,n,q,a,d%s,?]? "),
> + N_("Stage addition [y,n,q,a,d%s,?]? "),
> + N_("Stage this hunk [y,n,q,a,d%s,?]? ")
Missing trailing comma.
Rest of the patch looks good.
[snip]
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 690 bytes --]
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH v4 11/12] cache-tree: allow writing in-memory index as tree
2025-10-01 15:57 ` [PATCH v4 00/12] " Patrick Steinhardt
` (9 preceding siblings ...)
2025-10-01 15:57 ` [PATCH v4 10/12] add-patch: add support for in-memory index patching Patrick Steinhardt
@ 2025-10-01 15:57 ` Patrick Steinhardt
2025-10-01 15:57 ` [PATCH v4 12/12] builtin/history: implement "split" subcommand Patrick Steinhardt
` (2 subsequent siblings)
13 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-01 15:57 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren
The function `write_in_core_index_as_tree()` takes a repository and
writes its index into a tree object. What this function cannot do though
is to take an _arbitrary_ in-memory index.
Introduce a new `struct index_state` parameter so that the caller can
pass a different index than the one belonging to the repository. This
will be used in a subsequent commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
builtin/checkout.c | 3 ++-
cache-tree.c | 5 ++---
cache-tree.h | 3 ++-
3 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/builtin/checkout.c b/builtin/checkout.c
index d230b1f8995..0b90f398feb 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -902,7 +902,8 @@ static int merge_working_tree(const struct checkout_opts *opts,
0);
init_ui_merge_options(&o, the_repository);
o.verbosity = 0;
- work = write_in_core_index_as_tree(the_repository);
+ work = write_in_core_index_as_tree(the_repository,
+ the_repository->index);
ret = reset_tree(new_tree,
opts, 1,
diff --git a/cache-tree.c b/cache-tree.c
index d225554eedd..1fe03605225 100644
--- a/cache-tree.c
+++ b/cache-tree.c
@@ -700,11 +700,11 @@ static int write_index_as_tree_internal(struct object_id *oid,
return 0;
}
-struct tree* write_in_core_index_as_tree(struct repository *repo) {
+struct tree *write_in_core_index_as_tree(struct repository *repo,
+ struct index_state *index_state) {
struct object_id o;
int was_valid, ret;
- struct index_state *index_state = repo->index;
was_valid = index_state->cache_tree &&
cache_tree_fully_valid(index_state->cache_tree);
@@ -724,7 +724,6 @@ struct tree* write_in_core_index_as_tree(struct repository *repo) {
return lookup_tree(repo, &index_state->cache_tree->oid);
}
-
int write_index_as_tree(struct object_id *oid, struct index_state *index_state, const char *index_path, int flags, const char *prefix)
{
int entries, was_valid;
diff --git a/cache-tree.h b/cache-tree.h
index b82c4963e7c..f8bddae5235 100644
--- a/cache-tree.h
+++ b/cache-tree.h
@@ -47,7 +47,8 @@ int cache_tree_verify(struct repository *, struct index_state *);
#define WRITE_TREE_UNMERGED_INDEX (-2)
#define WRITE_TREE_PREFIX_ERROR (-3)
-struct tree* write_in_core_index_as_tree(struct repository *repo);
+struct tree *write_in_core_index_as_tree(struct repository *repo,
+ struct index_state *index_state);
int write_index_as_tree(struct object_id *oid, struct index_state *index_state, const char *index_path, int flags, const char *prefix);
void prime_cache_tree(struct repository *, struct index_state *, struct tree *);
--
2.51.0.700.g236ee7b076.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH v4 12/12] builtin/history: implement "split" subcommand
2025-10-01 15:57 ` [PATCH v4 00/12] " Patrick Steinhardt
` (10 preceding siblings ...)
2025-10-01 15:57 ` [PATCH v4 11/12] cache-tree: allow writing in-memory index as tree Patrick Steinhardt
@ 2025-10-01 15:57 ` Patrick Steinhardt
2025-10-14 13:38 ` Karthik Nayak
2025-10-14 13:41 ` [PATCH v4 00/12] Introduce git-history(1) command for easy history editing Karthik Nayak
2025-10-14 16:47 ` Junio C Hamano
13 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-01 15:57 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren
It is quite a common use case that one wants to split up one commit into
multiple commits by moving parts of the changes of the original commit
out into a separate commit. This is quite an involved operation though:
1. Identify the commit in question that is to be dropped.
2. Perform an interactive rebase on top of that commit's parent.
3. Modify the instruction sheet to "edit" the commit that is to be
split up.
4. Drop the commit via "git reset HEAD~".
5. Stage changes that should go into the first commit and commit it.
6. Stage changes that should go into the second commit and commit it.
7. Finalize the rebase.
This is quite complex, and overall I would claim that most people who
are not experts in Git would struggle with this flow.
Introduce a new "split" subcommand for git-history(1) to make this way
easier. All the user needs to do is to say `git history split $COMMIT`.
From hereon, Git asks the user which parts of the commit shall be moved
out into a separate commit and, once done, asks the user for the commit
message. Git then creates that split-out commit and applies the original
commit on top of it.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-history.adoc | 62 ++++++
builtin/history.c | 225 +++++++++++++++++++++
t/meson.build | 1 +
t/t3452-history-split.sh | 432 +++++++++++++++++++++++++++++++++++++++++
4 files changed, 720 insertions(+)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index b55babe206..83d675afea 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -10,6 +10,7 @@ SYNOPSIS
[synopsis]
git history [<options>]
git history reword [<options>] <commit>
+git history split [<options>] <commit> [--] [<pathspec>...]
DESCRIPTION
-----------
@@ -40,6 +41,26 @@ rewrite history in different ways:
provided, then this command will spawn an editor with the current
message of that commit.
+`split [--message=<message>] <commit> [--] [<pathspec>...]`::
+ Interactively split up <commit> into two commits by choosing
+ hunks introduced by it that will be moved into the new split-out
+ commit. These hunks will then be written into a new commit that
+ becomes the parent of the previous commit. The original commit
+ stays intact, except that its parent will be the newly split-out
+ commit.
++
+The commit message of the new commit will be asked for by launching the
+configured editor, unless it has been specified with the `-m` option.
+Authorship of the commit will be the same as for the original commit.
++
+If passed, _<pathspec>_ can be used to limit which changes shall be split out
+of the original commit. Files not matching any of the pathspecs will remain
+part of the original commit. For more details, see the 'pathspec' entry in
+linkgit:gitglossary[7].
++
+It is invalid to select either all or no hunks, as that would lead to
+one of the commits becoming empty.
+
CONFIGURATION
-------------
@@ -47,6 +68,47 @@ include::includes/cmd-config-section-all.adoc[]
include::config/sequencer.adoc[]
+EXAMPLES
+--------
+
+Split a commit
+~~~~~~~~~~~~~~
+
+----------
+$ git log --stat --oneline
+3f81232 (HEAD -> main) original
+ bar | 1 +
+ foo | 1 +
+ 2 files changed, 2 insertions(+)
+
+$ git history split HEAD --message="split-out commit"
+diff --git a/bar b/bar
+new file mode 100644
+index 0000000..5716ca5
+--- /dev/null
++++ b/bar
+@@ -0,0 +1 @@
++bar
+(1/1) Stage addition [y,n,q,a,d,e,p,?]? y
+
+diff --git a/foo b/foo
+new file mode 100644
+index 0000000..257cc56
+--- /dev/null
++++ b/foo
+@@ -0,0 +1 @@
++foo
+(1/1) Stage addition [y,n,q,a,d,e,p,?]? n
+
+$ git log --stat --oneline
+7cebe64 (HEAD -> main) original
+ foo | 1 +
+ 1 file changed, 1 insertion(+)
+d1582f3 split-out commit
+ bar | 1 +
+ 1 file changed, 1 insertion(+)
+----------
+
GIT
---
Part of the linkgit:git[1] suite
diff --git a/builtin/history.c b/builtin/history.c
index 7b2a0023e8..8851a2945e 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -1,6 +1,7 @@
#define USE_THE_REPOSITORY_VARIABLE
#include "builtin.h"
+#include "cache-tree.h"
#include "commit-reach.h"
#include "commit.h"
#include "config.h"
@@ -10,10 +11,13 @@
#include "hex.h"
#include "oidmap.h"
#include "parse-options.h"
+#include "path.h"
+#include "read-cache.h"
#include "refs.h"
#include "replay.h"
#include "reset.h"
#include "revision.h"
+#include "run-command.h"
#include "sequencer.h"
#include "strvec.h"
#include "tree.h"
@@ -368,6 +372,225 @@ static int cmd_history_reword(int argc,
return ret;
}
+static int split_commit(struct repository *repo,
+ struct commit *original_commit,
+ struct pathspec *pathspec,
+ const char *commit_message,
+ struct object_id *out)
+{
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
+ struct strbuf index_file = STRBUF_INIT, split_message = STRBUF_INIT;
+ struct child_process read_tree_cmd = CHILD_PROCESS_INIT;
+ struct index_state index = INDEX_STATE_INIT(repo);
+ struct object_id original_commit_tree_oid, parent_tree_oid;
+ const char *original_message, *original_body, *ptr;
+ char original_commit_oid[GIT_MAX_HEXSZ + 1];
+ char *original_author = NULL;
+ struct commit_list *parents = NULL;
+ struct commit *first_commit;
+ struct tree *split_tree;
+ size_t len;
+ int ret;
+
+ if (original_commit->parents)
+ parent_tree_oid = *get_commit_tree_oid(original_commit->parents->item);
+ else
+ oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
+ original_commit_tree_oid = *get_commit_tree_oid(original_commit);
+
+ /*
+ * Construct the first commit. This is done by taking the original
+ * commit parent's tree and selectively patching changes from the diff
+ * between that parent and its child.
+ */
+ repo_git_path_replace(repo, &index_file, "%s", "history-split.index");
+
+ read_tree_cmd.git_cmd = 1;
+ strvec_pushf(&read_tree_cmd.env, "GIT_INDEX_FILE=%s", index_file.buf);
+ strvec_push(&read_tree_cmd.args, "read-tree");
+ strvec_push(&read_tree_cmd.args, oid_to_hex(&parent_tree_oid));
+ ret = run_command(&read_tree_cmd);
+ if (ret < 0)
+ goto out;
+
+ ret = read_index_from(&index, index_file.buf, repo->gitdir);
+ if (ret < 0) {
+ ret = error(_("failed reading temporary index"));
+ goto out;
+ }
+
+ oid_to_hex_r(original_commit_oid, &original_commit->object.oid);
+ ret = run_add_p_index(repo, &index, index_file.buf, &interactive_opts,
+ original_commit_oid, pathspec);
+ if (ret < 0)
+ goto out;
+
+ split_tree = write_in_core_index_as_tree(repo, &index);
+ if (!split_tree) {
+ ret = error(_("failed split tree"));
+ goto out;
+ }
+
+ unlink(index_file.buf);
+
+ /*
+ * We disallow the cases where either the split-out commit or the
+ * original commit would become empty. Consequently, if we see that the
+ * new tree ID matches either of those trees we abort.
+ */
+ if (oideq(&split_tree->object.oid, &parent_tree_oid)) {
+ ret = error(_("split commit is empty"));
+ goto out;
+ } else if (oideq(&split_tree->object.oid, &original_commit_tree_oid)) {
+ ret = error(_("split commit tree matches original commit"));
+ goto out;
+ }
+
+ /* We retain authorship of the original commit. */
+ original_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);
+ ptr = find_commit_header(original_message, "author", &len);
+ if (ptr)
+ original_author = xmemdupz(ptr, len);
+
+ ret = fill_commit_message(repo, &parent_tree_oid, &split_tree->object.oid,
+ "", commit_message, "split-out", &split_message);
+ if (ret < 0)
+ goto out;
+
+ ret = commit_tree(split_message.buf, split_message.len, &split_tree->object.oid,
+ original_commit->parents, &out[0], original_author, NULL);
+ if (ret < 0) {
+ ret = error(_("failed writing split-out commit"));
+ goto out;
+ }
+
+ /*
+ * The second commit is much simpler to construct, as we can simply use
+ * the original commit details, except that we adjust its parent to be
+ * the newly split-out commit.
+ */
+ find_commit_subject(original_message, &original_body);
+ first_commit = lookup_commit_reference(repo, &out[0]);
+ commit_list_append(first_commit, &parents);
+
+ ret = commit_tree(original_body, strlen(original_body), &original_commit_tree_oid,
+ parents, &out[1], original_author, NULL);
+ if (ret < 0) {
+ ret = error(_("failed writing second commit"));
+ goto out;
+ }
+
+ ret = 0;
+
+out:
+ if (index_file.len)
+ unlink(index_file.buf);
+ strbuf_release(&split_message);
+ strbuf_release(&index_file);
+ free_commit_list(parents);
+ free(original_author);
+ release_index(&index);
+ return ret;
+}
+
+static int cmd_history_split(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo)
+{
+ const char * const usage[] = {
+ N_("git history split [<options>] <commit>"),
+ NULL,
+ };
+ const char *commit_message = NULL;
+ struct option options[] = {
+ OPT_STRING('m', "message", &commit_message, N_("message"), N_("commit message")),
+ OPT_END(),
+ };
+ struct oidmap rewritten_commits = OIDMAP_INIT;
+ struct commit *original_commit, *parent, *head;
+ struct strvec commits = STRVEC_INIT;
+ struct commit_list *list = NULL;
+ struct object_id split_commits[2];
+ struct pathspec pathspec = { 0 };
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc < 1) {
+ ret = error(_("command expects a revision"));
+ goto out;
+ }
+ repo_config(repo, git_default_config, NULL);
+
+ original_commit = lookup_commit_reference_by_name(argv[0]);
+ if (!original_commit) {
+ ret = error(_("commit to be split cannot be found: %s"), argv[0]);
+ goto out;
+ }
+
+ if (original_commit->parents && original_commit->parents->next) {
+ ret = error(_("commit to be split must not be a merge commit"));
+ goto out;
+ }
+
+ parent = original_commit->parents ? original_commit->parents->item : NULL;
+ if (parent && repo_parse_commit(repo, parent)) {
+ ret = error(_("unable to parse commit %s"),
+ oid_to_hex(&parent->object.oid));
+ goto out;
+ }
+
+ head = lookup_commit_reference_by_name("HEAD");
+ if (!head) {
+ ret = error(_("could not resolve HEAD to a commit"));
+ goto out;
+ }
+
+ commit_list_append(original_commit, &list);
+ if (!repo_is_descendant_of(repo, original_commit, list)) {
+ ret = error (_("split commit must be reachable from current HEAD commit"));
+ goto out;
+ }
+
+ parse_pathspec(&pathspec, 0,
+ PATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,
+ prefix, argv + 1);
+
+ /*
+ * Collect the list of commits that we'll have to reapply now already.
+ * This ensures that we'll abort early on in case the range of commits
+ * contains merges, which we do not yet handle.
+ */
+ ret = collect_commits(repo, parent, head, &commits);
+ if (ret < 0)
+ goto out;
+
+ /*
+ * Then we split up the commit and replace the original commit with the
+ * new new ones.
+ */
+ ret = split_commit(repo, original_commit, &pathspec,
+ commit_message, split_commits);
+ if (ret < 0)
+ goto out;
+
+ replace_commits(&commits, &original_commit->object.oid,
+ split_commits, ARRAY_SIZE(split_commits));
+
+ ret = apply_commits(repo, &commits, parent, head, "split");
+ if (ret < 0)
+ goto out;
+
+ ret = 0;
+
+out:
+ oidmap_clear(&rewritten_commits, 0);
+ clear_pathspec(&pathspec);
+ strvec_clear(&commits);
+ free_commit_list(list);
+ return ret;
+}
+
int cmd_history(int argc,
const char **argv,
const char *prefix,
@@ -376,11 +599,13 @@ int cmd_history(int argc,
const char * const usage[] = {
N_("git history [<options>]"),
N_("git history reword [<options>] <commit>"),
+ N_("git history split [<options>] <commit> [--] [<pathspec>...]"),
NULL,
};
parse_opt_subcommand_fn *fn = NULL;
struct option options[] = {
OPT_SUBCOMMAND("reword", &fn, cmd_history_reword),
+ OPT_SUBCOMMAND("split", &fn, cmd_history_split),
OPT_END(),
};
diff --git a/t/meson.build b/t/meson.build
index 2a74243202..fb05be16ae 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -384,6 +384,7 @@ integration_tests = [
't3438-rebase-broken-files.sh',
't3450-history.sh',
't3451-history-reword.sh',
+ 't3452-history-split.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
diff --git a/t/t3452-history-split.sh b/t/t3452-history-split.sh
new file mode 100755
index 0000000000..45d3b32ebf
--- /dev/null
+++ b/t/t3452-history-split.sh
@@ -0,0 +1,432 @@
+#!/bin/sh
+
+test_description='tests for git-history split subcommand'
+
+. ./test-lib.sh
+
+set_fake_editor () {
+ write_script fake-editor.sh <<-\EOF &&
+ echo "split-out commit" >"$1"
+ EOF
+ test_set_editor "$(pwd)"/fake-editor.sh
+}
+
+expect_log () {
+ git log --format="%s" >actual &&
+ cat >expect &&
+ test_cmp expect actual
+}
+
+expect_tree_entries () {
+ git ls-tree --name-only "$1" >actual &&
+ cat >expect &&
+ test_cmp expect actual
+}
+
+test_expect_success 'refuses to work with merge commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ git branch branch &&
+ test_commit ours &&
+ git switch branch &&
+ test_commit theirs &&
+ git switch - &&
+ git merge theirs &&
+ test_must_fail git history split HEAD 2>err &&
+ test_grep "commit to be split must not be a merge commit" err &&
+ test_must_fail git history split HEAD~ 2>err &&
+ test_grep "cannot rearrange commit history with merges" err
+ )
+'
+
+test_expect_success 'can split up tip commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit initial &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ git symbolic-ref HEAD >expect &&
+ set_fake_editor &&
+ git history split HEAD <<-EOF &&
+ y
+ n
+ EOF
+ git symbolic-ref HEAD >actual &&
+ test_cmp expect actual &&
+
+ expect_log <<-EOF &&
+ split-me
+ split-out commit
+ initial
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ bar
+ initial.t
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ initial.t
+ EOF
+ )
+'
+
+test_expect_success 'can split up root commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m root &&
+ test_commit tip &&
+
+ set_fake_editor &&
+ git history split HEAD~ <<-EOF &&
+ y
+ n
+ EOF
+
+ expect_log <<-EOF &&
+ tip
+ root
+ split-out commit
+ EOF
+
+ expect_tree_entries HEAD~2 <<-EOF &&
+ bar
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ bar
+ foo
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ tip.t
+ EOF
+ )
+'
+
+test_expect_success 'can split up in-between commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit initial &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+ test_commit tip &&
+
+ set_fake_editor &&
+ git history split HEAD~ <<-EOF &&
+ y
+ n
+ EOF
+
+ expect_log <<-EOF &&
+ tip
+ split-me
+ split-out commit
+ initial
+ EOF
+
+ expect_tree_entries HEAD~2 <<-EOF &&
+ bar
+ initial.t
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ bar
+ foo
+ initial.t
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ initial.t
+ tip.t
+ EOF
+ )
+'
+
+test_expect_success 'can pick multiple hunks' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar baz foo qux &&
+ git add . &&
+ git commit -m split-me &&
+
+ git history split HEAD -m "split-out commit" <<-EOF &&
+ y
+ n
+ y
+ n
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ bar
+ foo
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ baz
+ foo
+ qux
+ EOF
+ )
+'
+
+
+test_expect_success 'can use only last hunk' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ git history split HEAD -m "split-out commit" <<-EOF &&
+ n
+ y
+ EOF
+
+ expect_log <<-EOF &&
+ split-me
+ split-out commit
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ foo
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ EOF
+ )
+'
+
+test_expect_success 'aborts with empty commit message' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ test_must_fail git history split HEAD -m "" <<-EOF 2>err &&
+ y
+ n
+ EOF
+ test_grep "Aborting commit due to empty commit message." err
+ )
+'
+
+test_expect_success 'can specify message via option' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ git history split HEAD -m "message option" <<-EOF &&
+ y
+ n
+ EOF
+
+ expect_log <<-EOF
+ split-me
+ message option
+ EOF
+ )
+'
+
+test_expect_success 'commit message editor sees split-out changes' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ write_script fake-editor.sh <<-\EOF &&
+ cp "$1" . &&
+ echo "some commit message" >>"$1"
+ EOF
+ test_set_editor "$(pwd)"/fake-editor.sh &&
+
+ git history split HEAD <<-EOF &&
+ y
+ n
+ EOF
+
+ cat >expect <<-EOF &&
+
+ # Please enter the commit message for the split-out changes. Lines starting
+ # with ${SQ}#${SQ} will be kept; you may remove them yourself if you want to.
+ # Changes to be committed:
+ # new file: bar
+ #
+ EOF
+ test_cmp expect COMMIT_EDITMSG &&
+
+ expect_log <<-EOF
+ split-me
+ some commit message
+ EOF
+ )
+'
+
+test_expect_success 'can use pathspec to limit what gets split' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ git history split HEAD -m "message option" -- foo <<-EOF &&
+ y
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ foo
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ EOF
+ )
+'
+
+test_expect_success 'refuses to create empty split-out commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ test_must_fail git history split HEAD 2>err <<-EOF &&
+ n
+ n
+ EOF
+ test_grep "split commit is empty" err
+ )
+'
+
+test_expect_success 'hooks are executed for rewritten commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+ old_head=$(git rev-parse HEAD) &&
+
+ write_script .git/hooks/prepare-commit-msg <<-EOF &&
+ touch "$(pwd)/hooks.log"
+ EOF
+ write_script .git/hooks/post-commit <<-EOF &&
+ touch "$(pwd)/hooks.log"
+ EOF
+ write_script .git/hooks/post-rewrite <<-EOF &&
+ touch "$(pwd)/hooks.log"
+ EOF
+
+ set_fake_editor &&
+ git history split HEAD <<-EOF &&
+ y
+ n
+ EOF
+
+ expect_log <<-EOF &&
+ split-me
+ split-out commit
+ EOF
+
+ test_path_is_missing hooks.log
+ )
+'
+
+test_expect_success 'refuses to create empty original commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ test_must_fail git history split HEAD 2>err <<-EOF &&
+ y
+ y
+ EOF
+ test_grep "split commit tree matches original commit" err
+ )
+'
+
+test_expect_success 'retains changes in the worktree and index' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ echo a >a &&
+ echo b >b &&
+ git add . &&
+ git commit -m "initial commit" &&
+ echo a-modified >a &&
+ echo b-modified >b &&
+ git add b &&
+ git history split HEAD -m a-only <<-EOF &&
+ y
+ n
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ a
+ EOF
+ expect_tree_entries HEAD <<-EOF &&
+ a
+ b
+ EOF
+
+ cat >expect <<-\EOF &&
+ M a
+ M b
+ ?? actual
+ ?? expect
+ EOF
+ git status --porcelain >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_done
--
2.51.0.700.g236ee7b076.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH v4 12/12] builtin/history: implement "split" subcommand
2025-10-01 15:57 ` [PATCH v4 12/12] builtin/history: implement "split" subcommand Patrick Steinhardt
@ 2025-10-14 13:38 ` Karthik Nayak
2025-10-21 11:44 ` Patrick Steinhardt
0 siblings, 1 reply; 278+ messages in thread
From: Karthik Nayak @ 2025-10-14 13:38 UTC (permalink / raw)
To: Patrick Steinhardt, git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren
[-- Attachment #1: Type: text/plain, Size: 7898 bytes --]
Patrick Steinhardt <ps@pks.im> writes:
> It is quite a common use case that one wants to split up one commit into
> multiple commits by moving parts of the changes of the original commit
> out into a separate commit. This is quite an involved operation though:
>
> 1. Identify the commit in question that is to be dropped.
>
> 2. Perform an interactive rebase on top of that commit's parent.
>
> 3. Modify the instruction sheet to "edit" the commit that is to be
> split up.
>
> 4. Drop the commit via "git reset HEAD~".
>
> 5. Stage changes that should go into the first commit and commit it.
>
> 6. Stage changes that should go into the second commit and commit it.
>
> 7. Finalize the rebase.
>
> This is quite complex, and overall I would claim that most people who
> are not experts in Git would struggle with this flow.
>
> Introduce a new "split" subcommand for git-history(1) to make this way
> easier. All the user needs to do is to say `git history split $COMMIT`.
> From hereon, Git asks the user which parts of the commit shall be moved
> out into a separate commit and, once done, asks the user for the commit
> message. Git then creates that split-out commit and applies the original
> commit on top of it.
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> Documentation/git-history.adoc | 62 ++++++
> builtin/history.c | 225 +++++++++++++++++++++
> t/meson.build | 1 +
> t/t3452-history-split.sh | 432 +++++++++++++++++++++++++++++++++++++++++
> 4 files changed, 720 insertions(+)
>
> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
> index b55babe206..83d675afea 100644
> --- a/Documentation/git-history.adoc
> +++ b/Documentation/git-history.adoc
> @@ -10,6 +10,7 @@ SYNOPSIS
> [synopsis]
> git history [<options>]
> git history reword [<options>] <commit>
> +git history split [<options>] <commit> [--] [<pathspec>...]
>
> DESCRIPTION
> -----------
> @@ -40,6 +41,26 @@ rewrite history in different ways:
> provided, then this command will spawn an editor with the current
> message of that commit.
>
> +`split [--message=<message>] <commit> [--] [<pathspec>...]`::
> + Interactively split up <commit> into two commits by choosing
> + hunks introduced by it that will be moved into the new split-out
> + commit. These hunks will then be written into a new commit that
> + becomes the parent of the previous commit. The original commit
> + stays intact, except that its parent will be the newly split-out
> + commit.
>
So in essence we do this:
Before split:
P1 ── C0 ── C1 ── ... ── CN
└─(target) └─(HEAD)
After split:
P1 ── S0 ── C0' ── C1 ── ...... ── CN
│ └─(modified original) └─(HEAD)
└─(split-out hunks)
I do wonder if S0 should contain the existing message and the new
message should go to C0'. So perhaps more like
After split:
P1 ── C0' ── S0 ── C1 ── ..... ── CN
│ └─(split-out hunks) └─(HEAD)
└─(modified original)
Mostly because when you say split, I would assume we keep the original
as is and add on top of it. I don't really have a strong argument though
:)
[snip]
> +EXAMPLES
> +--------
> +
> +Split a commit
> +~~~~~~~~~~~~~~
> +
> +----------
> +$ git log --stat --oneline
> +3f81232 (HEAD -> main) original
> + bar | 1 +
> + foo | 1 +
> + 2 files changed, 2 insertions(+)
> +
> +$ git history split HEAD --message="split-out commit"
> +diff --git a/bar b/bar
> +new file mode 100644
> +index 0000000..5716ca5
> +--- /dev/null
> ++++ b/bar
> +@@ -0,0 +1 @@
> ++bar
> +(1/1) Stage addition [y,n,q,a,d,e,p,?]? y
> +
> +diff --git a/foo b/foo
> +new file mode 100644
> +index 0000000..257cc56
> +--- /dev/null
> ++++ b/foo
> +@@ -0,0 +1 @@
> ++foo
> +(1/1) Stage addition [y,n,q,a,d,e,p,?]? n
> +
> +$ git log --stat --oneline
> +7cebe64 (HEAD -> main) original
> + foo | 1 +
> + 1 file changed, 1 insertion(+)
> +d1582f3 split-out commit
> + bar | 1 +
> + 1 file changed, 1 insertion(+)
> +----------
> +
It's really nice to have examples.
[snip]
> +
> +static int cmd_history_split(int argc,
> + const char **argv,
> + const char *prefix,
> + struct repository *repo)
> +{
> + const char * const usage[] = {
> + N_("git history split [<options>] <commit>"),
> + NULL,
> + };
should be '*const' here.
> + const char *commit_message = NULL;
> + struct option options[] = {
> + OPT_STRING('m', "message", &commit_message, N_("message"), N_("commit message")),
> + OPT_END(),
> + };
> + struct oidmap rewritten_commits = OIDMAP_INIT;
> + struct commit *original_commit, *parent, *head;
> + struct strvec commits = STRVEC_INIT;
> + struct commit_list *list = NULL;
> + struct object_id split_commits[2];
> + struct pathspec pathspec = { 0 };
> + int ret;
> +
> + argc = parse_options(argc, argv, prefix, options, usage, 0);
> + if (argc < 1) {
> + ret = error(_("command expects a revision"));
> + goto out;
> + }
> + repo_config(repo, git_default_config, NULL);
> +
> + original_commit = lookup_commit_reference_by_name(argv[0]);
> + if (!original_commit) {
> + ret = error(_("commit to be split cannot be found: %s"), argv[0]);
> + goto out;
> + }
> +
> + if (original_commit->parents && original_commit->parents->next) {
> + ret = error(_("commit to be split must not be a merge commit"));
> + goto out;
> + }
Do we need this? Since we also check for merges in `collect_commits()` below.
> + parent = original_commit->parents ? original_commit->parents->item : NULL;
> + if (parent && repo_parse_commit(repo, parent)) {
> + ret = error(_("unable to parse commit %s"),
> + oid_to_hex(&parent->object.oid));
> + goto out;
> + }
> +
> + head = lookup_commit_reference_by_name("HEAD");
> + if (!head) {
> + ret = error(_("could not resolve HEAD to a commit"));
> + goto out;
> + }
> +
> + commit_list_append(original_commit, &list);
> + if (!repo_is_descendant_of(repo, original_commit, list)) {
> + ret = error (_("split commit must be reachable from current HEAD commit"));
s/error /error/
> + goto out;
> + }
> +
This is also checked within collect_commits(), no?
> + parse_pathspec(&pathspec, 0,
> + PATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,
> + prefix, argv + 1);
> +
> + /*
> + * Collect the list of commits that we'll have to reapply now already.
> + * This ensures that we'll abort early on in case the range of commits
> + * contains merges, which we do not yet handle.
> + */
Comment spacing is off here.
> + ret = collect_commits(repo, parent, head, &commits);
> + if (ret < 0)
> + goto out;
> +
> + /*
> + * Then we split up the commit and replace the original commit with the
> + * new new ones.
> + */
s/new/new
> + ret = split_commit(repo, original_commit, &pathspec,
> + commit_message, split_commits);
> + if (ret < 0)
> + goto out;
> +
This one was straight forward, it handles adding the hunks to the index
creating the two commits and linking them.
> + replace_commits(&commits, &original_commit->object.oid,
> + split_commits, ARRAY_SIZE(split_commits));
> +
Nice. We use the function introduced earlier to replace the replace the
commits with the new two commits.
> + ret = apply_commits(repo, &commits, parent, head, "split");
> + if (ret < 0)
> + goto out;
> +
> + ret = 0;
> +
> +out:
> + oidmap_clear(&rewritten_commits, 0);
> + clear_pathspec(&pathspec);
> + strvec_clear(&commits);
> + free_commit_list(list);
> + return ret;
> +}
[snip]
The tests look good. Nothing to add there.
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 690 bytes --]
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH v4 12/12] builtin/history: implement "split" subcommand
2025-10-14 13:38 ` Karthik Nayak
@ 2025-10-21 11:44 ` Patrick Steinhardt
2025-10-21 21:19 ` D. Ben Knoble
0 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-21 11:44 UTC (permalink / raw)
To: Karthik Nayak
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren
On Tue, Oct 14, 2025 at 09:38:51AM -0400, Karthik Nayak wrote:
> Patrick Steinhardt <ps@pks.im> writes:
> > diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
> > index b55babe206..83d675afea 100644
> > --- a/Documentation/git-history.adoc
> > +++ b/Documentation/git-history.adoc
> > @@ -40,6 +41,26 @@ rewrite history in different ways:
> > provided, then this command will spawn an editor with the current
> > message of that commit.
> >
> > +`split [--message=<message>] <commit> [--] [<pathspec>...]`::
> > + Interactively split up <commit> into two commits by choosing
> > + hunks introduced by it that will be moved into the new split-out
> > + commit. These hunks will then be written into a new commit that
> > + becomes the parent of the previous commit. The original commit
> > + stays intact, except that its parent will be the newly split-out
> > + commit.
> >
>
> So in essence we do this:
>
> Before split:
> P1 ── C0 ── C1 ── ... ── CN
> └─(target) └─(HEAD)
>
> After split:
> P1 ── S0 ── C0' ── C1 ── ...... ── CN
> │ └─(modified original) └─(HEAD)
> └─(split-out hunks)
>
> I do wonder if S0 should contain the existing message and the new
> message should go to C0'. So perhaps more like
>
> After split:
> P1 ── C0' ── S0 ── C1 ── ..... ── CN
> │ └─(split-out hunks) └─(HEAD)
> └─(modified original)
>
> Mostly because when you say split, I would assume we keep the original
> as is and add on top of it. I don't really have a strong argument though
> :)
Yeah, this has already caused some discussion beforehand. I guess you
can argue either way, and the suggestion from others was to simply allow
the user to edit both commit messages.
I don't at all mind going into that direction, but I wonder how to call
the "--message" switch in that case. We could of course just call these
"--first-message" and "--second-message", but that feels somewhat
awkward.
Also, I already have it in my mind that it would be cool to extend this
command so that you can split into arbitrary many commits. That is,
after you have split out the first commit we simply go back into
interactive mode to create a second commit tree. Rinse and repeat until
we have no chunks left anymore. But if we had such a mode though, then
numbered parameters don't make much sense anymore.
An alternative could be to just accept multiple "-m" arguments, and we
then apply the messages to the respective commits? Dunno.
> > + const char *commit_message = NULL;
> > + struct option options[] = {
> > + OPT_STRING('m', "message", &commit_message, N_("message"), N_("commit message")),
> > + OPT_END(),
> > + };
> > + struct oidmap rewritten_commits = OIDMAP_INIT;
> > + struct commit *original_commit, *parent, *head;
> > + struct strvec commits = STRVEC_INIT;
> > + struct commit_list *list = NULL;
> > + struct object_id split_commits[2];
> > + struct pathspec pathspec = { 0 };
> > + int ret;
> > +
> > + argc = parse_options(argc, argv, prefix, options, usage, 0);
> > + if (argc < 1) {
> > + ret = error(_("command expects a revision"));
> > + goto out;
> > + }
> > + repo_config(repo, git_default_config, NULL);
> > +
> > + original_commit = lookup_commit_reference_by_name(argv[0]);
> > + if (!original_commit) {
> > + ret = error(_("commit to be split cannot be found: %s"), argv[0]);
> > + goto out;
> > + }
> > +
> > + if (original_commit->parents && original_commit->parents->next) {
> > + ret = error(_("commit to be split must not be a merge commit"));
> > + goto out;
> > + }
>
> Do we need this? Since we also check for merges in `collect_commits()` below.
Yeah, we don't indeed.
> > + parent = original_commit->parents ? original_commit->parents->item : NULL;
> > + if (parent && repo_parse_commit(repo, parent)) {
> > + ret = error(_("unable to parse commit %s"),
> > + oid_to_hex(&parent->object.oid));
> > + goto out;
> > + }
> > +
> > + head = lookup_commit_reference_by_name("HEAD");
> > + if (!head) {
> > + ret = error(_("could not resolve HEAD to a commit"));
> > + goto out;
> > + }
> > +
> > + commit_list_append(original_commit, &list);
> > + if (!repo_is_descendant_of(repo, original_commit, list)) {
> > + ret = error (_("split commit must be reachable from current HEAD commit"));
>
> s/error /error/
>
> > + goto out;
> > + }
> > +
>
> This is also checked within collect_commits(), no?
The problem is rather that the check is wrong: we check whether the
original commit is a descendent of itself, which is always true. What we
actually want to check though is whether HEAD is a descendant of the
original commit.
The check in `collect_commits()` is slightly different, as we verify
that the range provided to us is actually the same. But that check
shouldn't ever hit if the above check actually triggers.
I'll fix the check here and drop the one in `collect_commits()`.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH v4 12/12] builtin/history: implement "split" subcommand
2025-10-21 11:44 ` Patrick Steinhardt
@ 2025-10-21 21:19 ` D. Ben Knoble
2025-10-27 9:58 ` Patrick Steinhardt
0 siblings, 1 reply; 278+ messages in thread
From: D. Ben Knoble @ 2025-10-21 21:19 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: Karthik Nayak, git, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren
On Tue, Oct 21, 2025 at 7:44 AM Patrick Steinhardt <ps@pks.im> wrote:
>
> On Tue, Oct 14, 2025 at 09:38:51AM -0400, Karthik Nayak wrote:
> > Patrick Steinhardt <ps@pks.im> writes:
> > > diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
> > > index b55babe206..83d675afea 100644
> > > --- a/Documentation/git-history.adoc
> > > +++ b/Documentation/git-history.adoc
> > > @@ -40,6 +41,26 @@ rewrite history in different ways:
> > > provided, then this command will spawn an editor with the current
> > > message of that commit.
> > >
> > > +`split [--message=<message>] <commit> [--] [<pathspec>...]`::
> > > + Interactively split up <commit> into two commits by choosing
> > > + hunks introduced by it that will be moved into the new split-out
> > > + commit. These hunks will then be written into a new commit that
> > > + becomes the parent of the previous commit. The original commit
> > > + stays intact, except that its parent will be the newly split-out
> > > + commit.
> > >
> >
> > So in essence we do this:
> >
> > Before split:
> > P1 ── C0 ── C1 ── ... ── CN
> > └─(target) └─(HEAD)
> >
> > After split:
> > P1 ── S0 ── C0' ── C1 ── ...... ── CN
> > │ └─(modified original) └─(HEAD)
> > └─(split-out hunks)
> >
> > I do wonder if S0 should contain the existing message and the new
> > message should go to C0'. So perhaps more like
> >
> > After split:
> > P1 ── C0' ── S0 ── C1 ── ..... ── CN
> > │ └─(split-out hunks) └─(HEAD)
> > └─(modified original)
> >
> > Mostly because when you say split, I would assume we keep the original
> > as is and add on top of it. I don't really have a strong argument though
> > :)
>
> Yeah, this has already caused some discussion beforehand. I guess you
> can argue either way, and the suggestion from others was to simply allow
> the user to edit both commit messages.
>
> I don't at all mind going into that direction, but I wonder how to call
> the "--message" switch in that case. We could of course just call these
> "--first-message" and "--second-message", but that feels somewhat
> awkward.
>
> Also, I already have it in my mind that it would be cool to extend this
> command so that you can split into arbitrary many commits. That is,
> after you have split out the first commit we simply go back into
> interactive mode to create a second commit tree. Rinse and repeat until
> we have no chunks left anymore. But if we had such a mode though, then
> numbered parameters don't make much sense anymore.
>
> An alternative could be to just accept multiple "-m" arguments, and we
> then apply the messages to the respective commits? Dunno.
Or *gasp* not support "-m" at all, and require the user to put
_something_ in an editor? 🤔
--
D. Ben Knoble
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v4 12/12] builtin/history: implement "split" subcommand
2025-10-21 21:19 ` D. Ben Knoble
@ 2025-10-27 9:58 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-27 9:58 UTC (permalink / raw)
To: D. Ben Knoble
Cc: Karthik Nayak, git, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren
On Tue, Oct 21, 2025 at 05:19:19PM -0400, D. Ben Knoble wrote:
> On Tue, Oct 21, 2025 at 7:44 AM Patrick Steinhardt <ps@pks.im> wrote:
> > On Tue, Oct 14, 2025 at 09:38:51AM -0400, Karthik Nayak wrote:
> > > Patrick Steinhardt <ps@pks.im> writes:
> > > > diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
> > > > index b55babe206..83d675afea 100644
> > > > --- a/Documentation/git-history.adoc
> > > > +++ b/Documentation/git-history.adoc
> > > > @@ -40,6 +41,26 @@ rewrite history in different ways:
> > > > provided, then this command will spawn an editor with the current
> > > > message of that commit.
> > > >
> > > > +`split [--message=<message>] <commit> [--] [<pathspec>...]`::
> > > > + Interactively split up <commit> into two commits by choosing
> > > > + hunks introduced by it that will be moved into the new split-out
> > > > + commit. These hunks will then be written into a new commit that
> > > > + becomes the parent of the previous commit. The original commit
> > > > + stays intact, except that its parent will be the newly split-out
> > > > + commit.
> > > >
> > >
> > > So in essence we do this:
> > >
> > > Before split:
> > > P1 ── C0 ── C1 ── ... ── CN
> > > └─(target) └─(HEAD)
> > >
> > > After split:
> > > P1 ── S0 ── C0' ── C1 ── ...... ── CN
> > > │ └─(modified original) └─(HEAD)
> > > └─(split-out hunks)
> > >
> > > I do wonder if S0 should contain the existing message and the new
> > > message should go to C0'. So perhaps more like
> > >
> > > After split:
> > > P1 ── C0' ── S0 ── C1 ── ..... ── CN
> > > │ └─(split-out hunks) └─(HEAD)
> > > └─(modified original)
> > >
> > > Mostly because when you say split, I would assume we keep the original
> > > as is and add on top of it. I don't really have a strong argument though
> > > :)
> >
> > Yeah, this has already caused some discussion beforehand. I guess you
> > can argue either way, and the suggestion from others was to simply allow
> > the user to edit both commit messages.
> >
> > I don't at all mind going into that direction, but I wonder how to call
> > the "--message" switch in that case. We could of course just call these
> > "--first-message" and "--second-message", but that feels somewhat
> > awkward.
> >
> > Also, I already have it in my mind that it would be cool to extend this
> > command so that you can split into arbitrary many commits. That is,
> > after you have split out the first commit we simply go back into
> > interactive mode to create a second commit tree. Rinse and repeat until
> > we have no chunks left anymore. But if we had such a mode though, then
> > numbered parameters don't make much sense anymore.
> >
> > An alternative could be to just accept multiple "-m" arguments, and we
> > then apply the messages to the respective commits? Dunno.
>
> Or *gasp* not support "-m" at all, and require the user to put
> _something_ in an editor? 🤔
Let's do that for now and discuss in more detail in follow-up patch
series.
Thanks!
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v4 00/12] Introduce git-history(1) command for easy history editing
2025-10-01 15:57 ` [PATCH v4 00/12] " Patrick Steinhardt
` (11 preceding siblings ...)
2025-10-01 15:57 ` [PATCH v4 12/12] builtin/history: implement "split" subcommand Patrick Steinhardt
@ 2025-10-14 13:41 ` Karthik Nayak
2025-10-14 16:47 ` Junio C Hamano
13 siblings, 0 replies; 278+ messages in thread
From: Karthik Nayak @ 2025-10-14 13:41 UTC (permalink / raw)
To: Patrick Steinhardt, git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren
[-- Attachment #1: Type: text/plain, Size: 1525 bytes --]
Patrick Steinhardt <ps@pks.im> writes:
> Hi,
>
> over recent months I've been playing around with Jujutsu quite
> frequently. While I still prefer using Git, there's been a couple
> features in it that I really like and that I'd like to have in Git, as
> well.
>
> A copule of these features relate to history editing. Most importantly,
> I really dig the following commands:
>
> - jj-abandon(1) to drop a specific commit from your history.
>
> - jj-absorb(1) to take some changes and automatically apply them to
> commits in your history that last modified the respective hunks.
>
> - jj-split(1) to split a commit into two.
>
> - jj-new(1) to insert a new commit after or before a specific other
> commit.
>
> Not all of these commands can be ported directly into Git. jj-new(1) for
> example doesn't really make a ton of sense for us, I'd claim. But some
> of these commands _do_ make sense.
>
> This patch series is a starting point for such a command. I've
> significantly slimmed it down from the first couple revisions now
> following the discussions at the Contributor's Summit yesterday. This
> was my intent anyway, as I already mentioned on the last iteration.
>
Hello,
I'm jumping in directly to review the fourth version of the patch
series. As such I might have missed discussions in the prev versions.
Apart from small comments and questions, it looks to be in a good shape.
I think the addition of changes would perhaps warrant a re-roll but we
should be close :)
Thanks,
Karthik
[snip]
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 690 bytes --]
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH v4 00/12] Introduce git-history(1) command for easy history editing
2025-10-01 15:57 ` [PATCH v4 00/12] " Patrick Steinhardt
` (12 preceding siblings ...)
2025-10-14 13:41 ` [PATCH v4 00/12] Introduce git-history(1) command for easy history editing Karthik Nayak
@ 2025-10-14 16:47 ` Junio C Hamano
13 siblings, 0 replies; 278+ messages in thread
From: Junio C Hamano @ 2025-10-14 16:47 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, D. Ben Knoble, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk, Elijah Newren
Patrick Steinhardt <ps@pks.im> writes:
> over recent months I've been playing around with Jujutsu quite
> frequently. While I still prefer using Git, there's been a couple
> features in it that I really like and that I'd like to have in Git, as
> well.
> ...
> Changes in v4:
> - I've rebuilt the patch series. It is now based on 821f583da6 (The
> thirteenth batcn, 2025-09-29) with sa/replay-atomic-ref-updates
> at 665c66a743 (replay: make atomic ref updates the default behavior,
> 2025-09-27) merged into it. This should fix all conflicts with seen.
> - I've reworked this patch series to use the same infra as
> git-replay(1), as discussed during the Contributor's Summit.
> - I've slimmed down the patch series to only tackle those commands
> that cannot result in a conflict to keep it simple. I also learned
> that Elijah has been working on a "git replay edit" command, so I
> dropped that command so that we can instead use his version.
> - During the Contributor's Summit we have agreed that for now, we
> won't care about hook execution just yet. This may be backfilled at
> a later point in time.
> - I dropped "commit.verbose" handling for now, as my understanding of
> it was wrong at first. This is something we should backfill.
> - Link to v3: https://lore.kernel.org/r/20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im
What is queued near the tip of 'seen' is v4 but rebased on the
updated version of sa/replay-atomic-ref-updates that came from
<20251013183311.33329-1-siddharthasthana31@gmail.com>. The rebase
only had a slight conflict at [PATCH v4 02/12].
The result based on the same 821f583da6 (The thirteenth batcn,
2025-09-29) with sa/replay-atomic-ref-updates at a07d37b3 (replay:
add replay.defaultAction config option, 2025-10-14).
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH v5 00/12] Introduce git-history(1) command for easy history editing
2025-08-19 10:55 [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing Patrick Steinhardt
` (17 preceding siblings ...)
2025-10-01 15:57 ` [PATCH v4 00/12] " Patrick Steinhardt
@ 2025-10-21 14:15 ` Patrick Steinhardt
2025-10-21 14:15 ` [PATCH v5 01/12] wt-status: provide function to expose status for trees Patrick Steinhardt
` (12 more replies)
2025-10-27 11:33 ` [PATCH v6 00/11] " Patrick Steinhardt
2025-12-03 10:48 ` [PATCH v7 00/12] " Patrick Steinhardt
20 siblings, 13 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-21 14:15 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
Hi,
over recent months I've been playing around with Jujutsu quite
frequently. While I still prefer using Git, there's been a couple
features in it that I really like and that I'd like to have in Git, as
well.
A copule of these features relate to history editing. Most importantly,
I really dig the following commands:
- jj-abandon(1) to drop a specific commit from your history.
- jj-absorb(1) to take some changes and automatically apply them to
commits in your history that last modified the respective hunks.
- jj-split(1) to split a commit into two.
- jj-new(1) to insert a new commit after or before a specific other
commit.
Not all of these commands can be ported directly into Git. jj-new(1) for
example doesn't really make a ton of sense for us, I'd claim. But some
of these commands _do_ make sense.
This patch series is a starting point for such a command. I've
significantly slimmed it down from the first couple revisions now
following the discussions at the Contributor's Summit yesterday. This
was my intent anyway, as I already mentioned on the last iteration.
Changes in v5:
- I've changed the patch series to be based on top of 133d151831 (The
twenty-first batch, 2025-10-20) with sa/replay-atomic-ref-updates at
a1c22e627e (SQAUASH??? t0450 band-aid, 2025-10-14) merged into it.
This is one the one hand to fix a conflict, but also to get some of
the CI updates to make GitLab CI work again.
- Some slight commit message improvements.
- Deduplicate subcommand usage strings by using defines.
- Fix the desendancy checks to properly verify that HEAD is a
descendant of the commit to be rewritten. Also add some tests for
this.
- Fix the hint that mentions that lines starting with the comment
character will be tripped after having written the commit message.
- Move an include to the correct commit.
- Link to v4: https://lore.kernel.org/r/20251001-b4-pks-history-builtin-v4-0-8e61ddb86317@pks.im
Changes in v4:
- I've rebuilt the patch series. It is now based on 821f583da6 (The
thirteenth batcn, 2025-09-29) with sa/replay-atomic-ref-updates
at 665c66a743 (replay: make atomic ref updates the default behavior,
2025-09-27) merged into it. This should fix all conflicts with seen.
- I've reworked this patch series to use the same infra as
git-replay(1), as discussed during the Contributor's Summit.
- I've slimmed down the patch series to only tackle those commands
that cannot result in a conflict to keep it simple. I also learned
that Elijah has been working on a "git replay edit" command, so I
dropped that command so that we can instead use his version.
- During the Contributor's Summit we have agreed that for now, we
won't care about hook execution just yet. This may be backfilled at
a later point in time.
- I dropped "commit.verbose" handling for now, as my understanding of
it was wrong at first. This is something we should backfill.
- Link to v3: https://lore.kernel.org/r/20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im
Changes in v3:
- Add logic to drive the "post-rewrite" hook and add tests to verify
that all hooks are executed as expected.
- Deduplicate logic to turn a replay action into a todo command.
- Move the addition of tests for the top-level git-history(1) command
to the correct commit.
- Some smaller commit message fixes.
- Honor "commit.verbose".
- Fix copy-paste error with an error message.
- Link to v2: https://lore.kernel.org/r/20250824-b4-pks-history-builtin-v2-0-964ac12f65bd@pks.im
Changes in v2:
- Add a new "reword" subcommand.
- List git-history(1) in "command-list.txt".
- Add some missing error handling.
- Simplify calling convention of `apply_commits()` to handle root
commits internally instead of requiring every caller to do so.
- Add tests to verify that git-history(1) refuses to work with changes
in the worktree or index.
- Mark git-history(1) as experimental.
- Introduce commands to manage interrupted history edits.
- A bunch of improvements to the manpage.
- Link to v1: https://lore.kernel.org/r/20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im
Thanks!
Patrick
---
Patrick Steinhardt (12):
wt-status: provide function to expose status for trees
replay: extract logic to pick commits
replay: stop using `the_repository`
replay: parse commits before dereferencing them
builtin: add new "history" command
builtin/history: implement "reword" subcommand
add-patch: split out header from "add-interactive.h"
add-patch: split out `struct interactive_options`
add-patch: remove dependency on "add-interactive" subsystem
add-patch: add support for in-memory index patching
cache-tree: allow writing in-memory index as tree
builtin/history: implement "split" subcommand
.gitignore | 1 +
Documentation/git-history.adoc | 114 ++++++++
Documentation/meson.build | 1 +
Makefile | 2 +
add-interactive.c | 174 +++---------
add-interactive.h | 46 +---
add-patch.c | 297 +++++++++++++++++---
add-patch.h | 64 +++++
builtin.h | 1 +
builtin/add.c | 22 +-
builtin/checkout.c | 7 +-
builtin/commit.c | 16 +-
builtin/history.c | 600 +++++++++++++++++++++++++++++++++++++++++
builtin/replay.c | 110 +-------
builtin/reset.c | 16 +-
builtin/stash.c | 46 ++--
cache-tree.c | 5 +-
cache-tree.h | 3 +-
command-list.txt | 1 +
commit.h | 2 +-
git.c | 1 +
meson.build | 2 +
replay.c | 118 ++++++++
replay.h | 18 ++
t/meson.build | 3 +
t/t3450-history.sh | 17 ++
t/t3451-history-reword.sh | 217 +++++++++++++++
t/t3452-history-split.sh | 447 ++++++++++++++++++++++++++++++
wt-status.c | 24 ++
wt-status.h | 3 +
30 files changed, 2001 insertions(+), 377 deletions(-)
Range-diff versus v4:
1: 5073f973555 ! 1: 74adad5ad51 wt-status: provide function to expose status for trees
@@ Commit message
includes information around whether the working tree or the index have
any changes.
- We're about to introduce a new command though where the changes in
- neither of them are actually relevant to us. Instead, what we want is to
- format the changes between two different trees. While it is a little bit
- of a stretch to add this as functionality to _working tree_ status, it
+ We're about to introduce a new command where the changes in neither of
+ them are actually relevant to us. Instead, what we want is to format the
+ changes between two different trees. While it is a little bit of a
+ stretch to add this as functionality to _working tree_ status, it
doesn't make any sense to open-code this functionality, either.
Implement a new function `wt_status_collect_changes_trees()` that diffs
2: b21049120fa ! 2: e92d28e9033 replay: extract logic to pick commits
@@ Commit message
Signed-off-by: Patrick Steinhardt <ps@pks.im>
## Makefile ##
-@@ Makefile: LIB_OBJS += refs/ref-cache.o
- LIB_OBJS += refspec.o
+@@ Makefile: LIB_OBJS += reftable/tree.o
+ LIB_OBJS += reftable/writer.o
LIB_OBJS += remote.o
LIB_OBJS += replace-object.o
+LIB_OBJS += replay.o
@@ builtin/replay.c: static void determine_replay_mode(struct repository *repo,
- return create_commit(repo, result->tree, pickme, replayed_base);
-}
-
- static int add_ref_to_transaction(struct ref_transaction *transaction,
- const char *refname,
- const struct object_id *new_oid,
+ static int handle_ref_update(const char *mode,
+ struct ref_transaction *transaction,
+ const char *refname,
@@ builtin/replay.c: int cmd_replay(int argc,
if (commit->parents->next)
die(_("replaying merge commits is not supported yet!"));
3: be13cd322df = 3: 652fbddc10e replay: stop using `the_repository`
4: 7af626c0fbc = 4: 0c9a409eea7 replay: parse commits before dereferencing them
5: ac152bca162 ! 5: d568ffc1f65 builtin: add new "history" command
@@ Commit message
- Multiple commits should be squashed into one.
- While these operations are all doable, it often feels needlessly cludgy
+ While these operations are all doable, it often feels needlessly kludgey
to do so by doing an interactive rebase, using the editor to say what
one wants, and then perform the actions. Furthermore, some operations
like splitting up a commit into two are way more involved than that and
@@ Commit message
Add a new "history" command to plug this gap. This command will have
several different subcommands to imperatively rewrite history for common
- use cases like the above. These commands will be implemented in
+ use cases like the above. These subcommands will be implemented in
subsequent commits.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
@@ Documentation/git-history.adoc (new)
+history.
+
+This command is similar to linkgit:git-rebase[1] and uses the same
-+underlying machinery. You should use rebases if you either want to
-+reapply a range of commits onto a different base, or interactive rebases
-+if you want to edit a range of commits.
++underlying machinery. You should use rebases if you want to reapply a range of
++commits onto a different base, or interactive rebases if you want to edit a
++range of commits.
+
+Note that this command does not (yet) work with histories that contain
+merges. You should use linkgit:git-rebase[1] with the `--rebase-merges`
6: 8519861bfb0 ! 6: 39acd4734c3 builtin/history: implement "reword" subcommand
@@ builtin/history.c
+#include "environment.h"
#include "gettext.h"
+#include "hex.h"
-+#include "oidmap.h"
#include "parse-options.h"
+#include "refs.h"
+#include "replay.h"
@@ builtin/history.c
+#include "tree.h"
+#include "wt-status.h"
+
++#define GIT_HISTORY_REWORD_USAGE N_("git history reword [<options>] <commit>")
++
+static int collect_commits(struct repository *repo,
+ struct commit *old_commit,
+ struct commit *new_commit,
@@ builtin/history.c
+ .assume_dashdash = 1,
+ };
+ struct strvec revisions = STRVEC_INIT;
-+ struct commit_list *from_list = NULL;
+ struct commit *child;
+ struct rev_info rev = { 0 };
+ int ret;
+
-+ /*
-+ * Check that the old commit actually is an ancestor of HEAD. If not
-+ * the whole request becomes nonsensical.
-+ */
-+ if (old_commit) {
-+ commit_list_insert(old_commit, &from_list);
-+ if (!repo_is_descendant_of(repo, new_commit, from_list)) {
-+ ret = error(_("commit must be reachable from current HEAD commit"));
-+ goto out;
-+ }
-+ }
-+
+ repo_init_revisions(repo, &rev, NULL);
+ strvec_push(&revisions, "");
+ strvec_push(&revisions, oid_to_hex(&new_commit->object.oid));
@@ builtin/history.c
+ ret = 0;
+
+out:
-+ free_commit_list(from_list);
+ strvec_clear(&revisions);
+ release_revisions(&rev);
+ reset_revision_walk();
@@ builtin/history.c
+ if (!provided_message) {
+ const char *path = git_path_commit_editmsg();
+ const char *hint =
-+ _("Please enter the commit message for the %s changes. Lines starting\n"
-+ "with '%s' will be kept; you may remove them yourself if you want to.\n");
++ _("Please enter the commit message for the %s changes."
++ " Lines starting\nwith '%s' will be ignored.\n");
+ struct wt_status s;
+
+ strbuf_addstr(out, default_message);
@@ builtin/history.c
+ struct repository *repo)
+{
+ const char * const usage[] = {
-+ N_("git history reword [<options>] <commit>"),
++ GIT_HISTORY_REWORD_USAGE,
+ NULL,
+ };
+ const char *commit_message = NULL;
@@ builtin/history.c
+ struct strvec commits = STRVEC_INIT;
+ struct object_id parent_tree_oid, original_commit_tree_oid;
+ struct object_id rewritten_commit;
++ struct commit_list *from_list = NULL;
+ const char *original_message, *original_body, *ptr;
+ char *original_author = NULL;
+ size_t len;
@@ builtin/history.c
+ ret = error(_("commit to be reworded cannot be found: %s"), argv[0]);
+ goto out;
+ }
-+ if (repo_parse_commit(repo, original_commit)) {
-+ ret = error(_("unable to parse commit %s"),
-+ oid_to_hex(&original_commit->object.oid));
-+ goto out;
-+ }
+ original_commit_tree_oid = repo_get_commit_tree(repo, original_commit)->object.oid;
+
+ parent = original_commit->parents ? original_commit->parents->item : NULL;
@@ builtin/history.c
+ goto out;
+ }
+
++ commit_list_append(original_commit, &from_list);
++ if (!repo_is_descendant_of(repo, head, from_list)) {
++ ret = error (_("split commit must be reachable from current HEAD commit"));
++ goto out;
++ }
++
+ /*
+ * Collect the list of commits that we'll have to reapply now already.
+ * This ensures that we'll abort early on in case the range of commits
@@ builtin/history.c
+ if (ret < 0)
+ goto out;
+
-+ ret = commit_tree(final_message.buf, final_message.len,
-+ &repo_get_commit_tree(repo, original_commit)->object.oid,
++ ret = commit_tree(final_message.buf, final_message.len, &original_commit_tree_oid,
+ original_commit->parents, &rewritten_commit, original_author, NULL);
+ if (ret < 0) {
+ ret = error(_("failed writing reworded commit"));
@@ builtin/history.c
+
+out:
+ strbuf_release(&final_message);
++ free_commit_list(from_list);
+ strvec_clear(&commits);
+ free(original_author);
+ return ret;
@@ builtin/history.c
{
const char * const usage[] = {
N_("git history [<options>]"),
-+ N_("git history reword [<options>] <commit>"),
++ GIT_HISTORY_REWORD_USAGE,
NULL,
};
+ parse_opt_subcommand_fn *fn = NULL;
@@ t/t3451-history-reword.sh (new)
+ )
+'
+
++test_expect_success 'refuses to work with unrelated commits' '
++ test_when_finished "rm -rf repo" &&
++ git init repo &&
++ (
++ cd repo &&
++ test_commit base &&
++ git branch branch &&
++ test_commit ours &&
++ git switch branch &&
++ test_commit theirs &&
++ test_must_fail git history reword ours 2>err &&
++ test_grep "split commit must be reachable from current HEAD commit" err
++ )
++'
++
+test_expect_success 'can reword tip of a branch' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
@@ t/t3451-history-reword.sh (new)
+ first
+
+ # Please enter the commit message for the reworded changes. Lines starting
-+ # with ${SQ}#${SQ} will be kept; you may remove them yourself if you want to.
++ # with ${SQ}#${SQ} will be ignored.
+ # Changes to be committed:
+ # new file: first.t
+ #
7: 89883c85fa3 = 7: 2e4291a3b87 add-patch: split out header from "add-interactive.h"
8: f1602b8513c ! 8: 715f3a32b82 add-patch: split out `struct interactive_options`
@@ Metadata
## Commit message ##
add-patch: split out `struct interactive_options`
- The `struct add_p_opt` is reused both by our the infra for "git add -p"
- and "git add -i". Users of `run_add_i()` for example are expected to
- pass `struct add_p_opt`. This is somewhat confusing and raises the
- question which options apply to what part of the stack.
+ The `struct add_p_opt` is reused both by our infra for "git add -p" and
+ "git add -i". Users of `run_add_i()` for example are expected to pass
+ `struct add_p_opt`. This is somewhat confusing and raises the question
+ of which options apply to what part of the stack.
But things are even more confusing than that: while callers are expected
to pass in `struct add_p_opt`, these options ultimately get used to
9: 8ef83293ae5 = 9: 94ea67db00c add-patch: remove dependency on "add-interactive" subsystem
10: 385c39356dc ! 10: 19c02eb16de add-patch: add support for in-memory index patching
@@ Commit message
With `run_add_p()` callers have the ability to apply changes from a
specific revision to a repository's index. This infra supports several
different modes, like for example applying changes to the index,
- worktree or both.
+ working tree or both.
One feature that is missing though is the ability to apply changes to an
in-memory index different from the repository's index. Add a new
@@ add-patch.c
#include "object-name.h"
#include "pager.h"
#include "read-cache-ll.h"
+@@ add-patch.c: static struct patch_mode patch_mode_add = {
+ N_("Stage mode change [y,n,q,a,d%s,?]? "),
+ N_("Stage deletion [y,n,q,a,d%s,?]? "),
+ N_("Stage addition [y,n,q,a,d%s,?]? "),
+- N_("Stage this hunk [y,n,q,a,d%s,?]? ")
++ N_("Stage this hunk [y,n,q,a,d%s,?]? "),
+ },
+ .edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk "
+ "will immediately be marked for staging."),
@@ add-patch.c: struct hunk {
struct add_p_state {
11: d26e225c854 = 11: 3161d4bad3f cache-tree: allow writing in-memory index as tree
12: fa878dc362c ! 12: e7a6537ecfe builtin/history: implement "split" subcommand
@@ builtin/history.c
#include "commit.h"
#include "config.h"
@@
+ #include "environment.h"
+ #include "gettext.h"
#include "hex.h"
- #include "oidmap.h"
++#include "oidmap.h"
#include "parse-options.h"
+#include "path.h"
+#include "read-cache.h"
@@ builtin/history.c
#include "sequencer.h"
#include "strvec.h"
#include "tree.h"
+ #include "wt-status.h"
+
+ #define GIT_HISTORY_REWORD_USAGE N_("git history reword [<options>] <commit>")
++#define GIT_HISTORY_SPLIT_USAGE N_("git history split [<options>] <commit> [--] [<pathspec>...]")
+
+ static int collect_commits(struct repository *repo,
+ struct commit *old_commit,
@@ builtin/history.c: static int cmd_history_reword(int argc,
return ret;
}
@@ builtin/history.c: static int cmd_history_reword(int argc,
+ struct repository *repo)
+{
+ const char * const usage[] = {
-+ N_("git history split [<options>] <commit>"),
++ GIT_HISTORY_SPLIT_USAGE,
+ NULL,
+ };
+ const char *commit_message = NULL;
@@ builtin/history.c: static int cmd_history_reword(int argc,
+ struct oidmap rewritten_commits = OIDMAP_INIT;
+ struct commit *original_commit, *parent, *head;
+ struct strvec commits = STRVEC_INIT;
-+ struct commit_list *list = NULL;
++ struct commit_list *from_list = NULL;
+ struct object_id split_commits[2];
+ struct pathspec pathspec = { 0 };
+ int ret;
@@ builtin/history.c: static int cmd_history_reword(int argc,
+ goto out;
+ }
+
-+ if (original_commit->parents && original_commit->parents->next) {
-+ ret = error(_("commit to be split must not be a merge commit"));
-+ goto out;
-+ }
-+
+ parent = original_commit->parents ? original_commit->parents->item : NULL;
+ if (parent && repo_parse_commit(repo, parent)) {
+ ret = error(_("unable to parse commit %s"),
@@ builtin/history.c: static int cmd_history_reword(int argc,
+ goto out;
+ }
+
-+ commit_list_append(original_commit, &list);
-+ if (!repo_is_descendant_of(repo, original_commit, list)) {
-+ ret = error (_("split commit must be reachable from current HEAD commit"));
++ commit_list_append(original_commit, &from_list);
++ if (!repo_is_descendant_of(repo, head, from_list)) {
++ ret = error(_("split commit must be reachable from current HEAD commit"));
+ goto out;
+ }
+
@@ builtin/history.c: static int cmd_history_reword(int argc,
+ prefix, argv + 1);
+
+ /*
-+ * Collect the list of commits that we'll have to reapply now already.
-+ * This ensures that we'll abort early on in case the range of commits
-+ * contains merges, which we do not yet handle.
-+ */
++ * Collect the list of commits that we'll have to reapply now already.
++ * This ensures that we'll abort early on in case the range of commits
++ * contains merges, which we do not yet handle.
++ */
+ ret = collect_commits(repo, parent, head, &commits);
+ if (ret < 0)
+ goto out;
+
+ /*
+ * Then we split up the commit and replace the original commit with the
-+ * new new ones.
++ * new ones.
+ */
+ ret = split_commit(repo, original_commit, &pathspec,
+ commit_message, split_commits);
@@ builtin/history.c: static int cmd_history_reword(int argc,
+
+out:
+ oidmap_clear(&rewritten_commits, 0);
++ free_commit_list(from_list);
+ clear_pathspec(&pathspec);
+ strvec_clear(&commits);
-+ free_commit_list(list);
+ return ret;
+}
+
@@ builtin/history.c: static int cmd_history_reword(int argc,
@@ builtin/history.c: int cmd_history(int argc,
const char * const usage[] = {
N_("git history [<options>]"),
- N_("git history reword [<options>] <commit>"),
-+ N_("git history split [<options>] <commit> [--] [<pathspec>...]"),
+ GIT_HISTORY_REWORD_USAGE,
++ GIT_HISTORY_SPLIT_USAGE,
NULL,
};
parse_opt_subcommand_fn *fn = NULL;
@@ t/t3452-history-split.sh (new)
+ git switch - &&
+ git merge theirs &&
+ test_must_fail git history split HEAD 2>err &&
-+ test_grep "commit to be split must not be a merge commit" err &&
++ test_grep "cannot rearrange commit history with merges" err &&
+ test_must_fail git history split HEAD~ 2>err &&
+ test_grep "cannot rearrange commit history with merges" err
+ )
+'
+
++test_expect_success 'refuses to work with unrelated commits' '
++ test_when_finished "rm -rf repo" &&
++ git init repo &&
++ (
++ cd repo &&
++ test_commit base &&
++ git branch branch &&
++ test_commit ours &&
++ git switch branch &&
++ test_commit theirs &&
++ test_must_fail git history split ours 2>err &&
++ test_grep "split commit must be reachable from current HEAD commit" err
++ )
++'
++
+test_expect_success 'can split up tip commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
@@ t/t3452-history-split.sh (new)
+ cat >expect <<-EOF &&
+
+ # Please enter the commit message for the split-out changes. Lines starting
-+ # with ${SQ}#${SQ} will be kept; you may remove them yourself if you want to.
++ # with ${SQ}#${SQ} will be ignored.
+ # Changes to be committed:
+ # new file: bar
+ #
---
base-commit: eef20e55db9c6590670e245245e207271daea61f
change-id: 20250819-b4-pks-history-builtin-83398f9a05f0
^ permalink raw reply [flat|nested] 278+ messages in thread* [PATCH v5 01/12] wt-status: provide function to expose status for trees
2025-10-21 14:15 ` [PATCH v5 " Patrick Steinhardt
@ 2025-10-21 14:15 ` Patrick Steinhardt
2025-10-21 20:38 ` Junio C Hamano
2025-10-21 14:15 ` [PATCH v5 02/12] replay: extract logic to pick commits Patrick Steinhardt
` (11 subsequent siblings)
12 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-21 14:15 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
The "wt-status" subsystem is responsible for printing status information
around the current state of the working tree. This most importantly
includes information around whether the working tree or the index have
any changes.
We're about to introduce a new command where the changes in neither of
them are actually relevant to us. Instead, what we want is to format the
changes between two different trees. While it is a little bit of a
stretch to add this as functionality to _working tree_ status, it
doesn't make any sense to open-code this functionality, either.
Implement a new function `wt_status_collect_changes_trees()` that diffs
two trees and formats the status accordingly. This function is not yet
used, but will be in a subsequent commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
wt-status.c | 24 ++++++++++++++++++++++++
wt-status.h | 3 +++
2 files changed, 27 insertions(+)
diff --git a/wt-status.c b/wt-status.c
index 8ffe6d3988f..b66edbfca6c 100644
--- a/wt-status.c
+++ b/wt-status.c
@@ -612,6 +612,30 @@ static void wt_status_collect_updated_cb(struct diff_queue_struct *q,
}
}
+void wt_status_collect_changes_trees(struct wt_status *s,
+ const struct object_id *old_treeish,
+ const struct object_id *new_treeish)
+{
+ struct diff_options opts = { 0 };
+
+ repo_diff_setup(s->repo, &opts);
+ opts.output_format = DIFF_FORMAT_CALLBACK;
+ opts.format_callback = wt_status_collect_updated_cb;
+ opts.format_callback_data = s;
+ opts.detect_rename = s->detect_rename >= 0 ? s->detect_rename : opts.detect_rename;
+ opts.rename_limit = s->rename_limit >= 0 ? s->rename_limit : opts.rename_limit;
+ opts.rename_score = s->rename_score >= 0 ? s->rename_score : opts.rename_score;
+ opts.flags.recursive = 1;
+ diff_setup_done(&opts);
+
+ diff_tree_oid(old_treeish, new_treeish, "", &opts);
+ diffcore_std(&opts);
+ diff_flush(&opts);
+ wt_status_get_state(s->repo, &s->state, 0);
+
+ diff_free(&opts);
+}
+
static void wt_status_collect_changes_worktree(struct wt_status *s)
{
struct rev_info rev;
diff --git a/wt-status.h b/wt-status.h
index e40a27214a7..924d7a5fa99 100644
--- a/wt-status.h
+++ b/wt-status.h
@@ -153,6 +153,9 @@ void wt_status_add_cut_line(struct wt_status *s);
void wt_status_prepare(struct repository *r, struct wt_status *s);
void wt_status_print(struct wt_status *s);
void wt_status_collect(struct wt_status *s);
+void wt_status_collect_changes_trees(struct wt_status *s,
+ const struct object_id *old_treeish,
+ const struct object_id *new_treeish);
/*
* Frees the buffers allocated by wt_status_collect.
*/
--
2.51.1.851.g4ebd6896fd.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH v5 01/12] wt-status: provide function to expose status for trees
2025-10-21 14:15 ` [PATCH v5 01/12] wt-status: provide function to expose status for trees Patrick Steinhardt
@ 2025-10-21 20:38 ` Junio C Hamano
0 siblings, 0 replies; 278+ messages in thread
From: Junio C Hamano @ 2025-10-21 20:38 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, D. Ben Knoble, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk, Elijah Newren,
Karthik Nayak
Patrick Steinhardt <ps@pks.im> writes:
> diff --git a/wt-status.c b/wt-status.c
> index 8ffe6d3988f..b66edbfca6c 100644
> --- a/wt-status.c
> +++ b/wt-status.c
> @@ -612,6 +612,30 @@ static void wt_status_collect_updated_cb(struct diff_queue_struct *q,
> }
> }
>
> +void wt_status_collect_changes_trees(struct wt_status *s,
> + const struct object_id *old_treeish,
> + const struct object_id *new_treeish)
> +{
> + struct diff_options opts = { 0 };
> +
> + repo_diff_setup(s->repo, &opts);
> + opts.output_format = DIFF_FORMAT_CALLBACK;
> + opts.format_callback = wt_status_collect_updated_cb;
> + opts.format_callback_data = s;
> + opts.detect_rename = s->detect_rename >= 0 ? s->detect_rename : opts.detect_rename;
> + opts.rename_limit = s->rename_limit >= 0 ? s->rename_limit : opts.rename_limit;
> + opts.rename_score = s->rename_score >= 0 ? s->rename_score : opts.rename_score;
> + opts.flags.recursive = 1;
> + diff_setup_done(&opts);
This is obviously modelled after collect_changes_index(), whose
callback this function reuses, except that the set up to prepare for
running a diff is different from their way to compare between HEAD
(or void, if root) with the index. We also do not have to worry
about sparse checkout. We do not have to worry about
ignore-submodule argument, either.
> + diff_tree_oid(old_treeish, new_treeish, "", &opts);
> + diffcore_std(&opts);
> + diff_flush(&opts);
> + wt_status_get_state(s->repo, &s->state, 0);
> +
> + diff_free(&opts);
> +}
> +
> static void wt_status_collect_changes_worktree(struct wt_status *s)
> {
> struct rev_info rev;
> diff --git a/wt-status.h b/wt-status.h
> index e40a27214a7..924d7a5fa99 100644
> --- a/wt-status.h
> +++ b/wt-status.h
> @@ -153,6 +153,9 @@ void wt_status_add_cut_line(struct wt_status *s);
> ...
Let's add a comment to help callers of this function that the
changes going from "old" to "new" are recorded as if they are
"staged" changes and recorded on the "index" side. It can only be
inferred from the use of collect_updated_cb, which was written to be
used by wt_status_collect_changes_index(), and probably it is a bit
brutal to expect for casual readers to realize on their own.
> +void wt_status_collect_changes_trees(struct wt_status *s,
> + const struct object_id *old_treeish,
> + const struct object_id *new_treeish);
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH v5 02/12] replay: extract logic to pick commits
2025-10-21 14:15 ` [PATCH v5 " Patrick Steinhardt
2025-10-21 14:15 ` [PATCH v5 01/12] wt-status: provide function to expose status for trees Patrick Steinhardt
@ 2025-10-21 14:15 ` Patrick Steinhardt
2025-10-21 20:41 ` Junio C Hamano
2025-10-21 14:15 ` [PATCH v5 03/12] replay: stop using `the_repository` Patrick Steinhardt
` (10 subsequent siblings)
12 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-21 14:15 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
We're about to add a new git-history(1) command that will reuse some of
the same infrastructure as git-replay(1). To prepare for this, extract
the logic to pick a commit into a new "replay.c" file so that it can be
shared between both commands.
Rename the function to have a "replay_" prefix to clearly indicate its
subsystem.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Makefile | 1 +
builtin/replay.c | 110 ++--------------------------------------------------
meson.build | 1 +
replay.c | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
replay.h | 18 +++++++++
5 files changed, 138 insertions(+), 107 deletions(-)
diff --git a/Makefile b/Makefile
index 1919d35bf3f..01c171b4f03 100644
--- a/Makefile
+++ b/Makefile
@@ -1261,6 +1261,7 @@ LIB_OBJS += reftable/tree.o
LIB_OBJS += reftable/writer.o
LIB_OBJS += remote.o
LIB_OBJS += replace-object.o
+LIB_OBJS += replay.o
LIB_OBJS += repo-settings.o
LIB_OBJS += repository.o
LIB_OBJS += rerere.o
diff --git a/builtin/replay.c b/builtin/replay.c
index d0f04927908..aeb7cc88fe5 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -2,7 +2,6 @@
* "git replay" builtin command
*/
-#define USE_THE_REPOSITORY_VARIABLE
#define DISABLE_SIGN_COMPARE_WARNINGS
#include "git-compat-util.h"
@@ -16,18 +15,12 @@
#include "object-name.h"
#include "parse-options.h"
#include "refs.h"
+#include "replay.h"
#include "revision.h"
#include "strmap.h"
#include <oidset.h>
#include <tree.h>
-static const char *short_commit_name(struct repository *repo,
- struct commit *commit)
-{
- return repo_find_unique_abbrev(repo, &commit->object.oid,
- DEFAULT_ABBREV);
-}
-
static struct commit *peel_committish(struct repository *repo, const char *name)
{
struct object *obj;
@@ -40,59 +33,6 @@ static struct commit *peel_committish(struct repository *repo, const char *name)
OBJ_COMMIT);
}
-static char *get_author(const char *message)
-{
- size_t len;
- const char *a;
-
- a = find_commit_header(message, "author", &len);
- if (a)
- return xmemdupz(a, len);
-
- return NULL;
-}
-
-static struct commit *create_commit(struct repository *repo,
- struct tree *tree,
- struct commit *based_on,
- struct commit *parent)
-{
- struct object_id ret;
- struct object *obj = NULL;
- struct commit_list *parents = NULL;
- char *author;
- char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
- struct commit_extra_header *extra = NULL;
- struct strbuf msg = STRBUF_INIT;
- const char *out_enc = get_commit_output_encoding();
- const char *message = repo_logmsg_reencode(repo, based_on,
- NULL, out_enc);
- const char *orig_message = NULL;
- const char *exclude_gpgsig[] = { "gpgsig", NULL };
-
- commit_list_insert(parent, &parents);
- extra = read_commit_extra_headers(based_on, exclude_gpgsig);
- find_commit_subject(message, &orig_message);
- strbuf_addstr(&msg, orig_message);
- author = get_author(message);
- reset_ident_date();
- if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,
- &ret, author, NULL, sign_commit, extra)) {
- error(_("failed to write commit object"));
- goto out;
- }
-
- obj = parse_object(repo, &ret);
-
-out:
- repo_unuse_commit_buffer(the_repository, based_on, message);
- free_commit_extra_headers(extra);
- free_commit_list(parents);
- strbuf_release(&msg);
- free(author);
- return (struct commit *)obj;
-}
-
struct ref_info {
struct commit *onto;
struct strset positive_refs;
@@ -241,50 +181,6 @@ static void determine_replay_mode(struct repository *repo,
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 repository *repo,
- struct commit *pickme,
- kh_oid_map_t *replayed_commits,
- struct commit *onto,
- struct merge_options *merge_opt,
- struct merge_result *result)
-{
- 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(repo, replayed_base);
- pickme_tree = repo_get_commit_tree(repo, pickme);
- base_tree = repo_get_commit_tree(repo, base);
-
- merge_opt->branch1 = short_commit_name(repo, replayed_base);
- merge_opt->branch2 = short_commit_name(repo, 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(repo, result->tree, pickme, replayed_base);
-}
-
static int handle_ref_update(const char *mode,
struct ref_transaction *transaction,
const char *refname,
@@ -472,8 +368,8 @@ int cmd_replay(int argc,
if (commit->parents->next)
die(_("replaying merge commits is not supported yet!"));
- last_commit = pick_regular_commit(repo, commit, replayed_commits,
- onto, &merge_opt, &result);
+ last_commit = replay_pick_regular_commit(repo, commit, replayed_commits,
+ onto, &merge_opt, &result);
if (!last_commit)
break;
diff --git a/meson.build b/meson.build
index cee94244759..ae8d4fef059 100644
--- a/meson.build
+++ b/meson.build
@@ -464,6 +464,7 @@ libgit_sources = [
'reftable/writer.c',
'remote.c',
'replace-object.c',
+ 'replay.c',
'repo-settings.c',
'repository.c',
'rerere.c',
diff --git a/replay.c b/replay.c
new file mode 100644
index 00000000000..e22ce399406
--- /dev/null
+++ b/replay.c
@@ -0,0 +1,115 @@
+#define USE_THE_REPOSITORY_VARIABLE
+
+#include "git-compat-util.h"
+#include "commit.h"
+#include "environment.h"
+#include "gettext.h"
+#include "ident.h"
+#include "object.h"
+#include "object-name.h"
+#include "replay.h"
+#include "tree.h"
+
+static const char *short_commit_name(struct repository *repo,
+ struct commit *commit)
+{
+ return repo_find_unique_abbrev(repo, &commit->object.oid,
+ DEFAULT_ABBREV);
+}
+
+static char *get_author(const char *message)
+{
+ size_t len;
+ const char *a;
+
+ a = find_commit_header(message, "author", &len);
+ if (a)
+ return xmemdupz(a, len);
+
+ return NULL;
+}
+
+static struct commit *create_commit(struct repository *repo,
+ struct tree *tree,
+ struct commit *based_on,
+ struct commit *parent)
+{
+ struct object_id ret;
+ struct object *obj = NULL;
+ struct commit_list *parents = NULL;
+ char *author;
+ char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
+ struct commit_extra_header *extra = NULL;
+ struct strbuf msg = STRBUF_INIT;
+ const char *out_enc = get_commit_output_encoding();
+ const char *message = repo_logmsg_reencode(repo, based_on,
+ NULL, out_enc);
+ const char *orig_message = NULL;
+ const char *exclude_gpgsig[] = { "gpgsig", NULL };
+
+ commit_list_insert(parent, &parents);
+ extra = read_commit_extra_headers(based_on, exclude_gpgsig);
+ find_commit_subject(message, &orig_message);
+ strbuf_addstr(&msg, orig_message);
+ author = get_author(message);
+ reset_ident_date();
+ if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,
+ &ret, author, NULL, sign_commit, extra)) {
+ error(_("failed to write commit object"));
+ goto out;
+ }
+
+ obj = parse_object(repo, &ret);
+
+out:
+ repo_unuse_commit_buffer(the_repository, based_on, message);
+ free_commit_extra_headers(extra);
+ free_commit_list(parents);
+ strbuf_release(&msg);
+ free(author);
+ return (struct commit *)obj;
+}
+
+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);
+}
+
+struct commit *replay_pick_regular_commit(struct repository *repo,
+ struct commit *pickme,
+ kh_oid_map_t *replayed_commits,
+ struct commit *onto,
+ struct merge_options *merge_opt,
+ struct merge_result *result)
+{
+ 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(repo, replayed_base);
+ pickme_tree = repo_get_commit_tree(repo, pickme);
+ base_tree = repo_get_commit_tree(repo, base);
+
+ merge_opt->branch1 = short_commit_name(repo, replayed_base);
+ merge_opt->branch2 = short_commit_name(repo, 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(repo, result->tree, pickme, replayed_base);
+}
diff --git a/replay.h b/replay.h
new file mode 100644
index 00000000000..a461b5c2341
--- /dev/null
+++ b/replay.h
@@ -0,0 +1,18 @@
+#ifndef REPLAY_H
+#define REPLAY_H
+
+#include "khash.h"
+#include "merge-ort.h"
+#include "repository.h"
+
+struct commit;
+struct tree;
+
+struct commit *replay_pick_regular_commit(struct repository *repo,
+ struct commit *pickme,
+ kh_oid_map_t *replayed_commits,
+ struct commit *onto,
+ struct merge_options *merge_opt,
+ struct merge_result *result);
+
+#endif
--
2.51.1.851.g4ebd6896fd.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH v5 02/12] replay: extract logic to pick commits
2025-10-21 14:15 ` [PATCH v5 02/12] replay: extract logic to pick commits Patrick Steinhardt
@ 2025-10-21 20:41 ` Junio C Hamano
0 siblings, 0 replies; 278+ messages in thread
From: Junio C Hamano @ 2025-10-21 20:41 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, D. Ben Knoble, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk, Elijah Newren,
Karthik Nayak
Patrick Steinhardt <ps@pks.im> writes:
> We're about to add a new git-history(1) command that will reuse some of
> the same infrastructure as git-replay(1). To prepare for this, extract
> the logic to pick a commit into a new "replay.c" file so that it can be
> shared between both commands.
>
> Rename the function to have a "replay_" prefix to clearly indicate its
> subsystem.
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> Makefile | 1 +
> builtin/replay.c | 110 ++--------------------------------------------------
> meson.build | 1 +
> replay.c | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
> replay.h | 18 +++++++++
> 5 files changed, 138 insertions(+), 107 deletions(-)
A clean refactoring that --color-moved helps vastly in reading. I
wish all our patches are this easy ;-)
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH v5 03/12] replay: stop using `the_repository`
2025-10-21 14:15 ` [PATCH v5 " Patrick Steinhardt
2025-10-21 14:15 ` [PATCH v5 01/12] wt-status: provide function to expose status for trees Patrick Steinhardt
2025-10-21 14:15 ` [PATCH v5 02/12] replay: extract logic to pick commits Patrick Steinhardt
@ 2025-10-21 14:15 ` Patrick Steinhardt
2025-10-21 20:48 ` Junio C Hamano
2025-10-21 20:52 ` Junio C Hamano
2025-10-21 14:15 ` [PATCH v5 04/12] replay: parse commits before dereferencing them Patrick Steinhardt
` (9 subsequent siblings)
12 siblings, 2 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-21 14:15 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
In `create_commit()` we're using `the_repository` even though we already
have a repository passed to use as an argument. Fix this.
Note that we still cannot get rid of `USE_THE_REPOSITORY_VARIABLE`. This
is because we use `DEFAULT_ABBREV and `get_commit_output_encoding()`,
both of which are stored as global variables that can be modified via
the Git configuration.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
replay.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/replay.c b/replay.c
index e22ce399406..13d75d80543 100644
--- a/replay.c
+++ b/replay.c
@@ -62,7 +62,7 @@ static struct commit *create_commit(struct repository *repo,
obj = parse_object(repo, &ret);
out:
- repo_unuse_commit_buffer(the_repository, based_on, message);
+ repo_unuse_commit_buffer(repo, based_on, message);
free_commit_extra_headers(extra);
free_commit_list(parents);
strbuf_release(&msg);
--
2.51.1.851.g4ebd6896fd.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH v5 03/12] replay: stop using `the_repository`
2025-10-21 14:15 ` [PATCH v5 03/12] replay: stop using `the_repository` Patrick Steinhardt
@ 2025-10-21 20:48 ` Junio C Hamano
2025-10-21 20:52 ` Junio C Hamano
1 sibling, 0 replies; 278+ messages in thread
From: Junio C Hamano @ 2025-10-21 20:48 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, D. Ben Knoble, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk, Elijah Newren,
Karthik Nayak
Patrick Steinhardt <ps@pks.im> writes:
> In `create_commit()` we're using `the_repository` even though we already
> have a repository passed to use as an argument. Fix this.
>
> Note that we still cannot get rid of `USE_THE_REPOSITORY_VARIABLE`. This
> is because we use `DEFAULT_ABBREV and `get_commit_output_encoding()`,
> both of which are stored as global variables that can be modified via
> the Git configuration.
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> replay.c | 2 +-
> 1 file changed, 1 insertion(+), 1 deletion(-)
Obviously correct.
>
> diff --git a/replay.c b/replay.c
> index e22ce399406..13d75d80543 100644
> --- a/replay.c
> +++ b/replay.c
> @@ -62,7 +62,7 @@ static struct commit *create_commit(struct repository *repo,
> obj = parse_object(repo, &ret);
>
> out:
> - repo_unuse_commit_buffer(the_repository, based_on, message);
> + repo_unuse_commit_buffer(repo, based_on, message);
> free_commit_extra_headers(extra);
> free_commit_list(parents);
> strbuf_release(&msg);
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v5 03/12] replay: stop using `the_repository`
2025-10-21 14:15 ` [PATCH v5 03/12] replay: stop using `the_repository` Patrick Steinhardt
2025-10-21 20:48 ` Junio C Hamano
@ 2025-10-21 20:52 ` Junio C Hamano
1 sibling, 0 replies; 278+ messages in thread
From: Junio C Hamano @ 2025-10-21 20:52 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, D. Ben Knoble, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk, Elijah Newren,
Karthik Nayak
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH v5 04/12] replay: parse commits before dereferencing them
2025-10-21 14:15 ` [PATCH v5 " Patrick Steinhardt
` (2 preceding siblings ...)
2025-10-21 14:15 ` [PATCH v5 03/12] replay: stop using `the_repository` Patrick Steinhardt
@ 2025-10-21 14:15 ` Patrick Steinhardt
2025-10-21 20:57 ` Junio C Hamano
2025-10-21 14:15 ` [PATCH v5 05/12] builtin: add new "history" command Patrick Steinhardt
` (8 subsequent siblings)
12 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-21 14:15 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
When looking up a commit it may not be parsed yet. Callers that wish to
access the fields of `struct commit` have to call `repo_parse_commit()`
first so that it is guaranteed to be populated.
We didn't yet care about doing so, because code paths that lead to
`pick_regular_commit()` in "builtin/replay.c" already implicitly parsed
the commits. But now that the function is exposed to outside callers
it's quite easy to get this wrong.
Make the function easier to use by calling `repo_parse_commit()`.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
replay.c | 3 +++
1 file changed, 3 insertions(+)
diff --git a/replay.c b/replay.c
index 13d75d80543..c3628d2488b 100644
--- a/replay.c
+++ b/replay.c
@@ -90,6 +90,9 @@ struct commit *replay_pick_regular_commit(struct repository *repo,
struct commit *base, *replayed_base;
struct tree *pickme_tree, *base_tree;
+ if (repo_parse_commit(repo, pickme))
+ return NULL;
+
base = pickme->parents->item;
replayed_base = mapped_commit(replayed_commits, base, onto);
--
2.51.1.851.g4ebd6896fd.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH v5 04/12] replay: parse commits before dereferencing them
2025-10-21 14:15 ` [PATCH v5 04/12] replay: parse commits before dereferencing them Patrick Steinhardt
@ 2025-10-21 20:57 ` Junio C Hamano
2025-10-27 9:57 ` Patrick Steinhardt
0 siblings, 1 reply; 278+ messages in thread
From: Junio C Hamano @ 2025-10-21 20:57 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, D. Ben Knoble, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk, Elijah Newren,
Karthik Nayak
Patrick Steinhardt <ps@pks.im> writes:
> When looking up a commit it may not be parsed yet. Callers that wish to
> access the fields of `struct commit` have to call `repo_parse_commit()`
> first so that it is guaranteed to be populated.
>
> We didn't yet care about doing so, because code paths that lead to
> `pick_regular_commit()` in "builtin/replay.c" already implicitly parsed
> the commits. But now that the function is exposed to outside callers
> it's quite easy to get this wrong.
>
> Make the function easier to use by calling `repo_parse_commit()`.
Two-and-half obvious questions.
* With this change, can we lose the parse-commit call(s) from
existing callers, or do the need to look at the in-core commit
object themselves before calling this function so they need to
have their parse-commit call(s) anyway?
* Can new callers you plan to add decide without having an already
parsed "pickme" commit object if they want to call this function,
iow, can they decide to call or not to call this function without
looking at the members of the commit structure?
* Are existing callers prepared to see NULL returned from this
function to signal an error?
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> replay.c | 3 +++
> 1 file changed, 3 insertions(+)
>
> diff --git a/replay.c b/replay.c
> index 13d75d80543..c3628d2488b 100644
> --- a/replay.c
> +++ b/replay.c
> @@ -90,6 +90,9 @@ struct commit *replay_pick_regular_commit(struct repository *repo,
> struct commit *base, *replayed_base;
> struct tree *pickme_tree, *base_tree;
>
> + if (repo_parse_commit(repo, pickme))
> + return NULL;
> +
> base = pickme->parents->item;
> replayed_base = mapped_commit(replayed_commits, base, onto);
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v5 04/12] replay: parse commits before dereferencing them
2025-10-21 20:57 ` Junio C Hamano
@ 2025-10-27 9:57 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-27 9:57 UTC (permalink / raw)
To: Junio C Hamano
Cc: git, D. Ben Knoble, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk, Elijah Newren,
Karthik Nayak
On Tue, Oct 21, 2025 at 01:57:54PM -0700, Junio C Hamano wrote:
> Patrick Steinhardt <ps@pks.im> writes:
>
> > When looking up a commit it may not be parsed yet. Callers that wish to
> > access the fields of `struct commit` have to call `repo_parse_commit()`
> > first so that it is guaranteed to be populated.
> >
> > We didn't yet care about doing so, because code paths that lead to
> > `pick_regular_commit()` in "builtin/replay.c" already implicitly parsed
> > the commits. But now that the function is exposed to outside callers
> > it's quite easy to get this wrong.
> >
> > Make the function easier to use by calling `repo_parse_commit()`.
>
> Two-and-half obvious questions.
>
> * With this change, can we lose the parse-commit call(s) from
> existing callers, or do the need to look at the in-core commit
> object themselves before calling this function so they need to
> have their parse-commit call(s) anyway?
There's only a single caller in "builtin/replay.c", and that caller
parses commits deep inside the callstack via `prepare_revision_walk()`.
So we cannot easily get rid of any calls.
> * Can new callers you plan to add decide without having an already
> parsed "pickme" commit object if they want to call this function,
> iow, can they decide to call or not to call this function without
> looking at the members of the commit structure?
Not quite sure I understand this question. In any case, I was hitting
segfaults in tests when I didn't have this call. But your questions made
me double-check this now, and I cannot see any of these failures
anymore. And we do use the same infra to pick commits as
"builtin/replay.c" does, so things should work alright.
Let me drop this commit for now. Things work without it, and in theory
they should. And now that I'm revamping the infra to not use the merge
machinery in the first place we don't even hit this code path anymore.
Thanks!
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH v5 05/12] builtin: add new "history" command
2025-10-21 14:15 ` [PATCH v5 " Patrick Steinhardt
` (3 preceding siblings ...)
2025-10-21 14:15 ` [PATCH v5 04/12] replay: parse commits before dereferencing them Patrick Steinhardt
@ 2025-10-21 14:15 ` Patrick Steinhardt
2025-10-21 21:15 ` Junio C Hamano
2025-10-21 14:15 ` [PATCH v5 06/12] builtin/history: implement "reword" subcommand Patrick Steinhardt
` (7 subsequent siblings)
12 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-21 14:15 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
When rewriting history via git-rebase(1) there are a couple of very
common use cases:
- The ordering of two commits should be reversed.
- A commit should be split up into two commits.
- A commit should be dropped from the history completely.
- Multiple commits should be squashed into one.
While these operations are all doable, it often feels needlessly kludgey
to do so by doing an interactive rebase, using the editor to say what
one wants, and then perform the actions. Furthermore, some operations
like splitting up a commit into two are way more involved than that and
require a whole series of commands.
Add a new "history" command to plug this gap. This command will have
several different subcommands to imperatively rewrite history for common
use cases like the above. These subcommands will be implemented in
subsequent commits.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
.gitignore | 1 +
Documentation/git-history.adoc | 45 ++++++++++++++++++++++++++++++++++++++++++
Documentation/meson.build | 1 +
Makefile | 1 +
builtin.h | 1 +
builtin/history.c | 22 +++++++++++++++++++++
command-list.txt | 1 +
git.c | 1 +
meson.build | 1 +
t/meson.build | 1 +
t/t3450-history.sh | 17 ++++++++++++++++
11 files changed, 92 insertions(+)
diff --git a/.gitignore b/.gitignore
index 78a45cb5bec..24635cf2d6f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -79,6 +79,7 @@
/git-grep
/git-hash-object
/git-help
+/git-history
/git-hook
/git-http-backend
/git-http-fetch
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
new file mode 100644
index 00000000000..57560525a70
--- /dev/null
+++ b/Documentation/git-history.adoc
@@ -0,0 +1,45 @@
+git-history(1)
+==============
+
+NAME
+----
+git-history - EXPERIMENTAL: Rewrite history of the current branch
+
+SYNOPSIS
+--------
+[synopsis]
+git history [<options>]
+
+DESCRIPTION
+-----------
+
+Rewrite history by rearranging or modifying specific commits in the
+history.
+
+This command is similar to linkgit:git-rebase[1] and uses the same
+underlying machinery. You should use rebases if you want to reapply a range of
+commits onto a different base, or interactive rebases if you want to edit a
+range of commits.
+
+Note that this command does not (yet) work with histories that contain
+merges. You should use linkgit:git-rebase[1] with the `--rebase-merges`
+flag instead.
+
+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
+
+COMMANDS
+--------
+
+This command requires a subcommand. Several subcommands are available to
+rewrite history in different ways:
+
+CONFIGURATION
+-------------
+
+include::includes/cmd-config-section-all.adoc[]
+
+include::config/sequencer.adoc[]
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Documentation/meson.build b/Documentation/meson.build
index 44f94cdb7ba..cf551a28ae7 100644
--- a/Documentation/meson.build
+++ b/Documentation/meson.build
@@ -64,6 +64,7 @@ manpages = {
'git-gui.adoc' : 1,
'git-hash-object.adoc' : 1,
'git-help.adoc' : 1,
+ 'git-history.adoc' : 1,
'git-hook.adoc' : 1,
'git-http-backend.adoc' : 1,
'git-http-fetch.adoc' : 1,
diff --git a/Makefile b/Makefile
index 01c171b4f03..1380ee1e196 100644
--- a/Makefile
+++ b/Makefile
@@ -1395,6 +1395,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o
BUILTIN_OBJS += builtin/grep.o
BUILTIN_OBJS += builtin/hash-object.o
BUILTIN_OBJS += builtin/help.o
+BUILTIN_OBJS += builtin/history.o
BUILTIN_OBJS += builtin/hook.o
BUILTIN_OBJS += builtin/index-pack.o
BUILTIN_OBJS += builtin/init-db.o
diff --git a/builtin.h b/builtin.h
index 1b35565fbd9..93c91d07d4b 100644
--- a/builtin.h
+++ b/builtin.h
@@ -172,6 +172,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix, struc
int cmd_grep(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_hash_object(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_help(int argc, const char **argv, const char *prefix, struct repository *repo);
+int cmd_history(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_hook(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_index_pack(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_init_db(int argc, const char **argv, const char *prefix, struct repository *repo);
diff --git a/builtin/history.c b/builtin/history.c
new file mode 100644
index 00000000000..f6fe32610b0
--- /dev/null
+++ b/builtin/history.c
@@ -0,0 +1,22 @@
+#include "builtin.h"
+#include "gettext.h"
+#include "parse-options.h"
+
+int cmd_history(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo UNUSED)
+{
+ const char * const usage[] = {
+ N_("git history [<options>]"),
+ NULL,
+ };
+ struct option options[] = {
+ OPT_END(),
+ };
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc)
+ usagef("unrecognized argument: %s", argv[0]);
+ return 0;
+}
diff --git a/command-list.txt b/command-list.txt
index accd3d0c4b5..f9005cf4597 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -115,6 +115,7 @@ git-grep mainporcelain info
git-gui mainporcelain
git-hash-object plumbingmanipulators
git-help ancillaryinterrogators complete
+git-history mainporcelain history
git-hook purehelpers
git-http-backend synchingrepositories
git-http-fetch synchelpers
diff --git a/git.c b/git.c
index c5fad56813f..744cb6527e0 100644
--- a/git.c
+++ b/git.c
@@ -586,6 +586,7 @@ static struct cmd_struct commands[] = {
{ "grep", cmd_grep, RUN_SETUP_GENTLY },
{ "hash-object", cmd_hash_object },
{ "help", cmd_help },
+ { "history", cmd_history, RUN_SETUP },
{ "hook", cmd_hook, RUN_SETUP },
{ "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },
{ "init", cmd_init_db },
diff --git a/meson.build b/meson.build
index ae8d4fef059..2d789612a01 100644
--- a/meson.build
+++ b/meson.build
@@ -604,6 +604,7 @@ builtin_sources = [
'builtin/grep.c',
'builtin/hash-object.c',
'builtin/help.c',
+ 'builtin/history.c',
'builtin/hook.c',
'builtin/index-pack.c',
'builtin/init-db.c',
diff --git a/t/meson.build b/t/meson.build
index 401b24e50e0..019435918fa 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -384,6 +384,7 @@ integration_tests = [
't3436-rebase-more-options.sh',
't3437-rebase-fixup-options.sh',
't3438-rebase-broken-files.sh',
+ 't3450-history.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
diff --git a/t/t3450-history.sh b/t/t3450-history.sh
new file mode 100755
index 00000000000..417c343d43b
--- /dev/null
+++ b/t/t3450-history.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+test_description='tests for git-history command'
+
+. ./test-lib.sh
+
+test_expect_success 'does nothing without any arguments' '
+ git history >out 2>&1 &&
+ test_must_be_empty out
+'
+
+test_expect_success 'raises an error with unknown argument' '
+ test_must_fail git history garbage 2>err &&
+ test_grep "unrecognized argument: garbage" err
+'
+
+test_done
--
2.51.1.851.g4ebd6896fd.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH v5 05/12] builtin: add new "history" command
2025-10-21 14:15 ` [PATCH v5 05/12] builtin: add new "history" command Patrick Steinhardt
@ 2025-10-21 21:15 ` Junio C Hamano
2025-10-27 9:57 ` Patrick Steinhardt
0 siblings, 1 reply; 278+ messages in thread
From: Junio C Hamano @ 2025-10-21 21:15 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, D. Ben Knoble, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk, Elijah Newren,
Karthik Nayak
Patrick Steinhardt <ps@pks.im> writes:
> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
> new file mode 100644
> index 00000000000..57560525a70
> --- /dev/null
> +++ b/Documentation/git-history.adoc
> @@ -0,0 +1,45 @@
> +git-history(1)
> +==============
> +
> +NAME
> +----
> +git-history - EXPERIMENTAL: Rewrite history of the current branch
>
>
> +SYNOPSIS
> +--------
> +[synopsis]
> +git history [<options>]
> +
> +DESCRIPTION
> +-----------
We would want to make sure that all experimental things identify
themselves in a similar way.
The way how replay identifies itself as experimental, which this
patch is modeled after, is somewhat different from what is done by
backfill, for-each-repo, last-modified, and sparse-checkout
commands.
> +Rewrite history by rearranging or modifying specific commits in the
> +history.
> +
> +This command is similar to linkgit:git-rebase[1] and uses the same
> +underlying machinery. You should use rebases if you want to reapply a range of
> +commits onto a different base, or interactive rebases if you want to edit a
> +range of commits.
> +
> +Note that this command does not (yet) work with histories that contain
> +merges. You should use linkgit:git-rebase[1] with the `--rebase-merges`
> +flag instead.
> +
> +THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
> +
> +COMMANDS
> +--------
> +
> +This command requires a subcommand. Several subcommands are available to
> +rewrite history in different ways:
Looking at "refs", "repo" and "sparse-checkout", none of them say
"requires a subcommand", even though they do. It would probably be
obvious from the syntax, so drop the first sentence, perhaps?
And "subcommand" -> "command" to match the section title.
The remainder of this step seems a bog standard "here is how you add
an empty shell for a new command" and I didn't see anything fishy in
it.
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v5 05/12] builtin: add new "history" command
2025-10-21 21:15 ` Junio C Hamano
@ 2025-10-27 9:57 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-27 9:57 UTC (permalink / raw)
To: Junio C Hamano
Cc: git, D. Ben Knoble, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk, Elijah Newren,
Karthik Nayak
On Tue, Oct 21, 2025 at 02:15:10PM -0700, Junio C Hamano wrote:
> Patrick Steinhardt <ps@pks.im> writes:
>
> > diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
> > new file mode 100644
> > index 00000000000..57560525a70
> > --- /dev/null
> > +++ b/Documentation/git-history.adoc
> > @@ -0,0 +1,45 @@
> > +git-history(1)
> > +==============
> > +
> > +NAME
> > +----
> > +git-history - EXPERIMENTAL: Rewrite history of the current branch
> >
> >
> > +SYNOPSIS
> > +--------
> > +[synopsis]
> > +git history [<options>]
> > +
> > +DESCRIPTION
> > +-----------
>
> We would want to make sure that all experimental things identify
> themselves in a similar way.
>
> The way how replay identifies itself as experimental, which this
> patch is modeled after, is somewhat different from what is done by
> backfill, for-each-repo, last-modified, and sparse-checkout
> commands.
I guess the only thing that's different with git-replay(1) is that we
also have the `(EXPERIMENTAL!)` tag in the synopsis. No other man page
does that as far as I can see.
But yeah, I agree that things should be consistent here. I think the
most sensible thing to do is to:
- Have the "EXPERIMENTAL:" tag in the NAME section.
- Have "THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE." in the
DESCRIPTION section, ideally after the first summarizing sentence.
> > +Rewrite history by rearranging or modifying specific commits in the
> > +history.
> > +
> > +This command is similar to linkgit:git-rebase[1] and uses the same
> > +underlying machinery. You should use rebases if you want to reapply a range of
> > +commits onto a different base, or interactive rebases if you want to edit a
> > +range of commits.
> > +
> > +Note that this command does not (yet) work with histories that contain
> > +merges. You should use linkgit:git-rebase[1] with the `--rebase-merges`
> > +flag instead.
> > +
> > +THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
> > +
> > +COMMANDS
> > +--------
> > +
> > +This command requires a subcommand. Several subcommands are available to
> > +rewrite history in different ways:
>
> Looking at "refs", "repo" and "sparse-checkout", none of them say
> "requires a subcommand", even though they do. It would probably be
> obvious from the syntax, so drop the first sentence, perhaps?
>
> And "subcommand" -> "command" to match the section title.
Makes sense.
Thanks!
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH v5 06/12] builtin/history: implement "reword" subcommand
2025-10-21 14:15 ` [PATCH v5 " Patrick Steinhardt
` (4 preceding siblings ...)
2025-10-21 14:15 ` [PATCH v5 05/12] builtin: add new "history" command Patrick Steinhardt
@ 2025-10-21 14:15 ` Patrick Steinhardt
2025-10-21 21:34 ` Junio C Hamano
2025-10-21 14:15 ` [PATCH v5 07/12] add-patch: split out header from "add-interactive.h" Patrick Steinhardt
` (6 subsequent siblings)
12 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-21 14:15 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
Implement a new "reword" subcommand for git-history(1). This subcommand
is essentially the same as if a user performed an interactive rebase
with a single commit changed to use the "reword" verb.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-history.adoc | 7 +
builtin/history.c | 364 ++++++++++++++++++++++++++++++++++++++++-
t/meson.build | 1 +
t/t3450-history.sh | 6 +-
t/t3451-history-reword.sh | 217 ++++++++++++++++++++++++
5 files changed, 588 insertions(+), 7 deletions(-)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index 57560525a70..6f016801936 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -9,6 +9,7 @@ SYNOPSIS
--------
[synopsis]
git history [<options>]
+git history reword [<options>] <commit>
DESCRIPTION
-----------
@@ -33,6 +34,12 @@ COMMANDS
This command requires a subcommand. Several subcommands are available to
rewrite history in different ways:
+`reword <commit> [--message=<message>]`::
+ Rewrite the commit message of the specified commit. All the other
+ details of this commit remain unchanged. If no commit message is
+ provided, then this command will spawn an editor with the current
+ message of that commit.
+
CONFIGURATION
-------------
diff --git a/builtin/history.c b/builtin/history.c
index f6fe32610b0..f14e88c9bf4 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -1,22 +1,378 @@
+#define USE_THE_REPOSITORY_VARIABLE
+
#include "builtin.h"
+#include "commit-reach.h"
+#include "commit.h"
+#include "config.h"
+#include "editor.h"
+#include "environment.h"
#include "gettext.h"
+#include "hex.h"
#include "parse-options.h"
+#include "refs.h"
+#include "replay.h"
+#include "reset.h"
+#include "revision.h"
+#include "sequencer.h"
+#include "strvec.h"
+#include "tree.h"
+#include "wt-status.h"
+
+#define GIT_HISTORY_REWORD_USAGE N_("git history reword [<options>] <commit>")
+
+static int collect_commits(struct repository *repo,
+ struct commit *old_commit,
+ struct commit *new_commit,
+ struct strvec *out)
+{
+ struct setup_revision_opt revision_opts = {
+ .assume_dashdash = 1,
+ };
+ struct strvec revisions = STRVEC_INIT;
+ struct commit *child;
+ struct rev_info rev = { 0 };
+ int ret;
+
+ repo_init_revisions(repo, &rev, NULL);
+ strvec_push(&revisions, "");
+ strvec_push(&revisions, oid_to_hex(&new_commit->object.oid));
+ if (old_commit)
+ strvec_pushf(&revisions, "^%s", oid_to_hex(&old_commit->object.oid));
+
+ setup_revisions_from_strvec(&revisions, &rev, &revision_opts);
+ if (revisions.nr != 1 || prepare_revision_walk(&rev)) {
+ ret = error(_("revision walk setup failed"));
+ goto out;
+ }
+
+ while ((child = get_revision(&rev))) {
+ if (old_commit && !child->parents)
+ BUG("revision walk did not find child commit");
+ if (child->parents && child->parents->next) {
+ ret = error(_("cannot rearrange commit history with merges"));
+ goto out;
+ }
+
+ strvec_push(out, oid_to_hex(&child->object.oid));
+
+ if (child->parents && old_commit &&
+ commit_list_contains(old_commit, child->parents))
+ break;
+ }
+
+ /*
+ * Revisions are in newest-order-first. We have to reverse the
+ * array though so that we pick the oldest commits first.
+ */
+ for (size_t i = 0, j = out->nr - 1; i < j; i++, j--)
+ SWAP(out->v[i], out->v[j]);
+
+ ret = 0;
+
+out:
+ strvec_clear(&revisions);
+ release_revisions(&rev);
+ reset_revision_walk();
+ return ret;
+}
+
+static void replace_commits(struct strvec *commits,
+ const struct object_id *commit_to_replace,
+ const struct object_id *replacements,
+ size_t replacements_nr)
+{
+ char commit_to_replace_oid[GIT_MAX_HEXSZ + 1];
+ struct strvec replacement_oids = STRVEC_INIT;
+ bool found = false;
+
+ oid_to_hex_r(commit_to_replace_oid, commit_to_replace);
+ for (size_t i = 0; i < replacements_nr; i++)
+ strvec_push(&replacement_oids, oid_to_hex(&replacements[i]));
+
+ for (size_t i = 0; i < commits->nr; i++) {
+ if (strcmp(commits->v[i], commit_to_replace_oid))
+ continue;
+ strvec_splice(commits, i, 1, replacement_oids.v, replacement_oids.nr);
+ found = true;
+ break;
+ }
+ if (!found)
+ BUG("could not find commit to replace");
+
+ strvec_clear(&replacement_oids);
+}
+
+static int apply_commits(struct repository *repo,
+ const struct strvec *commits,
+ struct commit *onto,
+ struct commit *orig_head,
+ const char *action)
+{
+ struct reset_head_opts reset_opts = { 0 };
+ struct merge_options merge_opts = { 0 };
+ struct merge_result result = { 0 };
+ struct strbuf buf = STRBUF_INIT;
+ kh_oid_map_t *replayed_commits;
+ int ret;
+
+ replayed_commits = kh_init_oid_map();
+
+ init_basic_merge_options(&merge_opts, repo);
+ merge_opts.show_rename_progress = 0;
+
+ for (size_t i = 0; i < commits->nr; i++) {
+ struct object_id commit_id;
+ struct commit *commit;
+ const char *end;
+ int hash_result;
+ khint_t pos;
+
+ if (parse_oid_hex_algop(commits->v[i], &commit_id, &end,
+ repo->hash_algo)) {
+ ret = error(_("invalid object ID: %s"), commits->v[i]);
+ goto out;
+ }
+
+ commit = lookup_commit(repo, &commit_id);
+ if (!commit || repo_parse_commit(repo, commit)) {
+ ret = error(_("failed to look up commit: %s"), oid_to_hex(&commit_id));
+ goto out;
+ }
+
+ if (!onto) {
+ onto = commit;
+ result.clean = 1;
+ result.tree = repo_get_commit_tree(repo, commit);
+ } else {
+ onto = replay_pick_regular_commit(repo, commit, replayed_commits,
+ onto, &merge_opts, &result);
+ if (!onto)
+ break;
+ }
+
+ pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hash_result);
+ if (hash_result == 0) {
+ ret = error(_("duplicate rewritten commit: %s\n"),
+ oid_to_hex(&commit->object.oid));
+ goto out;
+ }
+ kh_value(replayed_commits, pos) = onto;
+ }
+
+ if (!result.clean) {
+ ret = error(_("could not merge"));
+ goto out;
+ }
+
+ reset_opts.oid = &onto->object.oid;
+ strbuf_addf(&buf, "%s: switch to rewritten %s", action, oid_to_hex(reset_opts.oid));
+ reset_opts.flags = RESET_HEAD_REFS_ONLY | RESET_ORIG_HEAD;
+ reset_opts.orig_head = &orig_head->object.oid;
+ reset_opts.default_reflog_action = action;
+ if (reset_head(repo, &reset_opts) < 0) {
+ ret = error(_("could not switch to %s"), oid_to_hex(reset_opts.oid));
+ goto out;
+ }
+
+ ret = 0;
+
+out:
+ kh_destroy_oid_map(replayed_commits);
+ merge_finalize(&merge_opts, &result);
+ strbuf_release(&buf);
+ return ret;
+}
+
+static void change_data_free(void *util, const char *str UNUSED)
+{
+ struct wt_status_change_data *d = util;
+ free(d->rename_source);
+ free(d);
+}
+
+static int fill_commit_message(struct repository *repo,
+ const struct object_id *old_tree,
+ const struct object_id *new_tree,
+ const char *default_message,
+ const char *provided_message,
+ const char *action,
+ struct strbuf *out)
+{
+ if (!provided_message) {
+ const char *path = git_path_commit_editmsg();
+ const char *hint =
+ _("Please enter the commit message for the %s changes."
+ " Lines starting\nwith '%s' will be ignored.\n");
+ struct wt_status s;
+
+ strbuf_addstr(out, default_message);
+ strbuf_addch(out, '\n');
+ strbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);
+ write_file_buf(path, out->buf, out->len);
+
+ wt_status_prepare(repo, &s);
+ FREE_AND_NULL(s.branch);
+ s.ahead_behind_flags = AHEAD_BEHIND_QUICK;
+ s.commit_template = 1;
+ s.colopts = 0;
+ s.display_comment_prefix = 1;
+ s.hints = 0;
+ s.use_color = 0;
+ s.whence = FROM_COMMIT;
+ s.committable = 1;
+
+ s.fp = fopen(git_path_commit_editmsg(), "a");
+ if (!s.fp)
+ return error_errno(_("could not open '%s'"), git_path_commit_editmsg());
+
+ wt_status_collect_changes_trees(&s, old_tree, new_tree);
+ wt_status_print(&s);
+ wt_status_collect_free_buffers(&s);
+ string_list_clear_func(&s.change, change_data_free);
+
+ strbuf_reset(out);
+ if (launch_editor(path, out, NULL)) {
+ fprintf(stderr, _("Please supply the message using the -m option.\n"));
+ return -1;
+ }
+ strbuf_stripspace(out, comment_line_str);
+ } else {
+ strbuf_addstr(out, provided_message);
+ }
+
+ cleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);
+
+ if (!out->len) {
+ fprintf(stderr, _("Aborting commit due to empty commit message.\n"));
+ return -1;
+ }
+
+ return 0;
+}
+
+static int cmd_history_reword(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo)
+{
+ const char * const usage[] = {
+ GIT_HISTORY_REWORD_USAGE,
+ NULL,
+ };
+ const char *commit_message = NULL;
+ struct option options[] = {
+ OPT_STRING('m', "message", &commit_message, N_("message"), N_("commit message")),
+ OPT_END(),
+ };
+ struct strbuf final_message = STRBUF_INIT;
+ struct commit *original_commit, *parent, *head;
+ struct strvec commits = STRVEC_INIT;
+ struct object_id parent_tree_oid, original_commit_tree_oid;
+ struct object_id rewritten_commit;
+ struct commit_list *from_list = NULL;
+ const char *original_message, *original_body, *ptr;
+ char *original_author = NULL;
+ size_t len;
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc != 1) {
+ ret = error(_("command expects a single revision"));
+ goto out;
+ }
+ repo_config(repo, git_default_config, NULL);
+
+ original_commit = lookup_commit_reference_by_name(argv[0]);
+ if (!original_commit) {
+ ret = error(_("commit to be reworded cannot be found: %s"), argv[0]);
+ goto out;
+ }
+ original_commit_tree_oid = repo_get_commit_tree(repo, original_commit)->object.oid;
+
+ parent = original_commit->parents ? original_commit->parents->item : NULL;
+ if (parent) {
+ if (repo_parse_commit(repo, parent)) {
+ ret = error(_("unable to parse commit %s"),
+ oid_to_hex(&parent->object.oid));
+ goto out;
+ }
+ parent_tree_oid = repo_get_commit_tree(repo, parent)->object.oid;
+ } else {
+ oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
+ }
+
+ head = lookup_commit_reference_by_name("HEAD");
+ if (!head) {
+ ret = error(_("could not resolve HEAD to a commit"));
+ goto out;
+ }
+
+ commit_list_append(original_commit, &from_list);
+ if (!repo_is_descendant_of(repo, head, from_list)) {
+ ret = error (_("split commit must be reachable from current HEAD commit"));
+ goto out;
+ }
+
+ /*
+ * Collect the list of commits that we'll have to reapply now already.
+ * This ensures that we'll abort early on in case the range of commits
+ * contains merges, which we do not yet handle.
+ */
+ ret = collect_commits(repo, parent, head, &commits);
+ if (ret < 0)
+ goto out;
+
+ /* We retain authorship of the original commit. */
+ original_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);
+ ptr = find_commit_header(original_message, "author", &len);
+ if (ptr)
+ original_author = xmemdupz(ptr, len);
+ find_commit_subject(original_message, &original_body);
+
+ ret = fill_commit_message(repo, &parent_tree_oid, &original_commit_tree_oid,
+ original_body, commit_message, "reworded", &final_message);
+ if (ret < 0)
+ goto out;
+
+ ret = commit_tree(final_message.buf, final_message.len, &original_commit_tree_oid,
+ original_commit->parents, &rewritten_commit, original_author, NULL);
+ if (ret < 0) {
+ ret = error(_("failed writing reworded commit"));
+ goto out;
+ }
+
+ replace_commits(&commits, &original_commit->object.oid, &rewritten_commit, 1);
+
+ ret = apply_commits(repo, &commits, parent, head, "reword");
+ if (ret < 0)
+ goto out;
+
+ ret = 0;
+
+out:
+ strbuf_release(&final_message);
+ free_commit_list(from_list);
+ strvec_clear(&commits);
+ free(original_author);
+ return ret;
+}
int cmd_history(int argc,
const char **argv,
const char *prefix,
- struct repository *repo UNUSED)
+ struct repository *repo)
{
const char * const usage[] = {
N_("git history [<options>]"),
+ GIT_HISTORY_REWORD_USAGE,
NULL,
};
+ parse_opt_subcommand_fn *fn = NULL;
struct option options[] = {
+ OPT_SUBCOMMAND("reword", &fn, cmd_history_reword),
OPT_END(),
};
argc = parse_options(argc, argv, prefix, options, usage, 0);
- if (argc)
- usagef("unrecognized argument: %s", argv[0]);
- return 0;
+ return fn(argc, argv, prefix, repo);
}
diff --git a/t/meson.build b/t/meson.build
index 019435918fa..a3ec9199947 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -385,6 +385,7 @@ integration_tests = [
't3437-rebase-fixup-options.sh',
't3438-rebase-broken-files.sh',
't3450-history.sh',
+ 't3451-history-reword.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
diff --git a/t/t3450-history.sh b/t/t3450-history.sh
index 417c343d43b..f513463b92b 100755
--- a/t/t3450-history.sh
+++ b/t/t3450-history.sh
@@ -5,13 +5,13 @@ test_description='tests for git-history command'
. ./test-lib.sh
test_expect_success 'does nothing without any arguments' '
- git history >out 2>&1 &&
- test_must_be_empty out
+ test_must_fail git history 2>err &&
+ test_grep "need a subcommand" err
'
test_expect_success 'raises an error with unknown argument' '
test_must_fail git history garbage 2>err &&
- test_grep "unrecognized argument: garbage" err
+ test_grep "unknown subcommand: .garbage." err
'
test_done
diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
new file mode 100755
index 00000000000..b73371fb51c
--- /dev/null
+++ b/t/t3451-history-reword.sh
@@ -0,0 +1,217 @@
+#!/bin/sh
+
+test_description='tests for git-history reword subcommand'
+
+. ./test-lib.sh
+
+test_expect_success 'refuses to work with merge commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ git branch branch &&
+ test_commit ours &&
+ git switch branch &&
+ test_commit theirs &&
+ git switch - &&
+ git merge theirs &&
+ test_must_fail git history reword HEAD~ 2>err &&
+ test_grep "cannot rearrange commit history with merges" err &&
+ test_must_fail git history reword HEAD 2>err &&
+ test_grep "cannot rearrange commit history with merges" err
+ )
+'
+
+test_expect_success 'refuses to work with unrelated commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ git branch branch &&
+ test_commit ours &&
+ git switch branch &&
+ test_commit theirs &&
+ test_must_fail git history reword ours 2>err &&
+ test_grep "split commit must be reachable from current HEAD commit" err
+ )
+'
+
+test_expect_success 'can reword tip of a branch' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ git symbolic-ref HEAD >expect &&
+ git history reword -m "third reworded" HEAD &&
+ git symbolic-ref HEAD >actual &&
+ test_cmp expect actual &&
+
+ cat >expect <<-EOF &&
+ third reworded
+ second
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'can reword commit in the middle' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ git symbolic-ref HEAD >expect &&
+ git history reword -m "second reworded" HEAD~ &&
+ git symbolic-ref HEAD >actual &&
+ test_cmp expect actual &&
+
+ cat >expect <<-EOF &&
+ third
+ second reworded
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'can reword root commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ git history reword -m "first reworded" HEAD~2 &&
+
+ cat >expect <<-EOF &&
+ third
+ second
+ first reworded
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'can use editor to rewrite commit message' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+
+ write_script fake-editor.sh <<-\EOF &&
+ cp "$1" . &&
+ printf "\namend a comment\n" >>"$1"
+ EOF
+ test_set_editor "$(pwd)"/fake-editor.sh &&
+ git history reword HEAD &&
+
+ cat >expect <<-EOF &&
+ first
+
+ # Please enter the commit message for the reworded changes. Lines starting
+ # with ${SQ}#${SQ} will be ignored.
+ # Changes to be committed:
+ # new file: first.t
+ #
+ EOF
+ test_cmp expect COMMIT_EDITMSG &&
+
+ cat >expect <<-EOF &&
+ first
+
+ amend a comment
+
+ EOF
+ git log --format=%B >actual &&
+ test_cmp expect actual
+ )
+'
+
+# For now, git-history(1) does not yet execute any hooks. This is subject to
+# change in the future, and if it does this test here is expected to start
+# failing. In other words, this test is not an endorsement of the current
+# status quo.
+test_expect_success 'hooks are executed for rewritten commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ write_script .git/hooks/prepare-commit-msg <<-EOF &&
+ touch "$(pwd)/hooks.log
+ EOF
+ write_script .git/hooks/post-commit <<-EOF &&
+ touch "$(pwd)/hooks.log
+ EOF
+ write_script .git/hooks/post-rewrite <<-EOF &&
+ touch "$(pwd)/hooks.log
+ EOF
+
+ git history reword -m "second reworded" HEAD~ &&
+
+ cat >expect <<-EOF &&
+ third
+ second reworded
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual &&
+
+ test_path_is_missing hooks.log
+ )
+'
+
+test_expect_success 'aborts with empty commit message' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+
+ test_must_fail git history reword -m "" HEAD 2>err &&
+ test_grep "Aborting commit due to empty commit message." err
+ )
+'
+
+test_expect_success 'retains changes in the worktree and index' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch a b &&
+ git add . &&
+ git commit -m "initial commit" &&
+ echo foo >a &&
+ echo bar >b &&
+ git add b &&
+ git history reword HEAD -m message &&
+ cat >expect <<-\EOF &&
+ M a
+ M b
+ ?? actual
+ ?? expect
+ EOF
+ git status --porcelain >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_done
--
2.51.1.851.g4ebd6896fd.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH v5 06/12] builtin/history: implement "reword" subcommand
2025-10-21 14:15 ` [PATCH v5 06/12] builtin/history: implement "reword" subcommand Patrick Steinhardt
@ 2025-10-21 21:34 ` Junio C Hamano
2025-10-21 21:43 ` D. Ben Knoble
2025-10-27 9:58 ` Patrick Steinhardt
0 siblings, 2 replies; 278+ messages in thread
From: Junio C Hamano @ 2025-10-21 21:34 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, D. Ben Knoble, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk, Elijah Newren,
Karthik Nayak
Patrick Steinhardt <ps@pks.im> writes:
> Implement a new "reword" subcommand for git-history(1). This subcommand
> is essentially the same as if a user performed an interactive rebase
> with a single commit changed to use the "reword" verb.
Oy. I've always wanted something like this in "rebase -i" myself.
It is a bit sad that I have to learn a new command to do something
obvious and trivial like this, but that's life ;-)
Maybe "git history" becomes powerful enough and can replace my
every-day use of "rebase -i".
> @@ -9,6 +9,7 @@ SYNOPSIS
> --------
> [synopsis]
> git history [<options>]
> +git history reword [<options>] <commit>
It is curious that a command-less form is still listed here,
especially since this command "requires" a subcommand. I would have
expected that there will be a single line here after implementing a
single subcommand.
> +`reword <commit> [--message=<message>]`::
That should be `reword [--message=<message>] <commit>` no?
> + Rewrite the commit message of the specified commit. All the other
> + details of this commit remain unchanged. If no commit message is
> + provided, then this command will spawn an editor with the current
> + message of that commit.
As long as it takes more than one -m and concatenates them just like
"git commit -m <message1> -m <message2>" does, I would not complain
too much that a command line option to give message encourages sloppy
log messages.
> + if (!onto) {
> + onto = commit;
> + result.clean = 1;
> + result.tree = repo_get_commit_tree(repo, commit);
> + } else {
> + onto = replay_pick_regular_commit(repo, commit, replayed_commits,
> + onto, &merge_opts, &result);
> + if (!onto)
> + break;
> + }
Hmph, I would have expected that the overall flow of this command
would be
* find the commits above and including the <commit> in question,
making sure there is no merge.
* read metadata of <commit> like the parent (as we do not allow
merges), tree, author ident & time.
* create a new commit object that has the same metadata as <commit>
on top of the parent of <commit>, but with the updated message
and new committer ident & time.
* initialize a variable Current to point at the rewritten <commit>
* loop for each commit C in <commit>..HEAD range in reverse order
(we know we have a single strand of pearls):
- read metadata of C
- create a new commit object C' that has the same metadata and
message as C on top of the Current commit, with new committer
ident & time.
- make Current point at the resulting C'
* Point the Current with HEAD.
without having to touch any "pick" machinery. Why do we need to go
down to the merge machinery for a mere "reword" operation?
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH v5 06/12] builtin/history: implement "reword" subcommand
2025-10-21 21:34 ` Junio C Hamano
@ 2025-10-21 21:43 ` D. Ben Knoble
2025-10-27 9:58 ` Patrick Steinhardt
2025-10-27 9:58 ` Patrick Steinhardt
1 sibling, 1 reply; 278+ messages in thread
From: D. Ben Knoble @ 2025-10-21 21:43 UTC (permalink / raw)
To: Junio C Hamano
Cc: Patrick Steinhardt, git, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk, Elijah Newren,
Karthik Nayak
On Tue, Oct 21, 2025 at 5:34 PM Junio C Hamano <gitster@pobox.com> wrote:
>
> Patrick Steinhardt <ps@pks.im> writes:
>
> > Implement a new "reword" subcommand for git-history(1). This subcommand
> > is essentially the same as if a user performed an interactive rebase
> > with a single commit changed to use the "reword" verb.
>
> Oy. I've always wanted something like this in "rebase -i" myself.
>
> It is a bit sad that I have to learn a new command to do something
> obvious and trivial like this, but that's life ;-)
>
> Maybe "git history" becomes powerful enough and can replace my
> every-day use of "rebase -i".
>
> > @@ -9,6 +9,7 @@ SYNOPSIS
> > --------
> > [synopsis]
> > git history [<options>]
> > +git history reword [<options>] <commit>
>
> It is curious that a command-less form is still listed here,
> especially since this command "requires" a subcommand. I would have
> expected that there will be a single line here after implementing a
> single subcommand.
>
> > +`reword <commit> [--message=<message>]`::
>
> That should be `reword [--message=<message>] <commit>` no?
>
> > + Rewrite the commit message of the specified commit. All the other
> > + details of this commit remain unchanged. If no commit message is
> > + provided, then this command will spawn an editor with the current
> > + message of that commit.
>
> As long as it takes more than one -m and concatenates them just like
> "git commit -m <message1> -m <message2>" does, I would not complain
> too much that a command line option to give message encourages sloppy
> log messages.
>
> > + if (!onto) {
> > + onto = commit;
> > + result.clean = 1;
> > + result.tree = repo_get_commit_tree(repo, commit);
> > + } else {
> > + onto = replay_pick_regular_commit(repo, commit, replayed_commits,
> > + onto, &merge_opts, &result);
> > + if (!onto)
> > + break;
> > + }
>
> Hmph, I would have expected that the overall flow of this command
> would be
>
> * find the commits above and including the <commit> in question,
> making sure there is no merge.
I don't remember offhand if the implementation supports merges, so
this might not answer the question…
> without having to touch any "pick" machinery. Why do we need to go
> down to the merge machinery for a mere "reword" operation?
…but it would be nice to not overly restrict the commits that can be
reworded (IIRC, jj permits the equivalent).
--
D. Ben Knoble
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH v5 06/12] builtin/history: implement "reword" subcommand
2025-10-21 21:43 ` D. Ben Knoble
@ 2025-10-27 9:58 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-27 9:58 UTC (permalink / raw)
To: D. Ben Knoble
Cc: Junio C Hamano, git, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk, Elijah Newren,
Karthik Nayak
On Tue, Oct 21, 2025 at 05:43:16PM -0400, D. Ben Knoble wrote:
> On Tue, Oct 21, 2025 at 5:34 PM Junio C Hamano <gitster@pobox.com> wrote:
> > Hmph, I would have expected that the overall flow of this command
> > would be
> >
> > * find the commits above and including the <commit> in question,
> > making sure there is no merge.
>
> I don't remember offhand if the implementation supports merges, so
> this might not answer the question…
>
> > without having to touch any "pick" machinery. Why do we need to go
> > down to the merge machinery for a mere "reword" operation?
>
> …but it would be nice to not overly restrict the commits that can be
> reworded (IIRC, jj permits the equivalent).
I agree that it would be nice, but I'd defer that to the future. Let's
focus on the easy cases for now and then extend going forward.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v5 06/12] builtin/history: implement "reword" subcommand
2025-10-21 21:34 ` Junio C Hamano
2025-10-21 21:43 ` D. Ben Knoble
@ 2025-10-27 9:58 ` Patrick Steinhardt
1 sibling, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-27 9:58 UTC (permalink / raw)
To: Junio C Hamano
Cc: git, D. Ben Knoble, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk, Elijah Newren,
Karthik Nayak
On Tue, Oct 21, 2025 at 02:34:31PM -0700, Junio C Hamano wrote:
> Patrick Steinhardt <ps@pks.im> writes:
> > @@ -9,6 +9,7 @@ SYNOPSIS
> > --------
> > [synopsis]
> > git history [<options>]
> > +git history reword [<options>] <commit>
>
> It is curious that a command-less form is still listed here,
> especially since this command "requires" a subcommand. I would have
> expected that there will be a single line here after implementing a
> single subcommand.
True. You can execute `git history` alone, but it doesn't do anything
useful except for giving you an error and all the potential subcommands.
That's in contrast to e.g. git-reflog(1), which supports a command-less
mode that does something useful.
> > +`reword <commit> [--message=<message>]`::
>
> That should be `reword [--message=<message>] <commit>` no?
Indeed.
> > + Rewrite the commit message of the specified commit. All the other
> > + details of this commit remain unchanged. If no commit message is
> > + provided, then this command will spawn an editor with the current
> > + message of that commit.
>
> As long as it takes more than one -m and concatenates them just like
> "git commit -m <message1> -m <message2>" does, I would not complain
> too much that a command line option to give message encourages sloppy
> log messages.
I'll for now defer the discussion around "-m" completely, as the design
isn't entirely clear yet in the first place. So I'll just drop the
option in the next iteration.
> > + if (!onto) {
> > + onto = commit;
> > + result.clean = 1;
> > + result.tree = repo_get_commit_tree(repo, commit);
> > + } else {
> > + onto = replay_pick_regular_commit(repo, commit, replayed_commits,
> > + onto, &merge_opts, &result);
> > + if (!onto)
> > + break;
> > + }
>
> Hmph, I would have expected that the overall flow of this command
> would be
>
> * find the commits above and including the <commit> in question,
> making sure there is no merge.
>
> * read metadata of <commit> like the parent (as we do not allow
> merges), tree, author ident & time.
>
> * create a new commit object that has the same metadata as <commit>
> on top of the parent of <commit>, but with the updated message
> and new committer ident & time.
>
> * initialize a variable Current to point at the rewritten <commit>
>
> * loop for each commit C in <commit>..HEAD range in reverse order
> (we know we have a single strand of pearls):
>
> - read metadata of C
>
> - create a new commit object C' that has the same metadata and
> message as C on top of the Current commit, with new committer
> ident & time.
>
> - make Current point at the resulting C'
>
> * Point the Current with HEAD.
>
> without having to touch any "pick" machinery. Why do we need to go
> down to the merge machinery for a mere "reword" operation?
You're exactly right, we don't need the "pick" machinery at all right
now. I think this is still a leftover from previous iterations, where I
was also driving things like an "edit" command that _do_ require merges.
But neither splitting a commit nor rewording it does require a merge at
all.
Will simplify. We can reintroduce the heavier machinery at a later point
in time as needed.
Thanks!
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH v5 07/12] add-patch: split out header from "add-interactive.h"
2025-10-21 14:15 ` [PATCH v5 " Patrick Steinhardt
` (5 preceding siblings ...)
2025-10-21 14:15 ` [PATCH v5 06/12] builtin/history: implement "reword" subcommand Patrick Steinhardt
@ 2025-10-21 14:15 ` Patrick Steinhardt
2025-10-21 14:15 ` [PATCH v5 08/12] add-patch: split out `struct interactive_options` Patrick Steinhardt
` (5 subsequent siblings)
12 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-21 14:15 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
While we have a "add-patch.c" code file, its declarations are part of
"add-interactive.h". This makes it somewhat harder than necessary to
find relevant code and to identify clear boundaries between the two
subsystems.
Split up concerns and move declarations that relate to "add-patch.c"
into a new "add-patch.h" header.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
add-interactive.h | 23 +++--------------------
add-patch.c | 1 +
add-patch.h | 26 ++++++++++++++++++++++++++
3 files changed, 30 insertions(+), 20 deletions(-)
diff --git a/add-interactive.h b/add-interactive.h
index da49502b765..2e3d1d871d2 100644
--- a/add-interactive.h
+++ b/add-interactive.h
@@ -1,14 +1,11 @@
#ifndef ADD_INTERACTIVE_H
#define ADD_INTERACTIVE_H
+#include "add-patch.h"
#include "color.h"
-struct add_p_opt {
- int context;
- int interhunkcontext;
-};
-
-#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }
+struct pathspec;
+struct repository;
struct add_i_state {
struct repository *r;
@@ -35,21 +32,7 @@ void init_add_i_state(struct add_i_state *s, struct repository *r,
struct add_p_opt *add_p_opt);
void clear_add_i_state(struct add_i_state *s);
-struct repository;
-struct pathspec;
int run_add_i(struct repository *r, const struct pathspec *ps,
struct add_p_opt *add_p_opt);
-enum add_p_mode {
- ADD_P_ADD,
- ADD_P_STASH,
- ADD_P_RESET,
- ADD_P_CHECKOUT,
- ADD_P_WORKTREE,
-};
-
-int run_add_p(struct repository *r, enum add_p_mode mode,
- struct add_p_opt *o, const char *revision,
- const struct pathspec *ps);
-
#endif
diff --git a/add-patch.c b/add-patch.c
index 9402dc71bc6..3bb7bcf3d26 100644
--- a/add-patch.c
+++ b/add-patch.c
@@ -3,6 +3,7 @@
#include "git-compat-util.h"
#include "add-interactive.h"
+#include "add-patch.h"
#include "advice.h"
#include "editor.h"
#include "environment.h"
diff --git a/add-patch.h b/add-patch.h
new file mode 100644
index 00000000000..4394c741076
--- /dev/null
+++ b/add-patch.h
@@ -0,0 +1,26 @@
+#ifndef ADD_PATCH_H
+#define ADD_PATCH_H
+
+struct pathspec;
+struct repository;
+
+struct add_p_opt {
+ int context;
+ int interhunkcontext;
+};
+
+#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }
+
+enum add_p_mode {
+ ADD_P_ADD,
+ ADD_P_STASH,
+ ADD_P_RESET,
+ ADD_P_CHECKOUT,
+ ADD_P_WORKTREE,
+};
+
+int run_add_p(struct repository *r, enum add_p_mode mode,
+ struct add_p_opt *o, const char *revision,
+ const struct pathspec *ps);
+
+#endif
--
2.51.1.851.g4ebd6896fd.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH v5 08/12] add-patch: split out `struct interactive_options`
2025-10-21 14:15 ` [PATCH v5 " Patrick Steinhardt
` (6 preceding siblings ...)
2025-10-21 14:15 ` [PATCH v5 07/12] add-patch: split out header from "add-interactive.h" Patrick Steinhardt
@ 2025-10-21 14:15 ` Patrick Steinhardt
2025-10-21 14:15 ` [PATCH v5 09/12] add-patch: remove dependency on "add-interactive" subsystem Patrick Steinhardt
` (4 subsequent siblings)
12 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-21 14:15 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
The `struct add_p_opt` is reused both by our infra for "git add -p" and
"git add -i". Users of `run_add_i()` for example are expected to pass
`struct add_p_opt`. This is somewhat confusing and raises the question
of which options apply to what part of the stack.
But things are even more confusing than that: while callers are expected
to pass in `struct add_p_opt`, these options ultimately get used to
initialize a `struct add_i_state` that is used by both subsystems. So we
are basically going full circle here.
Refactor the code and split out a new `struct interactive_options` that
hosts common options used by both. These options are then applied to a
`struct interactive_config` that hosts common configuration.
This refactoring doesn't yet fully detangle the two subsystems from one
another, as we still end up calling `init_add_i_state()` in the "git add
-p" subsystem. This will be fixed in a subsequent commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
add-interactive.c | 174 +++++++++++------------------------------------------
add-interactive.h | 23 +------
add-patch.c | 170 +++++++++++++++++++++++++++++++++++++++++++--------
add-patch.h | 36 ++++++++++-
builtin/add.c | 22 +++----
builtin/checkout.c | 4 +-
builtin/commit.c | 16 ++---
builtin/reset.c | 16 ++---
builtin/stash.c | 46 +++++++-------
commit.h | 2 +-
10 files changed, 270 insertions(+), 239 deletions(-)
diff --git a/add-interactive.c b/add-interactive.c
index 68fc09547dd..05d2e7eefe3 100644
--- a/add-interactive.c
+++ b/add-interactive.c
@@ -3,7 +3,6 @@
#include "git-compat-util.h"
#include "add-interactive.h"
#include "color.h"
-#include "config.h"
#include "diffcore.h"
#include "gettext.h"
#include "hash.h"
@@ -20,119 +19,18 @@
#include "prompt.h"
#include "tree.h"
-static void init_color(struct repository *r, enum git_colorbool use_color,
- const char *section_and_slot, char *dst,
- const char *default_color)
-{
- char *key = xstrfmt("color.%s", section_and_slot);
- const char *value;
-
- if (!want_color(use_color))
- dst[0] = '\0';
- else if (repo_config_get_value(r, key, &value) ||
- color_parse(value, dst))
- strlcpy(dst, default_color, COLOR_MAXLEN);
-
- free(key);
-}
-
-static enum git_colorbool check_color_config(struct repository *r, const char *var)
-{
- const char *value;
- enum git_colorbool ret;
-
- if (repo_config_get_value(r, var, &value))
- ret = GIT_COLOR_UNKNOWN;
- else
- ret = git_config_colorbool(var, value);
-
- /*
- * Do not rely on want_color() to fall back to color.ui for us. It uses
- * the value parsed by git_color_config(), which may not have been
- * called by the main command.
- */
- if (ret == GIT_COLOR_UNKNOWN &&
- !repo_config_get_value(r, "color.ui", &value))
- ret = git_config_colorbool("color.ui", value);
-
- return ret;
-}
-
void init_add_i_state(struct add_i_state *s, struct repository *r,
- struct add_p_opt *add_p_opt)
+ struct interactive_options *opts)
{
s->r = r;
- s->context = -1;
- s->interhunkcontext = -1;
-
- s->use_color_interactive = check_color_config(r, "color.interactive");
-
- init_color(r, s->use_color_interactive, "interactive.header",
- s->header_color, GIT_COLOR_BOLD);
- init_color(r, s->use_color_interactive, "interactive.help",
- s->help_color, GIT_COLOR_BOLD_RED);
- init_color(r, s->use_color_interactive, "interactive.prompt",
- s->prompt_color, GIT_COLOR_BOLD_BLUE);
- init_color(r, s->use_color_interactive, "interactive.error",
- s->error_color, GIT_COLOR_BOLD_RED);
- strlcpy(s->reset_color_interactive,
- want_color(s->use_color_interactive) ? GIT_COLOR_RESET : "", COLOR_MAXLEN);
-
- s->use_color_diff = check_color_config(r, "color.diff");
-
- init_color(r, s->use_color_diff, "diff.frag", s->fraginfo_color,
- diff_get_color(s->use_color_diff, DIFF_FRAGINFO));
- init_color(r, s->use_color_diff, "diff.context", s->context_color,
- "fall back");
- if (!strcmp(s->context_color, "fall back"))
- init_color(r, s->use_color_diff, "diff.plain",
- s->context_color,
- diff_get_color(s->use_color_diff, DIFF_CONTEXT));
- init_color(r, s->use_color_diff, "diff.old", s->file_old_color,
- diff_get_color(s->use_color_diff, DIFF_FILE_OLD));
- init_color(r, s->use_color_diff, "diff.new", s->file_new_color,
- diff_get_color(s->use_color_diff, DIFF_FILE_NEW));
- strlcpy(s->reset_color_diff,
- want_color(s->use_color_diff) ? GIT_COLOR_RESET : "", COLOR_MAXLEN);
-
- FREE_AND_NULL(s->interactive_diff_filter);
- repo_config_get_string(r, "interactive.difffilter",
- &s->interactive_diff_filter);
-
- FREE_AND_NULL(s->interactive_diff_algorithm);
- repo_config_get_string(r, "diff.algorithm",
- &s->interactive_diff_algorithm);
-
- if (!repo_config_get_int(r, "diff.context", &s->context))
- if (s->context < 0)
- die(_("%s cannot be negative"), "diff.context");
- if (!repo_config_get_int(r, "diff.interHunkContext", &s->interhunkcontext))
- if (s->interhunkcontext < 0)
- die(_("%s cannot be negative"), "diff.interHunkContext");
-
- repo_config_get_bool(r, "interactive.singlekey", &s->use_single_key);
- if (s->use_single_key)
- setbuf(stdin, NULL);
-
- if (add_p_opt->context != -1) {
- if (add_p_opt->context < 0)
- die(_("%s cannot be negative"), "--unified");
- s->context = add_p_opt->context;
- }
- if (add_p_opt->interhunkcontext != -1) {
- if (add_p_opt->interhunkcontext < 0)
- die(_("%s cannot be negative"), "--inter-hunk-context");
- s->interhunkcontext = add_p_opt->interhunkcontext;
- }
+ interactive_config_init(&s->cfg, r, opts);
}
void clear_add_i_state(struct add_i_state *s)
{
- FREE_AND_NULL(s->interactive_diff_filter);
- FREE_AND_NULL(s->interactive_diff_algorithm);
+ interactive_config_clear(&s->cfg);
memset(s, 0, sizeof(*s));
- s->use_color_interactive = GIT_COLOR_UNKNOWN;
- s->use_color_diff = GIT_COLOR_UNKNOWN;
+ interactive_config_clear(&s->cfg);
}
/*
@@ -286,7 +184,7 @@ static void list(struct add_i_state *s, struct string_list *list, int *selected,
return;
if (opts->header)
- color_fprintf_ln(stdout, s->header_color,
+ color_fprintf_ln(stdout, s->cfg.header_color,
"%s", opts->header);
for (i = 0; i < list->nr; i++) {
@@ -354,7 +252,7 @@ static ssize_t list_and_choose(struct add_i_state *s,
list(s, &items->items, items->selected, &opts->list_opts);
- color_fprintf(stdout, s->prompt_color, "%s", opts->prompt);
+ color_fprintf(stdout, s->cfg.prompt_color, "%s", opts->prompt);
fputs(singleton ? "> " : ">> ", stdout);
fflush(stdout);
@@ -432,7 +330,7 @@ static ssize_t list_and_choose(struct add_i_state *s,
if (from < 0 || from >= items->items.nr ||
(singleton && from + 1 != to)) {
- color_fprintf_ln(stderr, s->error_color,
+ color_fprintf_ln(stderr, s->cfg.error_color,
_("Huh (%s)?"), p);
break;
} else if (singleton) {
@@ -992,7 +890,7 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps,
free(files->items.items[i].string);
} else if (item->index.unmerged ||
item->worktree.unmerged) {
- color_fprintf_ln(stderr, s->error_color,
+ color_fprintf_ln(stderr, s->cfg.error_color,
_("ignoring unmerged: %s"),
files->items.items[i].string);
free(item);
@@ -1014,9 +912,9 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps,
opts->prompt = N_("Patch update");
count = list_and_choose(s, files, opts);
if (count > 0) {
- struct add_p_opt add_p_opt = {
- .context = s->context,
- .interhunkcontext = s->interhunkcontext,
+ struct interactive_options opts = {
+ .context = s->cfg.context,
+ .interhunkcontext = s->cfg.interhunkcontext,
};
struct strvec args = STRVEC_INIT;
struct pathspec ps_selected = { 0 };
@@ -1028,7 +926,7 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps,
parse_pathspec(&ps_selected,
PATHSPEC_ALL_MAGIC & ~PATHSPEC_LITERAL,
PATHSPEC_LITERAL_PATH, "", args.v);
- res = run_add_p(s->r, ADD_P_ADD, &add_p_opt, NULL, &ps_selected);
+ res = run_add_p(s->r, ADD_P_ADD, &opts, NULL, &ps_selected);
strvec_clear(&args);
clear_pathspec(&ps_selected);
}
@@ -1064,10 +962,10 @@ static int run_diff(struct add_i_state *s, const struct pathspec *ps,
struct child_process cmd = CHILD_PROCESS_INIT;
strvec_pushl(&cmd.args, "git", "diff", "-p", "--cached", NULL);
- if (s->context != -1)
- strvec_pushf(&cmd.args, "--unified=%i", s->context);
- if (s->interhunkcontext != -1)
- strvec_pushf(&cmd.args, "--inter-hunk-context=%i", s->interhunkcontext);
+ if (s->cfg.context != -1)
+ strvec_pushf(&cmd.args, "--unified=%i", s->cfg.context);
+ if (s->cfg.interhunkcontext != -1)
+ strvec_pushf(&cmd.args, "--inter-hunk-context=%i", s->cfg.interhunkcontext);
strvec_pushl(&cmd.args, oid_to_hex(!is_initial ? &oid :
s->r->hash_algo->empty_tree), "--", NULL);
for (i = 0; i < files->items.nr; i++)
@@ -1085,17 +983,17 @@ static int run_help(struct add_i_state *s, const struct pathspec *ps UNUSED,
struct prefix_item_list *files UNUSED,
struct list_and_choose_options *opts UNUSED)
{
- color_fprintf_ln(stdout, s->help_color, "status - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "status - %s",
_("show paths with changes"));
- color_fprintf_ln(stdout, s->help_color, "update - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "update - %s",
_("add working tree state to the staged set of changes"));
- color_fprintf_ln(stdout, s->help_color, "revert - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "revert - %s",
_("revert staged set of changes back to the HEAD version"));
- color_fprintf_ln(stdout, s->help_color, "patch - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "patch - %s",
_("pick hunks and update selectively"));
- color_fprintf_ln(stdout, s->help_color, "diff - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "diff - %s",
_("view diff between HEAD and index"));
- color_fprintf_ln(stdout, s->help_color, "add untracked - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "add untracked - %s",
_("add contents of untracked files to the staged set of changes"));
return 0;
@@ -1103,21 +1001,21 @@ static int run_help(struct add_i_state *s, const struct pathspec *ps UNUSED,
static void choose_prompt_help(struct add_i_state *s)
{
- color_fprintf_ln(stdout, s->help_color, "%s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "%s",
_("Prompt help:"));
- color_fprintf_ln(stdout, s->help_color, "1 - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "1 - %s",
_("select a single item"));
- color_fprintf_ln(stdout, s->help_color, "3-5 - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "3-5 - %s",
_("select a range of items"));
- color_fprintf_ln(stdout, s->help_color, "2-3,6-9 - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "2-3,6-9 - %s",
_("select multiple ranges"));
- color_fprintf_ln(stdout, s->help_color, "foo - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "foo - %s",
_("select item based on unique prefix"));
- color_fprintf_ln(stdout, s->help_color, "-... - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "-... - %s",
_("unselect specified items"));
- color_fprintf_ln(stdout, s->help_color, "* - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "* - %s",
_("choose all items"));
- color_fprintf_ln(stdout, s->help_color, " - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, " - %s",
_("(empty) finish selecting"));
}
@@ -1152,7 +1050,7 @@ static void print_command_item(int i, int selected UNUSED,
static void command_prompt_help(struct add_i_state *s)
{
- const char *help_color = s->help_color;
+ const char *help_color = s->cfg.help_color;
color_fprintf_ln(stdout, help_color, "%s", _("Prompt help:"));
color_fprintf_ln(stdout, help_color, "1 - %s",
_("select a numbered item"));
@@ -1163,7 +1061,7 @@ static void command_prompt_help(struct add_i_state *s)
}
int run_add_i(struct repository *r, const struct pathspec *ps,
- struct add_p_opt *add_p_opt)
+ struct interactive_options *interactive_opts)
{
struct add_i_state s = { NULL };
struct print_command_item_data data = { "[", "]" };
@@ -1206,15 +1104,15 @@ int run_add_i(struct repository *r, const struct pathspec *ps,
->util = util;
}
- init_add_i_state(&s, r, add_p_opt);
+ init_add_i_state(&s, r, interactive_opts);
/*
* When color was asked for, use the prompt color for
* highlighting, otherwise use square brackets.
*/
- if (want_color(s.use_color_interactive)) {
- data.color = s.prompt_color;
- data.reset = s.reset_color_interactive;
+ if (want_color(s.cfg.use_color_interactive)) {
+ data.color = s.cfg.prompt_color;
+ data.reset = s.cfg.reset_color_interactive;
}
print_file_item_data.color = data.color;
print_file_item_data.reset = data.reset;
diff --git a/add-interactive.h b/add-interactive.h
index 2e3d1d871d2..eefa2edc7c1 100644
--- a/add-interactive.h
+++ b/add-interactive.h
@@ -2,37 +2,20 @@
#define ADD_INTERACTIVE_H
#include "add-patch.h"
-#include "color.h"
struct pathspec;
struct repository;
struct add_i_state {
struct repository *r;
- enum git_colorbool use_color_interactive;
- enum git_colorbool use_color_diff;
- char header_color[COLOR_MAXLEN];
- char help_color[COLOR_MAXLEN];
- char prompt_color[COLOR_MAXLEN];
- char error_color[COLOR_MAXLEN];
- char reset_color_interactive[COLOR_MAXLEN];
-
- char fraginfo_color[COLOR_MAXLEN];
- char context_color[COLOR_MAXLEN];
- char file_old_color[COLOR_MAXLEN];
- char file_new_color[COLOR_MAXLEN];
- char reset_color_diff[COLOR_MAXLEN];
-
- int use_single_key;
- char *interactive_diff_filter, *interactive_diff_algorithm;
- int context, interhunkcontext;
+ struct interactive_config cfg;
};
void init_add_i_state(struct add_i_state *s, struct repository *r,
- struct add_p_opt *add_p_opt);
+ struct interactive_options *opts);
void clear_add_i_state(struct add_i_state *s);
int run_add_i(struct repository *r, const struct pathspec *ps,
- struct add_p_opt *add_p_opt);
+ struct interactive_options *opts);
#endif
diff --git a/add-patch.c b/add-patch.c
index 3bb7bcf3d26..1c625a7d7a7 100644
--- a/add-patch.c
+++ b/add-patch.c
@@ -5,6 +5,8 @@
#include "add-interactive.h"
#include "add-patch.h"
#include "advice.h"
+#include "config.h"
+#include "diff.h"
#include "editor.h"
#include "environment.h"
#include "gettext.h"
@@ -279,6 +281,122 @@ struct add_p_state {
const char *revision;
};
+static void init_color(struct repository *r,
+ enum git_colorbool use_color,
+ const char *section_and_slot, char *dst,
+ const char *default_color)
+{
+ char *key = xstrfmt("color.%s", section_and_slot);
+ const char *value;
+
+ if (!want_color(use_color))
+ dst[0] = '\0';
+ else if (repo_config_get_value(r, key, &value) ||
+ color_parse(value, dst))
+ strlcpy(dst, default_color, COLOR_MAXLEN);
+
+ free(key);
+}
+
+static enum git_colorbool check_color_config(struct repository *r, const char *var)
+{
+ const char *value;
+ enum git_colorbool ret;
+
+ if (repo_config_get_value(r, var, &value))
+ ret = GIT_COLOR_UNKNOWN;
+ else
+ ret = git_config_colorbool(var, value);
+
+ /*
+ * Do not rely on want_color() to fall back to color.ui for us. It uses
+ * the value parsed by git_color_config(), which may not have been
+ * called by the main command.
+ */
+ if (ret == GIT_COLOR_UNKNOWN &&
+ !repo_config_get_value(r, "color.ui", &value))
+ ret = git_config_colorbool("color.ui", value);
+
+ return ret;
+}
+
+void interactive_config_init(struct interactive_config *cfg,
+ struct repository *r,
+ struct interactive_options *opts)
+{
+ cfg->context = -1;
+ cfg->interhunkcontext = -1;
+
+ cfg->use_color_interactive = check_color_config(r, "color.interactive");
+
+ init_color(r, cfg->use_color_interactive, "interactive.header",
+ cfg->header_color, GIT_COLOR_BOLD);
+ init_color(r, cfg->use_color_interactive, "interactive.help",
+ cfg->help_color, GIT_COLOR_BOLD_RED);
+ init_color(r, cfg->use_color_interactive, "interactive.prompt",
+ cfg->prompt_color, GIT_COLOR_BOLD_BLUE);
+ init_color(r, cfg->use_color_interactive, "interactive.error",
+ cfg->error_color, GIT_COLOR_BOLD_RED);
+ strlcpy(cfg->reset_color_interactive,
+ want_color(cfg->use_color_interactive) ? GIT_COLOR_RESET : "", COLOR_MAXLEN);
+
+ cfg->use_color_diff = check_color_config(r, "color.diff");
+
+ init_color(r, cfg->use_color_diff, "diff.frag", cfg->fraginfo_color,
+ diff_get_color(cfg->use_color_diff, DIFF_FRAGINFO));
+ init_color(r, cfg->use_color_diff, "diff.context", cfg->context_color,
+ "fall back");
+ if (!strcmp(cfg->context_color, "fall back"))
+ init_color(r, cfg->use_color_diff, "diff.plain",
+ cfg->context_color,
+ diff_get_color(cfg->use_color_diff, DIFF_CONTEXT));
+ init_color(r, cfg->use_color_diff, "diff.old", cfg->file_old_color,
+ diff_get_color(cfg->use_color_diff, DIFF_FILE_OLD));
+ init_color(r, cfg->use_color_diff, "diff.new", cfg->file_new_color,
+ diff_get_color(cfg->use_color_diff, DIFF_FILE_NEW));
+ strlcpy(cfg->reset_color_diff,
+ want_color(cfg->use_color_diff) ? GIT_COLOR_RESET : "", COLOR_MAXLEN);
+
+ FREE_AND_NULL(cfg->interactive_diff_filter);
+ repo_config_get_string(r, "interactive.difffilter",
+ &cfg->interactive_diff_filter);
+
+ FREE_AND_NULL(cfg->interactive_diff_algorithm);
+ repo_config_get_string(r, "diff.algorithm",
+ &cfg->interactive_diff_algorithm);
+
+ if (!repo_config_get_int(r, "diff.context", &cfg->context))
+ if (cfg->context < 0)
+ die(_("%s cannot be negative"), "diff.context");
+ if (!repo_config_get_int(r, "diff.interHunkContext", &cfg->interhunkcontext))
+ if (cfg->interhunkcontext < 0)
+ die(_("%s cannot be negative"), "diff.interHunkContext");
+
+ repo_config_get_bool(r, "interactive.singlekey", &cfg->use_single_key);
+ if (cfg->use_single_key)
+ setbuf(stdin, NULL);
+
+ if (opts->context != -1) {
+ if (opts->context < 0)
+ die(_("%s cannot be negative"), "--unified");
+ cfg->context = opts->context;
+ }
+ if (opts->interhunkcontext != -1) {
+ if (opts->interhunkcontext < 0)
+ die(_("%s cannot be negative"), "--inter-hunk-context");
+ cfg->interhunkcontext = opts->interhunkcontext;
+ }
+}
+
+void interactive_config_clear(struct interactive_config *cfg)
+{
+ FREE_AND_NULL(cfg->interactive_diff_filter);
+ FREE_AND_NULL(cfg->interactive_diff_algorithm);
+ memset(cfg, 0, sizeof(*cfg));
+ cfg->use_color_interactive = GIT_COLOR_UNKNOWN;
+ cfg->use_color_diff = GIT_COLOR_UNKNOWN;
+}
+
static void add_p_state_clear(struct add_p_state *s)
{
size_t i;
@@ -299,9 +417,9 @@ static void err(struct add_p_state *s, const char *fmt, ...)
va_list args;
va_start(args, fmt);
- fputs(s->s.error_color, stdout);
+ fputs(s->s.cfg.error_color, stdout);
vprintf(fmt, args);
- puts(s->s.reset_color_interactive);
+ puts(s->s.cfg.reset_color_interactive);
va_end(args);
}
@@ -424,12 +542,12 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
int res;
strvec_pushv(&args, s->mode->diff_cmd);
- if (s->s.context != -1)
- strvec_pushf(&args, "--unified=%i", s->s.context);
- if (s->s.interhunkcontext != -1)
- strvec_pushf(&args, "--inter-hunk-context=%i", s->s.interhunkcontext);
- if (s->s.interactive_diff_algorithm)
- strvec_pushf(&args, "--diff-algorithm=%s", s->s.interactive_diff_algorithm);
+ if (s->s.cfg.context != -1)
+ strvec_pushf(&args, "--unified=%i", s->s.cfg.context);
+ if (s->s.cfg.interhunkcontext != -1)
+ strvec_pushf(&args, "--inter-hunk-context=%i", s->s.cfg.interhunkcontext);
+ if (s->s.cfg.interactive_diff_algorithm)
+ strvec_pushf(&args, "--diff-algorithm=%s", s->s.cfg.interactive_diff_algorithm);
if (s->revision) {
struct object_id oid;
strvec_push(&args,
@@ -458,9 +576,9 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
}
strbuf_complete_line(plain);
- if (want_color_fd(1, s->s.use_color_diff)) {
+ if (want_color_fd(1, s->s.cfg.use_color_diff)) {
struct child_process colored_cp = CHILD_PROCESS_INIT;
- const char *diff_filter = s->s.interactive_diff_filter;
+ const char *diff_filter = s->s.cfg.interactive_diff_filter;
setup_child_process(s, &colored_cp, NULL);
xsnprintf((char *)args.v[color_arg_index], 8, "--color");
@@ -693,7 +811,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
hunk->colored_end - hunk->colored_start);
return;
} else {
- strbuf_addstr(out, s->s.fraginfo_color);
+ strbuf_addstr(out, s->s.cfg.fraginfo_color);
p = s->colored.buf + header->colored_extra_start;
len = header->colored_extra_end
- header->colored_extra_start;
@@ -715,7 +833,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
if (len)
strbuf_add(out, p, len);
else if (colored)
- strbuf_addf(out, "%s\n", s->s.reset_color_diff);
+ strbuf_addf(out, "%s\n", s->s.cfg.reset_color_diff);
else
strbuf_addch(out, '\n');
}
@@ -1104,12 +1222,12 @@ static void recolor_hunk(struct add_p_state *s, struct hunk *hunk)
strbuf_addstr(&s->colored,
plain[current] == '-' ?
- s->s.file_old_color :
+ s->s.cfg.file_old_color :
plain[current] == '+' ?
- s->s.file_new_color :
- s->s.context_color);
+ s->s.cfg.file_new_color :
+ s->s.cfg.context_color);
strbuf_add(&s->colored, plain + current, eol - current);
- strbuf_addstr(&s->colored, s->s.reset_color_diff);
+ strbuf_addstr(&s->colored, s->s.cfg.reset_color_diff);
if (next > eol)
strbuf_add(&s->colored, plain + eol, next - eol);
current = next;
@@ -1238,7 +1356,7 @@ static int run_apply_check(struct add_p_state *s,
static int read_single_character(struct add_p_state *s)
{
- if (s->s.use_single_key) {
+ if (s->s.cfg.use_single_key) {
int res = read_key_without_echo(&s->answer);
printf("%s\n", res == EOF ? "" : s->answer.buf);
return res;
@@ -1252,7 +1370,7 @@ static int read_single_character(struct add_p_state *s)
static int prompt_yesno(struct add_p_state *s, const char *prompt)
{
for (;;) {
- color_fprintf(stdout, s->s.prompt_color, "%s", _(prompt));
+ color_fprintf(stdout, s->s.cfg.prompt_color, "%s", _(prompt));
fflush(stdout);
if (read_single_character(s) == EOF)
return -1;
@@ -1559,15 +1677,15 @@ static int patch_update_file(struct add_p_state *s,
else
prompt_mode_type = PROMPT_HUNK;
- printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->s.prompt_color,
+ printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->s.cfg.prompt_color,
(uintmax_t)hunk_index + 1,
(uintmax_t)(file_diff->hunk_nr
? file_diff->hunk_nr
: 1));
printf(_(s->mode->prompt_mode[prompt_mode_type]),
s->buf.buf);
- if (*s->s.reset_color_interactive)
- fputs(s->s.reset_color_interactive, stdout);
+ if (*s->s.cfg.reset_color_interactive)
+ fputs(s->s.cfg.reset_color_interactive, stdout);
fflush(stdout);
if (read_single_character(s) == EOF)
break;
@@ -1729,7 +1847,7 @@ static int patch_update_file(struct add_p_state *s,
err(s, _("Sorry, cannot split this hunk"));
} else if (!split_hunk(s, file_diff,
hunk - file_diff->hunk)) {
- color_fprintf_ln(stdout, s->s.header_color,
+ color_fprintf_ln(stdout, s->s.cfg.header_color,
_("Split into %d hunks."),
(int)splittable_into);
rendered_hunk_index = -1;
@@ -1747,7 +1865,7 @@ static int patch_update_file(struct add_p_state *s,
} else if (s->answer.buf[0] == '?') {
const char *p = _(help_patch_remainder), *eol = p;
- color_fprintf(stdout, s->s.help_color, "%s",
+ color_fprintf(stdout, s->s.cfg.help_color, "%s",
_(s->mode->help_patch_text));
/*
@@ -1765,7 +1883,7 @@ static int patch_update_file(struct add_p_state *s,
if (*p != '?' && !strchr(s->buf.buf, *p))
continue;
- color_fprintf_ln(stdout, s->s.help_color,
+ color_fprintf_ln(stdout, s->s.cfg.help_color,
"%.*s", (int)(eol - p), p);
}
} else {
@@ -1806,7 +1924,7 @@ static int patch_update_file(struct add_p_state *s,
}
int run_add_p(struct repository *r, enum add_p_mode mode,
- struct add_p_opt *o, const char *revision,
+ struct interactive_options *opts, const char *revision,
const struct pathspec *ps)
{
struct add_p_state s = {
@@ -1814,7 +1932,7 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
};
size_t i, binary_count = 0;
- init_add_i_state(&s.s, r, o);
+ init_add_i_state(&s.s, r, opts);
if (mode == ADD_P_STASH)
s.mode = &patch_mode_stash;
diff --git a/add-patch.h b/add-patch.h
index 4394c741076..a4a05d9d145 100644
--- a/add-patch.h
+++ b/add-patch.h
@@ -1,15 +1,45 @@
#ifndef ADD_PATCH_H
#define ADD_PATCH_H
+#include "color.h"
+
struct pathspec;
struct repository;
-struct add_p_opt {
+struct interactive_options {
int context;
int interhunkcontext;
};
-#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }
+#define INTERACTIVE_OPTIONS_INIT { \
+ .context = -1, \
+ .interhunkcontext = -1, \
+}
+
+struct interactive_config {
+ enum git_colorbool use_color_interactive;
+ enum git_colorbool use_color_diff;
+ char header_color[COLOR_MAXLEN];
+ char help_color[COLOR_MAXLEN];
+ char prompt_color[COLOR_MAXLEN];
+ char error_color[COLOR_MAXLEN];
+ char reset_color_interactive[COLOR_MAXLEN];
+
+ char fraginfo_color[COLOR_MAXLEN];
+ char context_color[COLOR_MAXLEN];
+ char file_old_color[COLOR_MAXLEN];
+ char file_new_color[COLOR_MAXLEN];
+ char reset_color_diff[COLOR_MAXLEN];
+
+ int use_single_key;
+ char *interactive_diff_filter, *interactive_diff_algorithm;
+ int context, interhunkcontext;
+};
+
+void interactive_config_init(struct interactive_config *cfg,
+ struct repository *r,
+ struct interactive_options *opts);
+void interactive_config_clear(struct interactive_config *cfg);
enum add_p_mode {
ADD_P_ADD,
@@ -20,7 +50,7 @@ enum add_p_mode {
};
int run_add_p(struct repository *r, enum add_p_mode mode,
- struct add_p_opt *o, const char *revision,
+ struct interactive_options *opts, const char *revision,
const struct pathspec *ps);
#endif
diff --git a/builtin/add.c b/builtin/add.c
index 32709794b38..6f1e2130528 100644
--- a/builtin/add.c
+++ b/builtin/add.c
@@ -31,7 +31,7 @@ static const char * const builtin_add_usage[] = {
NULL
};
static int patch_interactive, add_interactive, edit_interactive;
-static struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+static struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
static int take_worktree_changes;
static int add_renormalize;
static int pathspec_file_nul;
@@ -160,7 +160,7 @@ static int refresh(struct repository *repo, int verbose, const struct pathspec *
int interactive_add(struct repository *repo,
const char **argv,
const char *prefix,
- int patch, struct add_p_opt *add_p_opt)
+ int patch, struct interactive_options *interactive_opts)
{
struct pathspec pathspec;
int ret;
@@ -172,9 +172,9 @@ int interactive_add(struct repository *repo,
prefix, argv);
if (patch)
- ret = !!run_add_p(repo, ADD_P_ADD, add_p_opt, NULL, &pathspec);
+ ret = !!run_add_p(repo, ADD_P_ADD, interactive_opts, NULL, &pathspec);
else
- ret = !!run_add_i(repo, &pathspec, add_p_opt);
+ ret = !!run_add_i(repo, &pathspec, interactive_opts);
clear_pathspec(&pathspec);
return ret;
@@ -256,8 +256,8 @@ static struct option builtin_add_options[] = {
OPT_GROUP(""),
OPT_BOOL('i', "interactive", &add_interactive, N_("interactive picking")),
OPT_BOOL('p', "patch", &patch_interactive, N_("select hunks interactively")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT_BOOL('e', "edit", &edit_interactive, N_("edit current diff and apply")),
OPT__FORCE(&ignored_too, N_("allow adding otherwise ignored files"), 0),
OPT_BOOL('u', "update", &take_worktree_changes, N_("update tracked files")),
@@ -400,9 +400,9 @@ int cmd_add(int argc,
prepare_repo_settings(repo);
repo->settings.command_requires_full_index = 0;
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
if (patch_interactive)
@@ -412,11 +412,11 @@ int cmd_add(int argc,
die(_("options '%s' and '%s' cannot be used together"), "--dry-run", "--interactive/--patch");
if (pathspec_from_file)
die(_("options '%s' and '%s' cannot be used together"), "--pathspec-from-file", "--interactive/--patch");
- exit(interactive_add(repo, argv + 1, prefix, patch_interactive, &add_p_opt));
+ exit(interactive_add(repo, argv + 1, prefix, patch_interactive, &interactive_opts));
} else {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--interactive/--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--interactive/--patch");
}
diff --git a/builtin/checkout.c b/builtin/checkout.c
index f9453473fe2..d230b1f8995 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -546,7 +546,7 @@ static int checkout_paths(const struct checkout_opts *opts,
if (opts->patch_mode) {
enum add_p_mode patch_mode;
- struct add_p_opt add_p_opt = {
+ struct interactive_options interactive_opts = {
.context = opts->patch_context,
.interhunkcontext = opts->patch_interhunk_context,
};
@@ -575,7 +575,7 @@ static int checkout_paths(const struct checkout_opts *opts,
else
BUG("either flag must have been set, worktree=%d, index=%d",
opts->checkout_worktree, opts->checkout_index);
- return !!run_add_p(the_repository, patch_mode, &add_p_opt,
+ return !!run_add_p(the_repository, patch_mode, &interactive_opts,
rev, &opts->pathspec);
}
diff --git a/builtin/commit.c b/builtin/commit.c
index 0243f17d53c..640495cc57e 100644
--- a/builtin/commit.c
+++ b/builtin/commit.c
@@ -123,7 +123,7 @@ static const char *edit_message, *use_message;
static char *fixup_message, *fixup_commit, *squash_message;
static const char *fixup_prefix;
static int all, also, interactive, patch_interactive, only, amend, signoff;
-static struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+static struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
static int edit_flag = -1; /* unspecified */
static int quiet, verbose, no_verify, allow_empty, dry_run, renew_authorship;
static int config_commit_verbose = -1; /* unspecified */
@@ -356,9 +356,9 @@ static const char *prepare_index(const char **argv, const char *prefix,
const char *ret;
char *path = NULL;
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
if (is_status)
@@ -407,7 +407,7 @@ static const char *prepare_index(const char **argv, const char *prefix,
old_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT));
setenv(INDEX_ENVIRONMENT, the_repository->index_file, 1);
- if (interactive_add(the_repository, argv, prefix, patch_interactive, &add_p_opt) != 0)
+ if (interactive_add(the_repository, argv, prefix, patch_interactive, &interactive_opts) != 0)
die(_("interactive add failed"));
the_repository->index_file = old_repo_index_file;
@@ -432,9 +432,9 @@ static const char *prepare_index(const char **argv, const char *prefix,
ret = get_lock_file_path(&index_lock);
goto out;
} else {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--interactive/--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--interactive/--patch");
}
@@ -1742,8 +1742,8 @@ int cmd_commit(int argc,
OPT_BOOL('i', "include", &also, N_("add specified files to index for commit")),
OPT_BOOL(0, "interactive", &interactive, N_("interactively add files")),
OPT_BOOL('p', "patch", &patch_interactive, N_("interactively add changes")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT_BOOL('o', "only", &only, N_("commit only specified files")),
OPT_BOOL('n', "no-verify", &no_verify, N_("bypass pre-commit and commit-msg hooks")),
OPT_BOOL(0, "dry-run", &dry_run, N_("show what would be committed")),
diff --git a/builtin/reset.c b/builtin/reset.c
index ed35802af15..088449e1209 100644
--- a/builtin/reset.c
+++ b/builtin/reset.c
@@ -346,7 +346,7 @@ int cmd_reset(int argc,
struct object_id oid;
struct pathspec pathspec;
int intent_to_add = 0;
- struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
const struct option options[] = {
OPT__QUIET(&quiet, N_("be quiet, only report errors")),
OPT_BOOL(0, "no-refresh", &no_refresh,
@@ -371,8 +371,8 @@ int cmd_reset(int argc,
PARSE_OPT_OPTARG,
option_parse_recurse_submodules_worktree_updater),
OPT_BOOL('p', "patch", &patch_mode, N_("select hunks interactively")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT_BOOL('N', "intent-to-add", &intent_to_add,
N_("record only the fact that removed paths will be added later")),
OPT_PATHSPEC_FROM_FILE(&pathspec_from_file),
@@ -423,9 +423,9 @@ int cmd_reset(int argc,
oidcpy(&oid, &tree->object.oid);
}
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
prepare_repo_settings(the_repository);
@@ -436,12 +436,12 @@ int cmd_reset(int argc,
die(_("options '%s' and '%s' cannot be used together"), "--patch", "--{hard,mixed,soft}");
trace2_cmd_mode("patch-interactive");
update_ref_status = !!run_add_p(the_repository, ADD_P_RESET,
- &add_p_opt, rev, &pathspec);
+ &interactive_opts, rev, &pathspec);
goto cleanup;
} else {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--patch");
}
diff --git a/builtin/stash.c b/builtin/stash.c
index 948eba06fbc..3b509052338 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -1306,7 +1306,7 @@ static int stash_staged(struct stash_info *info, struct strbuf *out_patch,
static int stash_patch(struct stash_info *info, const struct pathspec *ps,
struct strbuf *out_patch, int quiet,
- struct add_p_opt *add_p_opt)
+ struct interactive_options *interactive_opts)
{
int ret = 0;
struct child_process cp_read_tree = CHILD_PROCESS_INIT;
@@ -1331,7 +1331,7 @@ static int stash_patch(struct stash_info *info, const struct pathspec *ps,
old_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT));
setenv(INDEX_ENVIRONMENT, the_repository->index_file, 1);
- ret = !!run_add_p(the_repository, ADD_P_STASH, add_p_opt, NULL, ps);
+ ret = !!run_add_p(the_repository, ADD_P_STASH, interactive_opts, NULL, ps);
the_repository->index_file = old_repo_index_file;
if (old_index_env && *old_index_env)
@@ -1427,7 +1427,8 @@ static int stash_working_tree(struct stash_info *info, const struct pathspec *ps
}
static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_buf,
- int include_untracked, int patch_mode, struct add_p_opt *add_p_opt,
+ int include_untracked, int patch_mode,
+ struct interactive_options *interactive_opts,
int only_staged, struct stash_info *info, struct strbuf *patch,
int quiet)
{
@@ -1509,7 +1510,7 @@ static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_b
untracked_commit_option = 1;
}
if (patch_mode) {
- ret = stash_patch(info, ps, patch, quiet, add_p_opt);
+ ret = stash_patch(info, ps, patch, quiet, interactive_opts);
if (ret < 0) {
if (!quiet)
fprintf_ln(stderr, _("Cannot save the current "
@@ -1595,7 +1596,8 @@ static int create_stash(int argc, const char **argv, const char *prefix UNUSED,
}
static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int quiet,
- int keep_index, int patch_mode, struct add_p_opt *add_p_opt,
+ int keep_index, int patch_mode,
+ struct interactive_options *interactive_opts,
int include_untracked, int only_staged)
{
int ret = 0;
@@ -1667,7 +1669,7 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q
if (stash_msg)
strbuf_addstr(&stash_msg_buf, stash_msg);
if (do_create_stash(ps, &stash_msg_buf, include_untracked, patch_mode,
- add_p_opt, only_staged, &info, &patch, quiet)) {
+ interactive_opts, only_staged, &info, &patch, quiet)) {
ret = -1;
goto done;
}
@@ -1841,7 +1843,7 @@ static int push_stash(int argc, const char **argv, const char *prefix,
const char *stash_msg = NULL;
char *pathspec_from_file = NULL;
struct pathspec ps;
- struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
struct option options[] = {
OPT_BOOL('k', "keep-index", &keep_index,
N_("keep index")),
@@ -1849,8 +1851,8 @@ static int push_stash(int argc, const char **argv, const char *prefix,
N_("stash staged changes only")),
OPT_BOOL('p', "patch", &patch_mode,
N_("stash in patch mode")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT__QUIET(&quiet, N_("quiet mode")),
OPT_BOOL('u', "include-untracked", &include_untracked,
N_("include untracked files in stash")),
@@ -1907,19 +1909,19 @@ static int push_stash(int argc, const char **argv, const char *prefix,
}
if (!patch_mode) {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--patch");
}
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
ret = do_push_stash(&ps, stash_msg, quiet, keep_index, patch_mode,
- &add_p_opt, include_untracked, only_staged);
+ &interactive_opts, include_untracked, only_staged);
clear_pathspec(&ps);
free(pathspec_from_file);
@@ -1944,7 +1946,7 @@ static int save_stash(int argc, const char **argv, const char *prefix,
const char *stash_msg = NULL;
struct pathspec ps;
struct strbuf stash_msg_buf = STRBUF_INIT;
- struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
struct option options[] = {
OPT_BOOL('k', "keep-index", &keep_index,
N_("keep index")),
@@ -1952,8 +1954,8 @@ static int save_stash(int argc, const char **argv, const char *prefix,
N_("stash staged changes only")),
OPT_BOOL('p', "patch", &patch_mode,
N_("stash in patch mode")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT__QUIET(&quiet, N_("quiet mode")),
OPT_BOOL('u', "include-untracked", &include_untracked,
N_("include untracked files in stash")),
@@ -1973,20 +1975,20 @@ static int save_stash(int argc, const char **argv, const char *prefix,
memset(&ps, 0, sizeof(ps));
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
if (!patch_mode) {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--patch");
}
ret = do_push_stash(&ps, stash_msg, quiet, keep_index,
- patch_mode, &add_p_opt, include_untracked,
+ patch_mode, &interactive_opts, include_untracked,
only_staged);
strbuf_release(&stash_msg_buf);
diff --git a/commit.h b/commit.h
index 1d6e0c7518b..7b6e59d6c19 100644
--- a/commit.h
+++ b/commit.h
@@ -258,7 +258,7 @@ int for_each_commit_graft(each_commit_graft_fn, void *);
int interactive_add(struct repository *repo,
const char **argv,
const char *prefix,
- int patch, struct add_p_opt *add_p_opt);
+ int patch, struct interactive_options *opts);
struct commit_extra_header {
struct commit_extra_header *next;
--
2.51.1.851.g4ebd6896fd.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH v5 09/12] add-patch: remove dependency on "add-interactive" subsystem
2025-10-21 14:15 ` [PATCH v5 " Patrick Steinhardt
` (7 preceding siblings ...)
2025-10-21 14:15 ` [PATCH v5 08/12] add-patch: split out `struct interactive_options` Patrick Steinhardt
@ 2025-10-21 14:15 ` Patrick Steinhardt
2025-10-21 14:15 ` [PATCH v5 10/12] add-patch: add support for in-memory index patching Patrick Steinhardt
` (3 subsequent siblings)
12 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-21 14:15 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
With the preceding commit we have split out interactive configuration
that is used by both "git add -p" and "git add -i". But we still
initialize that configuration in the "add -p" subsystem by calling
`init_add_i_state()`, even though we only do so to initialize the
interactive configuration as well as a repository pointer.
Stop doing so and instead store and initialize the interactive
configuration in `struct add_p_state` directly.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
add-patch.c | 70 ++++++++++++++++++++++++++++++++-----------------------------
1 file changed, 37 insertions(+), 33 deletions(-)
diff --git a/add-patch.c b/add-patch.c
index 1c625a7d7a7..362726c962d 100644
--- a/add-patch.c
+++ b/add-patch.c
@@ -2,7 +2,6 @@
#define DISABLE_SIGN_COMPARE_WARNINGS
#include "git-compat-util.h"
-#include "add-interactive.h"
#include "add-patch.h"
#include "advice.h"
#include "config.h"
@@ -263,7 +262,8 @@ struct hunk {
};
struct add_p_state {
- struct add_i_state s;
+ struct repository *r;
+ struct interactive_config cfg;
struct strbuf answer, buf;
/* parsed diff */
@@ -408,7 +408,7 @@ static void add_p_state_clear(struct add_p_state *s)
for (i = 0; i < s->file_diff_nr; i++)
free(s->file_diff[i].hunk);
free(s->file_diff);
- clear_add_i_state(&s->s);
+ interactive_config_clear(&s->cfg);
}
__attribute__((format (printf, 2, 3)))
@@ -417,9 +417,9 @@ static void err(struct add_p_state *s, const char *fmt, ...)
va_list args;
va_start(args, fmt);
- fputs(s->s.cfg.error_color, stdout);
+ fputs(s->cfg.error_color, stdout);
vprintf(fmt, args);
- puts(s->s.cfg.reset_color_interactive);
+ puts(s->cfg.reset_color_interactive);
va_end(args);
}
@@ -437,7 +437,7 @@ static void setup_child_process(struct add_p_state *s,
cp->git_cmd = 1;
strvec_pushf(&cp->env,
- INDEX_ENVIRONMENT "=%s", s->s.r->index_file);
+ INDEX_ENVIRONMENT "=%s", s->r->index_file);
}
static int parse_range(const char **p,
@@ -542,12 +542,12 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
int res;
strvec_pushv(&args, s->mode->diff_cmd);
- if (s->s.cfg.context != -1)
- strvec_pushf(&args, "--unified=%i", s->s.cfg.context);
- if (s->s.cfg.interhunkcontext != -1)
- strvec_pushf(&args, "--inter-hunk-context=%i", s->s.cfg.interhunkcontext);
- if (s->s.cfg.interactive_diff_algorithm)
- strvec_pushf(&args, "--diff-algorithm=%s", s->s.cfg.interactive_diff_algorithm);
+ if (s->cfg.context != -1)
+ strvec_pushf(&args, "--unified=%i", s->cfg.context);
+ if (s->cfg.interhunkcontext != -1)
+ strvec_pushf(&args, "--inter-hunk-context=%i", s->cfg.interhunkcontext);
+ if (s->cfg.interactive_diff_algorithm)
+ strvec_pushf(&args, "--diff-algorithm=%s", s->cfg.interactive_diff_algorithm);
if (s->revision) {
struct object_id oid;
strvec_push(&args,
@@ -576,9 +576,9 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
}
strbuf_complete_line(plain);
- if (want_color_fd(1, s->s.cfg.use_color_diff)) {
+ if (want_color_fd(1, s->cfg.use_color_diff)) {
struct child_process colored_cp = CHILD_PROCESS_INIT;
- const char *diff_filter = s->s.cfg.interactive_diff_filter;
+ const char *diff_filter = s->cfg.interactive_diff_filter;
setup_child_process(s, &colored_cp, NULL);
xsnprintf((char *)args.v[color_arg_index], 8, "--color");
@@ -811,7 +811,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
hunk->colored_end - hunk->colored_start);
return;
} else {
- strbuf_addstr(out, s->s.cfg.fraginfo_color);
+ strbuf_addstr(out, s->cfg.fraginfo_color);
p = s->colored.buf + header->colored_extra_start;
len = header->colored_extra_end
- header->colored_extra_start;
@@ -833,7 +833,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
if (len)
strbuf_add(out, p, len);
else if (colored)
- strbuf_addf(out, "%s\n", s->s.cfg.reset_color_diff);
+ strbuf_addf(out, "%s\n", s->cfg.reset_color_diff);
else
strbuf_addch(out, '\n');
}
@@ -1222,12 +1222,12 @@ static void recolor_hunk(struct add_p_state *s, struct hunk *hunk)
strbuf_addstr(&s->colored,
plain[current] == '-' ?
- s->s.cfg.file_old_color :
+ s->cfg.file_old_color :
plain[current] == '+' ?
- s->s.cfg.file_new_color :
- s->s.cfg.context_color);
+ s->cfg.file_new_color :
+ s->cfg.context_color);
strbuf_add(&s->colored, plain + current, eol - current);
- strbuf_addstr(&s->colored, s->s.cfg.reset_color_diff);
+ strbuf_addstr(&s->colored, s->cfg.reset_color_diff);
if (next > eol)
strbuf_add(&s->colored, plain + eol, next - eol);
current = next;
@@ -1356,7 +1356,7 @@ static int run_apply_check(struct add_p_state *s,
static int read_single_character(struct add_p_state *s)
{
- if (s->s.cfg.use_single_key) {
+ if (s->cfg.use_single_key) {
int res = read_key_without_echo(&s->answer);
printf("%s\n", res == EOF ? "" : s->answer.buf);
return res;
@@ -1370,7 +1370,7 @@ static int read_single_character(struct add_p_state *s)
static int prompt_yesno(struct add_p_state *s, const char *prompt)
{
for (;;) {
- color_fprintf(stdout, s->s.cfg.prompt_color, "%s", _(prompt));
+ color_fprintf(stdout, s->cfg.prompt_color, "%s", _(prompt));
fflush(stdout);
if (read_single_character(s) == EOF)
return -1;
@@ -1677,15 +1677,15 @@ static int patch_update_file(struct add_p_state *s,
else
prompt_mode_type = PROMPT_HUNK;
- printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->s.cfg.prompt_color,
+ printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->cfg.prompt_color,
(uintmax_t)hunk_index + 1,
(uintmax_t)(file_diff->hunk_nr
? file_diff->hunk_nr
: 1));
printf(_(s->mode->prompt_mode[prompt_mode_type]),
s->buf.buf);
- if (*s->s.cfg.reset_color_interactive)
- fputs(s->s.cfg.reset_color_interactive, stdout);
+ if (*s->cfg.reset_color_interactive)
+ fputs(s->cfg.reset_color_interactive, stdout);
fflush(stdout);
if (read_single_character(s) == EOF)
break;
@@ -1847,7 +1847,7 @@ static int patch_update_file(struct add_p_state *s,
err(s, _("Sorry, cannot split this hunk"));
} else if (!split_hunk(s, file_diff,
hunk - file_diff->hunk)) {
- color_fprintf_ln(stdout, s->s.cfg.header_color,
+ color_fprintf_ln(stdout, s->cfg.header_color,
_("Split into %d hunks."),
(int)splittable_into);
rendered_hunk_index = -1;
@@ -1865,7 +1865,7 @@ static int patch_update_file(struct add_p_state *s,
} else if (s->answer.buf[0] == '?') {
const char *p = _(help_patch_remainder), *eol = p;
- color_fprintf(stdout, s->s.cfg.help_color, "%s",
+ color_fprintf(stdout, s->cfg.help_color, "%s",
_(s->mode->help_patch_text));
/*
@@ -1883,7 +1883,7 @@ static int patch_update_file(struct add_p_state *s,
if (*p != '?' && !strchr(s->buf.buf, *p))
continue;
- color_fprintf_ln(stdout, s->s.cfg.help_color,
+ color_fprintf_ln(stdout, s->cfg.help_color,
"%.*s", (int)(eol - p), p);
}
} else {
@@ -1903,7 +1903,7 @@ static int patch_update_file(struct add_p_state *s,
strbuf_reset(&s->buf);
reassemble_patch(s, file_diff, 0, &s->buf);
- discard_index(s->s.r->index);
+ discard_index(s->r->index);
if (s->mode->apply_for_checkout)
apply_for_checkout(s, &s->buf,
s->mode->is_reverse);
@@ -1914,8 +1914,8 @@ static int patch_update_file(struct add_p_state *s,
NULL, 0, NULL, 0))
error(_("'git apply' failed"));
}
- if (repo_read_index(s->s.r) >= 0)
- repo_refresh_and_write_index(s->s.r, REFRESH_QUIET, 0,
+ if (repo_read_index(s->r) >= 0)
+ repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,
1, NULL, NULL, NULL);
}
@@ -1928,11 +1928,15 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
const struct pathspec *ps)
{
struct add_p_state s = {
- { r }, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
+ .r = r,
+ .answer = STRBUF_INIT,
+ .buf = STRBUF_INIT,
+ .plain = STRBUF_INIT,
+ .colored = STRBUF_INIT,
};
size_t i, binary_count = 0;
- init_add_i_state(&s.s, r, opts);
+ interactive_config_init(&s.cfg, r, opts);
if (mode == ADD_P_STASH)
s.mode = &patch_mode_stash;
--
2.51.1.851.g4ebd6896fd.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH v5 10/12] add-patch: add support for in-memory index patching
2025-10-21 14:15 ` [PATCH v5 " Patrick Steinhardt
` (8 preceding siblings ...)
2025-10-21 14:15 ` [PATCH v5 09/12] add-patch: remove dependency on "add-interactive" subsystem Patrick Steinhardt
@ 2025-10-21 14:15 ` Patrick Steinhardt
2025-10-21 14:15 ` [PATCH v5 11/12] cache-tree: allow writing in-memory index as tree Patrick Steinhardt
` (2 subsequent siblings)
12 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-21 14:15 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
With `run_add_p()` callers have the ability to apply changes from a
specific revision to a repository's index. This infra supports several
different modes, like for example applying changes to the index,
working tree or both.
One feature that is missing though is the ability to apply changes to an
in-memory index different from the repository's index. Add a new
function `run_add_p_index()` to plug this gap.
This new function will be used in a subsequent commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
add-patch.c | 112 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
add-patch.h | 8 +++++
2 files changed, 116 insertions(+), 4 deletions(-)
diff --git a/add-patch.c b/add-patch.c
index 362726c962d..b8d46d54a2e 100644
--- a/add-patch.c
+++ b/add-patch.c
@@ -4,11 +4,13 @@
#include "git-compat-util.h"
#include "add-patch.h"
#include "advice.h"
+#include "commit.h"
#include "config.h"
#include "diff.h"
#include "editor.h"
#include "environment.h"
#include "gettext.h"
+#include "hex.h"
#include "object-name.h"
#include "pager.h"
#include "read-cache-ll.h"
@@ -47,7 +49,7 @@ static struct patch_mode patch_mode_add = {
N_("Stage mode change [y,n,q,a,d%s,?]? "),
N_("Stage deletion [y,n,q,a,d%s,?]? "),
N_("Stage addition [y,n,q,a,d%s,?]? "),
- N_("Stage this hunk [y,n,q,a,d%s,?]? ")
+ N_("Stage this hunk [y,n,q,a,d%s,?]? "),
},
.edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk "
"will immediately be marked for staging."),
@@ -263,6 +265,8 @@ struct hunk {
struct add_p_state {
struct repository *r;
+ struct index_state *index;
+ const char *index_file;
struct interactive_config cfg;
struct strbuf answer, buf;
@@ -437,7 +441,7 @@ static void setup_child_process(struct add_p_state *s,
cp->git_cmd = 1;
strvec_pushf(&cp->env,
- INDEX_ENVIRONMENT "=%s", s->r->index_file);
+ INDEX_ENVIRONMENT "=%s", s->index_file);
}
static int parse_range(const char **p,
@@ -1903,7 +1907,7 @@ static int patch_update_file(struct add_p_state *s,
strbuf_reset(&s->buf);
reassemble_patch(s, file_diff, 0, &s->buf);
- discard_index(s->r->index);
+ discard_index(s->index);
if (s->mode->apply_for_checkout)
apply_for_checkout(s, &s->buf,
s->mode->is_reverse);
@@ -1914,9 +1918,11 @@ static int patch_update_file(struct add_p_state *s,
NULL, 0, NULL, 0))
error(_("'git apply' failed"));
}
- if (repo_read_index(s->r) >= 0)
+ if (read_index_from(s->index, s->index_file, s->r->gitdir) >= 0 &&
+ s->index == s->r->index) {
repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,
1, NULL, NULL, NULL);
+ }
}
putchar('\n');
@@ -1929,6 +1935,8 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
{
struct add_p_state s = {
.r = r,
+ .index = r->index,
+ .index_file = r->index_file,
.answer = STRBUF_INIT,
.buf = STRBUF_INIT,
.plain = STRBUF_INIT,
@@ -1987,3 +1995,99 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
add_p_state_clear(&s);
return 0;
}
+
+int run_add_p_index(struct repository *r,
+ struct index_state *index,
+ const char *index_file,
+ struct interactive_options *opts,
+ const char *revision,
+ const struct pathspec *ps)
+{
+ struct patch_mode mode = {
+ .apply_args = { "--cached", NULL },
+ .apply_check_args = { "--cached", NULL },
+ .prompt_mode = {
+ N_("Stage mode change [y,n,q,a,d%s,?]? "),
+ N_("Stage deletion [y,n,q,a,d%s,?]? "),
+ N_("Stage addition [y,n,q,a,d%s,?]? "),
+ N_("Stage this hunk [y,n,q,a,d%s,?]? ")
+ },
+ .edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk "
+ "will immediately be marked for staging."),
+ .help_patch_text =
+ N_("y - stage this hunk\n"
+ "n - do not stage this hunk\n"
+ "q - quit; do not stage this hunk or any of the remaining "
+ "ones\n"
+ "a - stage this hunk and all later hunks in the file\n"
+ "d - do not stage this hunk or any of the later hunks in "
+ "the file\n"),
+ .index_only = 1,
+ };
+ struct add_p_state s = {
+ .r = r,
+ .index = index,
+ .index_file = index_file,
+ .answer = STRBUF_INIT,
+ .buf = STRBUF_INIT,
+ .plain = STRBUF_INIT,
+ .colored = STRBUF_INIT,
+ .mode = &mode,
+ .revision = revision,
+ };
+ struct strbuf parent_revision = STRBUF_INIT;
+ char parent_tree_oid[GIT_MAX_HEXSZ + 1];
+ size_t binary_count = 0;
+ struct commit *commit;
+ int ret;
+
+ commit = lookup_commit_reference_by_name(revision);
+ if (!commit) {
+ err(&s, _("Revision does not refer to a commit"));
+ ret = -1;
+ goto out;
+ }
+
+ if (commit->parents)
+ oid_to_hex_r(parent_tree_oid, get_commit_tree_oid(commit->parents->item));
+ else
+ oid_to_hex_r(parent_tree_oid, r->hash_algo->empty_tree);
+
+ strbuf_addf(&parent_revision, "%s~", revision);
+ mode.diff_cmd[0] = "diff-tree";
+ mode.diff_cmd[1] = "-r";
+ mode.diff_cmd[2] = parent_tree_oid;
+
+ interactive_config_init(&s.cfg, r, opts);
+
+ if (parse_diff(&s, ps) < 0) {
+ ret = -1;
+ goto out;
+ }
+
+ for (size_t i = 0; i < s.file_diff_nr; i++) {
+ if (s.file_diff[i].binary && !s.file_diff[i].hunk_nr)
+ binary_count++;
+ else if (patch_update_file(&s, s.file_diff + i))
+ break;
+ }
+
+ if (s.file_diff_nr == 0) {
+ err(&s, _("No changes."));
+ ret = -1;
+ goto out;
+ }
+
+ if (binary_count == s.file_diff_nr) {
+ err(&s, _("Only binary files changed."));
+ ret = -1;
+ goto out;
+ }
+
+ ret = 0;
+
+out:
+ strbuf_release(&parent_revision);
+ add_p_state_clear(&s);
+ return ret;
+}
diff --git a/add-patch.h b/add-patch.h
index a4a05d9d145..901c42fd7b6 100644
--- a/add-patch.h
+++ b/add-patch.h
@@ -3,6 +3,7 @@
#include "color.h"
+struct index_state;
struct pathspec;
struct repository;
@@ -53,4 +54,11 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
struct interactive_options *opts, const char *revision,
const struct pathspec *ps);
+int run_add_p_index(struct repository *r,
+ struct index_state *index,
+ const char *index_file,
+ struct interactive_options *opts,
+ const char *revision,
+ const struct pathspec *ps);
+
#endif
--
2.51.1.851.g4ebd6896fd.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH v5 11/12] cache-tree: allow writing in-memory index as tree
2025-10-21 14:15 ` [PATCH v5 " Patrick Steinhardt
` (9 preceding siblings ...)
2025-10-21 14:15 ` [PATCH v5 10/12] add-patch: add support for in-memory index patching Patrick Steinhardt
@ 2025-10-21 14:15 ` Patrick Steinhardt
2025-10-21 14:16 ` [PATCH v5 12/12] builtin/history: implement "split" subcommand Patrick Steinhardt
2025-10-21 18:53 ` [PATCH v5 00/12] Introduce git-history(1) command for easy history editing Junio C Hamano
12 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-21 14:15 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
The function `write_in_core_index_as_tree()` takes a repository and
writes its index into a tree object. What this function cannot do though
is to take an _arbitrary_ in-memory index.
Introduce a new `struct index_state` parameter so that the caller can
pass a different index than the one belonging to the repository. This
will be used in a subsequent commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
builtin/checkout.c | 3 ++-
cache-tree.c | 5 ++---
cache-tree.h | 3 ++-
3 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/builtin/checkout.c b/builtin/checkout.c
index d230b1f8995..0b90f398feb 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -902,7 +902,8 @@ static int merge_working_tree(const struct checkout_opts *opts,
0);
init_ui_merge_options(&o, the_repository);
o.verbosity = 0;
- work = write_in_core_index_as_tree(the_repository);
+ work = write_in_core_index_as_tree(the_repository,
+ the_repository->index);
ret = reset_tree(new_tree,
opts, 1,
diff --git a/cache-tree.c b/cache-tree.c
index 2aba47060e9..b67d0d703d2 100644
--- a/cache-tree.c
+++ b/cache-tree.c
@@ -699,11 +699,11 @@ static int write_index_as_tree_internal(struct object_id *oid,
return 0;
}
-struct tree* write_in_core_index_as_tree(struct repository *repo) {
+struct tree *write_in_core_index_as_tree(struct repository *repo,
+ struct index_state *index_state) {
struct object_id o;
int was_valid, ret;
- struct index_state *index_state = repo->index;
was_valid = index_state->cache_tree &&
cache_tree_fully_valid(index_state->cache_tree);
@@ -723,7 +723,6 @@ struct tree* write_in_core_index_as_tree(struct repository *repo) {
return lookup_tree(repo, &index_state->cache_tree->oid);
}
-
int write_index_as_tree(struct object_id *oid, struct index_state *index_state, const char *index_path, int flags, const char *prefix)
{
int entries, was_valid;
diff --git a/cache-tree.h b/cache-tree.h
index b82c4963e7c..f8bddae5235 100644
--- a/cache-tree.h
+++ b/cache-tree.h
@@ -47,7 +47,8 @@ int cache_tree_verify(struct repository *, struct index_state *);
#define WRITE_TREE_UNMERGED_INDEX (-2)
#define WRITE_TREE_PREFIX_ERROR (-3)
-struct tree* write_in_core_index_as_tree(struct repository *repo);
+struct tree *write_in_core_index_as_tree(struct repository *repo,
+ struct index_state *index_state);
int write_index_as_tree(struct object_id *oid, struct index_state *index_state, const char *index_path, int flags, const char *prefix);
void prime_cache_tree(struct repository *, struct index_state *, struct tree *);
--
2.51.1.851.g4ebd6896fd.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH v5 12/12] builtin/history: implement "split" subcommand
2025-10-21 14:15 ` [PATCH v5 " Patrick Steinhardt
` (10 preceding siblings ...)
2025-10-21 14:15 ` [PATCH v5 11/12] cache-tree: allow writing in-memory index as tree Patrick Steinhardt
@ 2025-10-21 14:16 ` Patrick Steinhardt
2025-10-21 18:53 ` [PATCH v5 00/12] Introduce git-history(1) command for easy history editing Junio C Hamano
12 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-21 14:16 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
It is quite a common use case that one wants to split up one commit into
multiple commits by moving parts of the changes of the original commit
out into a separate commit. This is quite an involved operation though:
1. Identify the commit in question that is to be dropped.
2. Perform an interactive rebase on top of that commit's parent.
3. Modify the instruction sheet to "edit" the commit that is to be
split up.
4. Drop the commit via "git reset HEAD~".
5. Stage changes that should go into the first commit and commit it.
6. Stage changes that should go into the second commit and commit it.
7. Finalize the rebase.
This is quite complex, and overall I would claim that most people who
are not experts in Git would struggle with this flow.
Introduce a new "split" subcommand for git-history(1) to make this way
easier. All the user needs to do is to say `git history split $COMMIT`.
From hereon, Git asks the user which parts of the commit shall be moved
out into a separate commit and, once done, asks the user for the commit
message. Git then creates that split-out commit and applies the original
commit on top of it.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-history.adoc | 62 ++++++
builtin/history.c | 222 ++++++++++++++++++++
t/meson.build | 1 +
t/t3452-history-split.sh | 447 +++++++++++++++++++++++++++++++++++++++++
4 files changed, 732 insertions(+)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index 6f016801936..d741caf8347 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -10,6 +10,7 @@ SYNOPSIS
[synopsis]
git history [<options>]
git history reword [<options>] <commit>
+git history split [<options>] <commit> [--] [<pathspec>...]
DESCRIPTION
-----------
@@ -40,6 +41,26 @@ rewrite history in different ways:
provided, then this command will spawn an editor with the current
message of that commit.
+`split [--message=<message>] <commit> [--] [<pathspec>...]`::
+ Interactively split up <commit> into two commits by choosing
+ hunks introduced by it that will be moved into the new split-out
+ commit. These hunks will then be written into a new commit that
+ becomes the parent of the previous commit. The original commit
+ stays intact, except that its parent will be the newly split-out
+ commit.
++
+The commit message of the new commit will be asked for by launching the
+configured editor, unless it has been specified with the `-m` option.
+Authorship of the commit will be the same as for the original commit.
++
+If passed, _<pathspec>_ can be used to limit which changes shall be split out
+of the original commit. Files not matching any of the pathspecs will remain
+part of the original commit. For more details, see the 'pathspec' entry in
+linkgit:gitglossary[7].
++
+It is invalid to select either all or no hunks, as that would lead to
+one of the commits becoming empty.
+
CONFIGURATION
-------------
@@ -47,6 +68,47 @@ include::includes/cmd-config-section-all.adoc[]
include::config/sequencer.adoc[]
+EXAMPLES
+--------
+
+Split a commit
+~~~~~~~~~~~~~~
+
+----------
+$ git log --stat --oneline
+3f81232 (HEAD -> main) original
+ bar | 1 +
+ foo | 1 +
+ 2 files changed, 2 insertions(+)
+
+$ git history split HEAD --message="split-out commit"
+diff --git a/bar b/bar
+new file mode 100644
+index 0000000..5716ca5
+--- /dev/null
++++ b/bar
+@@ -0,0 +1 @@
++bar
+(1/1) Stage addition [y,n,q,a,d,e,p,?]? y
+
+diff --git a/foo b/foo
+new file mode 100644
+index 0000000..257cc56
+--- /dev/null
++++ b/foo
+@@ -0,0 +1 @@
++foo
+(1/1) Stage addition [y,n,q,a,d,e,p,?]? n
+
+$ git log --stat --oneline
+7cebe64 (HEAD -> main) original
+ foo | 1 +
+ 1 file changed, 1 insertion(+)
+d1582f3 split-out commit
+ bar | 1 +
+ 1 file changed, 1 insertion(+)
+----------
+
GIT
---
Part of the linkgit:git[1] suite
diff --git a/builtin/history.c b/builtin/history.c
index f14e88c9bf4..6ca4382c8e2 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -1,6 +1,7 @@
#define USE_THE_REPOSITORY_VARIABLE
#include "builtin.h"
+#include "cache-tree.h"
#include "commit-reach.h"
#include "commit.h"
#include "config.h"
@@ -8,17 +9,22 @@
#include "environment.h"
#include "gettext.h"
#include "hex.h"
+#include "oidmap.h"
#include "parse-options.h"
+#include "path.h"
+#include "read-cache.h"
#include "refs.h"
#include "replay.h"
#include "reset.h"
#include "revision.h"
+#include "run-command.h"
#include "sequencer.h"
#include "strvec.h"
#include "tree.h"
#include "wt-status.h"
#define GIT_HISTORY_REWORD_USAGE N_("git history reword [<options>] <commit>")
+#define GIT_HISTORY_SPLIT_USAGE N_("git history split [<options>] <commit> [--] [<pathspec>...]")
static int collect_commits(struct repository *repo,
struct commit *old_commit,
@@ -357,6 +363,220 @@ static int cmd_history_reword(int argc,
return ret;
}
+static int split_commit(struct repository *repo,
+ struct commit *original_commit,
+ struct pathspec *pathspec,
+ const char *commit_message,
+ struct object_id *out)
+{
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
+ struct strbuf index_file = STRBUF_INIT, split_message = STRBUF_INIT;
+ struct child_process read_tree_cmd = CHILD_PROCESS_INIT;
+ struct index_state index = INDEX_STATE_INIT(repo);
+ struct object_id original_commit_tree_oid, parent_tree_oid;
+ const char *original_message, *original_body, *ptr;
+ char original_commit_oid[GIT_MAX_HEXSZ + 1];
+ char *original_author = NULL;
+ struct commit_list *parents = NULL;
+ struct commit *first_commit;
+ struct tree *split_tree;
+ size_t len;
+ int ret;
+
+ if (original_commit->parents)
+ parent_tree_oid = *get_commit_tree_oid(original_commit->parents->item);
+ else
+ oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
+ original_commit_tree_oid = *get_commit_tree_oid(original_commit);
+
+ /*
+ * Construct the first commit. This is done by taking the original
+ * commit parent's tree and selectively patching changes from the diff
+ * between that parent and its child.
+ */
+ repo_git_path_replace(repo, &index_file, "%s", "history-split.index");
+
+ read_tree_cmd.git_cmd = 1;
+ strvec_pushf(&read_tree_cmd.env, "GIT_INDEX_FILE=%s", index_file.buf);
+ strvec_push(&read_tree_cmd.args, "read-tree");
+ strvec_push(&read_tree_cmd.args, oid_to_hex(&parent_tree_oid));
+ ret = run_command(&read_tree_cmd);
+ if (ret < 0)
+ goto out;
+
+ ret = read_index_from(&index, index_file.buf, repo->gitdir);
+ if (ret < 0) {
+ ret = error(_("failed reading temporary index"));
+ goto out;
+ }
+
+ oid_to_hex_r(original_commit_oid, &original_commit->object.oid);
+ ret = run_add_p_index(repo, &index, index_file.buf, &interactive_opts,
+ original_commit_oid, pathspec);
+ if (ret < 0)
+ goto out;
+
+ split_tree = write_in_core_index_as_tree(repo, &index);
+ if (!split_tree) {
+ ret = error(_("failed split tree"));
+ goto out;
+ }
+
+ unlink(index_file.buf);
+
+ /*
+ * We disallow the cases where either the split-out commit or the
+ * original commit would become empty. Consequently, if we see that the
+ * new tree ID matches either of those trees we abort.
+ */
+ if (oideq(&split_tree->object.oid, &parent_tree_oid)) {
+ ret = error(_("split commit is empty"));
+ goto out;
+ } else if (oideq(&split_tree->object.oid, &original_commit_tree_oid)) {
+ ret = error(_("split commit tree matches original commit"));
+ goto out;
+ }
+
+ /* We retain authorship of the original commit. */
+ original_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);
+ ptr = find_commit_header(original_message, "author", &len);
+ if (ptr)
+ original_author = xmemdupz(ptr, len);
+
+ ret = fill_commit_message(repo, &parent_tree_oid, &split_tree->object.oid,
+ "", commit_message, "split-out", &split_message);
+ if (ret < 0)
+ goto out;
+
+ ret = commit_tree(split_message.buf, split_message.len, &split_tree->object.oid,
+ original_commit->parents, &out[0], original_author, NULL);
+ if (ret < 0) {
+ ret = error(_("failed writing split-out commit"));
+ goto out;
+ }
+
+ /*
+ * The second commit is much simpler to construct, as we can simply use
+ * the original commit details, except that we adjust its parent to be
+ * the newly split-out commit.
+ */
+ find_commit_subject(original_message, &original_body);
+ first_commit = lookup_commit_reference(repo, &out[0]);
+ commit_list_append(first_commit, &parents);
+
+ ret = commit_tree(original_body, strlen(original_body), &original_commit_tree_oid,
+ parents, &out[1], original_author, NULL);
+ if (ret < 0) {
+ ret = error(_("failed writing second commit"));
+ goto out;
+ }
+
+ ret = 0;
+
+out:
+ if (index_file.len)
+ unlink(index_file.buf);
+ strbuf_release(&split_message);
+ strbuf_release(&index_file);
+ free_commit_list(parents);
+ free(original_author);
+ release_index(&index);
+ return ret;
+}
+
+static int cmd_history_split(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo)
+{
+ const char * const usage[] = {
+ GIT_HISTORY_SPLIT_USAGE,
+ NULL,
+ };
+ const char *commit_message = NULL;
+ struct option options[] = {
+ OPT_STRING('m', "message", &commit_message, N_("message"), N_("commit message")),
+ OPT_END(),
+ };
+ struct oidmap rewritten_commits = OIDMAP_INIT;
+ struct commit *original_commit, *parent, *head;
+ struct strvec commits = STRVEC_INIT;
+ struct commit_list *from_list = NULL;
+ struct object_id split_commits[2];
+ struct pathspec pathspec = { 0 };
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc < 1) {
+ ret = error(_("command expects a revision"));
+ goto out;
+ }
+ repo_config(repo, git_default_config, NULL);
+
+ original_commit = lookup_commit_reference_by_name(argv[0]);
+ if (!original_commit) {
+ ret = error(_("commit to be split cannot be found: %s"), argv[0]);
+ goto out;
+ }
+
+ parent = original_commit->parents ? original_commit->parents->item : NULL;
+ if (parent && repo_parse_commit(repo, parent)) {
+ ret = error(_("unable to parse commit %s"),
+ oid_to_hex(&parent->object.oid));
+ goto out;
+ }
+
+ head = lookup_commit_reference_by_name("HEAD");
+ if (!head) {
+ ret = error(_("could not resolve HEAD to a commit"));
+ goto out;
+ }
+
+ commit_list_append(original_commit, &from_list);
+ if (!repo_is_descendant_of(repo, head, from_list)) {
+ ret = error(_("split commit must be reachable from current HEAD commit"));
+ goto out;
+ }
+
+ parse_pathspec(&pathspec, 0,
+ PATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,
+ prefix, argv + 1);
+
+ /*
+ * Collect the list of commits that we'll have to reapply now already.
+ * This ensures that we'll abort early on in case the range of commits
+ * contains merges, which we do not yet handle.
+ */
+ ret = collect_commits(repo, parent, head, &commits);
+ if (ret < 0)
+ goto out;
+
+ /*
+ * Then we split up the commit and replace the original commit with the
+ * new ones.
+ */
+ ret = split_commit(repo, original_commit, &pathspec,
+ commit_message, split_commits);
+ if (ret < 0)
+ goto out;
+
+ replace_commits(&commits, &original_commit->object.oid,
+ split_commits, ARRAY_SIZE(split_commits));
+
+ ret = apply_commits(repo, &commits, parent, head, "split");
+ if (ret < 0)
+ goto out;
+
+ ret = 0;
+
+out:
+ oidmap_clear(&rewritten_commits, 0);
+ free_commit_list(from_list);
+ clear_pathspec(&pathspec);
+ strvec_clear(&commits);
+ return ret;
+}
+
int cmd_history(int argc,
const char **argv,
const char *prefix,
@@ -365,11 +585,13 @@ int cmd_history(int argc,
const char * const usage[] = {
N_("git history [<options>]"),
GIT_HISTORY_REWORD_USAGE,
+ GIT_HISTORY_SPLIT_USAGE,
NULL,
};
parse_opt_subcommand_fn *fn = NULL;
struct option options[] = {
OPT_SUBCOMMAND("reword", &fn, cmd_history_reword),
+ OPT_SUBCOMMAND("split", &fn, cmd_history_split),
OPT_END(),
};
diff --git a/t/meson.build b/t/meson.build
index a3ec9199947..5d3014a768f 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -386,6 +386,7 @@ integration_tests = [
't3438-rebase-broken-files.sh',
't3450-history.sh',
't3451-history-reword.sh',
+ 't3452-history-split.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
diff --git a/t/t3452-history-split.sh b/t/t3452-history-split.sh
new file mode 100755
index 00000000000..d9874c61f62
--- /dev/null
+++ b/t/t3452-history-split.sh
@@ -0,0 +1,447 @@
+#!/bin/sh
+
+test_description='tests for git-history split subcommand'
+
+. ./test-lib.sh
+
+set_fake_editor () {
+ write_script fake-editor.sh <<-\EOF &&
+ echo "split-out commit" >"$1"
+ EOF
+ test_set_editor "$(pwd)"/fake-editor.sh
+}
+
+expect_log () {
+ git log --format="%s" >actual &&
+ cat >expect &&
+ test_cmp expect actual
+}
+
+expect_tree_entries () {
+ git ls-tree --name-only "$1" >actual &&
+ cat >expect &&
+ test_cmp expect actual
+}
+
+test_expect_success 'refuses to work with merge commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ git branch branch &&
+ test_commit ours &&
+ git switch branch &&
+ test_commit theirs &&
+ git switch - &&
+ git merge theirs &&
+ test_must_fail git history split HEAD 2>err &&
+ test_grep "cannot rearrange commit history with merges" err &&
+ test_must_fail git history split HEAD~ 2>err &&
+ test_grep "cannot rearrange commit history with merges" err
+ )
+'
+
+test_expect_success 'refuses to work with unrelated commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ git branch branch &&
+ test_commit ours &&
+ git switch branch &&
+ test_commit theirs &&
+ test_must_fail git history split ours 2>err &&
+ test_grep "split commit must be reachable from current HEAD commit" err
+ )
+'
+
+test_expect_success 'can split up tip commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit initial &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ git symbolic-ref HEAD >expect &&
+ set_fake_editor &&
+ git history split HEAD <<-EOF &&
+ y
+ n
+ EOF
+ git symbolic-ref HEAD >actual &&
+ test_cmp expect actual &&
+
+ expect_log <<-EOF &&
+ split-me
+ split-out commit
+ initial
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ bar
+ initial.t
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ initial.t
+ EOF
+ )
+'
+
+test_expect_success 'can split up root commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m root &&
+ test_commit tip &&
+
+ set_fake_editor &&
+ git history split HEAD~ <<-EOF &&
+ y
+ n
+ EOF
+
+ expect_log <<-EOF &&
+ tip
+ root
+ split-out commit
+ EOF
+
+ expect_tree_entries HEAD~2 <<-EOF &&
+ bar
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ bar
+ foo
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ tip.t
+ EOF
+ )
+'
+
+test_expect_success 'can split up in-between commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit initial &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+ test_commit tip &&
+
+ set_fake_editor &&
+ git history split HEAD~ <<-EOF &&
+ y
+ n
+ EOF
+
+ expect_log <<-EOF &&
+ tip
+ split-me
+ split-out commit
+ initial
+ EOF
+
+ expect_tree_entries HEAD~2 <<-EOF &&
+ bar
+ initial.t
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ bar
+ foo
+ initial.t
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ initial.t
+ tip.t
+ EOF
+ )
+'
+
+test_expect_success 'can pick multiple hunks' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar baz foo qux &&
+ git add . &&
+ git commit -m split-me &&
+
+ git history split HEAD -m "split-out commit" <<-EOF &&
+ y
+ n
+ y
+ n
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ bar
+ foo
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ baz
+ foo
+ qux
+ EOF
+ )
+'
+
+
+test_expect_success 'can use only last hunk' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ git history split HEAD -m "split-out commit" <<-EOF &&
+ n
+ y
+ EOF
+
+ expect_log <<-EOF &&
+ split-me
+ split-out commit
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ foo
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ EOF
+ )
+'
+
+test_expect_success 'aborts with empty commit message' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ test_must_fail git history split HEAD -m "" <<-EOF 2>err &&
+ y
+ n
+ EOF
+ test_grep "Aborting commit due to empty commit message." err
+ )
+'
+
+test_expect_success 'can specify message via option' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ git history split HEAD -m "message option" <<-EOF &&
+ y
+ n
+ EOF
+
+ expect_log <<-EOF
+ split-me
+ message option
+ EOF
+ )
+'
+
+test_expect_success 'commit message editor sees split-out changes' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ write_script fake-editor.sh <<-\EOF &&
+ cp "$1" . &&
+ echo "some commit message" >>"$1"
+ EOF
+ test_set_editor "$(pwd)"/fake-editor.sh &&
+
+ git history split HEAD <<-EOF &&
+ y
+ n
+ EOF
+
+ cat >expect <<-EOF &&
+
+ # Please enter the commit message for the split-out changes. Lines starting
+ # with ${SQ}#${SQ} will be ignored.
+ # Changes to be committed:
+ # new file: bar
+ #
+ EOF
+ test_cmp expect COMMIT_EDITMSG &&
+
+ expect_log <<-EOF
+ split-me
+ some commit message
+ EOF
+ )
+'
+
+test_expect_success 'can use pathspec to limit what gets split' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ git history split HEAD -m "message option" -- foo <<-EOF &&
+ y
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ foo
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ EOF
+ )
+'
+
+test_expect_success 'refuses to create empty split-out commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ test_must_fail git history split HEAD 2>err <<-EOF &&
+ n
+ n
+ EOF
+ test_grep "split commit is empty" err
+ )
+'
+
+test_expect_success 'hooks are executed for rewritten commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+ old_head=$(git rev-parse HEAD) &&
+
+ write_script .git/hooks/prepare-commit-msg <<-EOF &&
+ touch "$(pwd)/hooks.log"
+ EOF
+ write_script .git/hooks/post-commit <<-EOF &&
+ touch "$(pwd)/hooks.log"
+ EOF
+ write_script .git/hooks/post-rewrite <<-EOF &&
+ touch "$(pwd)/hooks.log"
+ EOF
+
+ set_fake_editor &&
+ git history split HEAD <<-EOF &&
+ y
+ n
+ EOF
+
+ expect_log <<-EOF &&
+ split-me
+ split-out commit
+ EOF
+
+ test_path_is_missing hooks.log
+ )
+'
+
+test_expect_success 'refuses to create empty original commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ test_must_fail git history split HEAD 2>err <<-EOF &&
+ y
+ y
+ EOF
+ test_grep "split commit tree matches original commit" err
+ )
+'
+
+test_expect_success 'retains changes in the worktree and index' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ echo a >a &&
+ echo b >b &&
+ git add . &&
+ git commit -m "initial commit" &&
+ echo a-modified >a &&
+ echo b-modified >b &&
+ git add b &&
+ git history split HEAD -m a-only <<-EOF &&
+ y
+ n
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ a
+ EOF
+ expect_tree_entries HEAD <<-EOF &&
+ a
+ b
+ EOF
+
+ cat >expect <<-\EOF &&
+ M a
+ M b
+ ?? actual
+ ?? expect
+ EOF
+ git status --porcelain >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_done
--
2.51.1.851.g4ebd6896fd.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH v5 00/12] Introduce git-history(1) command for easy history editing
2025-10-21 14:15 ` [PATCH v5 " Patrick Steinhardt
` (11 preceding siblings ...)
2025-10-21 14:16 ` [PATCH v5 12/12] builtin/history: implement "split" subcommand Patrick Steinhardt
@ 2025-10-21 18:53 ` Junio C Hamano
12 siblings, 0 replies; 278+ messages in thread
From: Junio C Hamano @ 2025-10-21 18:53 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, D. Ben Knoble, Sergey Organov, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk, Elijah Newren,
Karthik Nayak
Patrick Steinhardt <ps@pks.im> writes:
> A copule of these features relate to history editing. Most importantly,
> I really dig the following commands:
>
> - jj-abandon(1) to drop a specific commit from your history.
>
> - jj-absorb(1) to take some changes and automatically apply them to
> commits in your history that last modified the respective hunks.
>
> - jj-split(1) to split a commit into two.
>
> - jj-new(1) to insert a new commit after or before a specific other
> commit.
>
> Not all of these commands can be ported directly into Git. jj-new(1) for
> example doesn't really make a ton of sense for us, I'd claim. But some
> of these commands _do_ make sense.
>
> This patch series is a starting point for such a command. I've
> significantly slimmed it down from the first couple revisions now
> following the discussions at the Contributor's Summit yesterday. This
> was my intent anyway, as I already mentioned on the last iteration.
Will replace. The other topic this depends on seems to be almost
ready, hopefully, and the part this series depends on should not
change much, even though what is remaining may be a bit more than
finding where to squash the tip "SQUASH" fixup in.
Thanks.
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH v6 00/11] Introduce git-history(1) command for easy history editing
2025-08-19 10:55 [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing Patrick Steinhardt
` (18 preceding siblings ...)
2025-10-21 14:15 ` [PATCH v5 " Patrick Steinhardt
@ 2025-10-27 11:33 ` Patrick Steinhardt
2025-10-27 11:33 ` [PATCH v6 01/11] wt-status: provide function to expose status for trees Patrick Steinhardt
` (12 more replies)
2025-12-03 10:48 ` [PATCH v7 00/12] " Patrick Steinhardt
20 siblings, 13 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-27 11:33 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
Hi,
over recent months I've been playing around with Jujutsu quite
frequently. While I still prefer using Git, there's been a couple
features in it that I really like and that I'd like to have in Git, as
well.
A copule of these features relate to history editing. Most importantly,
I really dig the following commands:
- jj-abandon(1) to drop a specific commit from your history.
- jj-absorb(1) to take some changes and automatically apply them to
commits in your history that last modified the respective hunks.
- jj-split(1) to split a commit into two.
- jj-new(1) to insert a new commit after or before a specific other
commit.
Not all of these commands can be ported directly into Git. jj-new(1) for
example doesn't really make a ton of sense for us, I'd claim. But some
of these commands _do_ make sense.
This patch series is a starting point for such a command. I've
significantly slimmed it down from the first couple revisions now
following the discussions at the Contributor's Summit yesterday. This
was my intent anyway, as I already mentioned on the last iteration.
Changes in v6:
- I've rebased the patch series again to pull in the latest updates
from sa/replay-atomic-ref-updates and fix conflicts. It is now based
on 4e98b730f1 (The twenty-fourth batch, 2025-10-24) with ab661bb1bb
(replay: add replay.refAction config option, 2025-10-23) merged into
it.
- I've dropped the "-m" options for now, so commit messages are always
asked for via the editor. These can be introduced in a subsequent
patch series once discussion around them has settled.
- We don't use the merge machinery anymore to pick the commits.
- Drop the commit to parse commits in the replay machinery. It didn't
seem to be necessary in v5 anymore, and now that we don't use the
merge machinery at all we don't ever take that code path in the
first place.
- Link to v5:
https://lore.kernel.org/r/20251021-b4-pks-history-builtin-v5-0-78d23f578fe6@pks.im
Changes in v5:
- I've changed the patch series to be based on top of 133d151831 (The
twenty-first batch, 2025-10-20) with sa/replay-atomic-ref-updates at
a1c22e627e (SQAUASH??? t0450 band-aid, 2025-10-14) merged into it.
This is one the one hand to fix a conflict, but also to get some of
the CI updates to make GitLab CI work again.
- Some slight commit message improvements.
- Deduplicate subcommand usage strings by using defines.
- Fix the desendancy checks to properly verify that HEAD is a
descendant of the commit to be rewritten. Also add some tests for
this.
- Fix the hint that mentions that lines starting with the comment
character will be tripped after having written the commit message.
- Move an include to the correct commit.
- Link to v4: https://lore.kernel.org/r/20251001-b4-pks-history-builtin-v4-0-8e61ddb86317@pks.im
Changes in v4:
- I've rebuilt the patch series. It is now based on 821f583da6 (The
thirteenth batcn, 2025-09-29) with sa/replay-atomic-ref-updates
at 665c66a743 (replay: make atomic ref updates the default behavior,
2025-09-27) merged into it. This should fix all conflicts with seen.
- I've reworked this patch series to use the same infra as
git-replay(1), as discussed during the Contributor's Summit.
- I've slimmed down the patch series to only tackle those commands
that cannot result in a conflict to keep it simple. I also learned
that Elijah has been working on a "git replay edit" command, so I
dropped that command so that we can instead use his version.
- During the Contributor's Summit we have agreed that for now, we
won't care about hook execution just yet. This may be backfilled at
a later point in time.
- I dropped "commit.verbose" handling for now, as my understanding of
it was wrong at first. This is something we should backfill.
- Link to v3: https://lore.kernel.org/r/20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im
Changes in v3:
- Add logic to drive the "post-rewrite" hook and add tests to verify
that all hooks are executed as expected.
- Deduplicate logic to turn a replay action into a todo command.
- Move the addition of tests for the top-level git-history(1) command
to the correct commit.
- Some smaller commit message fixes.
- Honor "commit.verbose".
- Fix copy-paste error with an error message.
- Link to v2: https://lore.kernel.org/r/20250824-b4-pks-history-builtin-v2-0-964ac12f65bd@pks.im
Changes in v2:
- Add a new "reword" subcommand.
- List git-history(1) in "command-list.txt".
- Add some missing error handling.
- Simplify calling convention of `apply_commits()` to handle root
commits internally instead of requiring every caller to do so.
- Add tests to verify that git-history(1) refuses to work with changes
in the worktree or index.
- Mark git-history(1) as experimental.
- Introduce commands to manage interrupted history edits.
- A bunch of improvements to the manpage.
- Link to v1: https://lore.kernel.org/r/20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im
Thanks!
Patrick
---
Patrick Steinhardt (11):
wt-status: provide function to expose status for trees
replay: extract logic to pick commits
replay: stop using `the_repository`
builtin: add new "history" command
builtin/history: implement "reword" subcommand
add-patch: split out header from "add-interactive.h"
add-patch: split out `struct interactive_options`
add-patch: remove dependency on "add-interactive" subsystem
add-patch: add support for in-memory index patching
cache-tree: allow writing in-memory index as tree
builtin/history: implement "split" subcommand
.gitignore | 1 +
Documentation/git-history.adoc | 111 ++++++++
Documentation/meson.build | 1 +
Makefile | 2 +
add-interactive.c | 174 +++----------
add-interactive.h | 46 +---
add-patch.c | 297 +++++++++++++++++++---
add-patch.h | 64 +++++
builtin.h | 1 +
builtin/add.c | 22 +-
builtin/checkout.c | 7 +-
builtin/commit.c | 16 +-
builtin/history.c | 561 +++++++++++++++++++++++++++++++++++++++++
builtin/replay.c | 110 +-------
builtin/reset.c | 16 +-
builtin/stash.c | 46 ++--
cache-tree.c | 5 +-
cache-tree.h | 3 +-
command-list.txt | 1 +
commit.h | 2 +-
git.c | 1 +
meson.build | 2 +
replay.c | 115 +++++++++
replay.h | 23 ++
t/meson.build | 3 +
t/t3450-history.sh | 17 ++
t/t3451-history-reword.sh | 237 +++++++++++++++++
t/t3452-history-split.sh | 432 +++++++++++++++++++++++++++++++
wt-status.c | 24 ++
wt-status.h | 9 +
30 files changed, 1972 insertions(+), 377 deletions(-)
Range-diff versus v5:
1: c351cd9fb34 ! 1: 7e32877a47c wt-status: provide function to expose status for trees
@@ wt-status.h: void wt_status_add_cut_line(struct wt_status *s);
void wt_status_prepare(struct repository *r, struct wt_status *s);
void wt_status_print(struct wt_status *s);
void wt_status_collect(struct wt_status *s);
++
++/*
++ * Collect all changes between the two trees. Changes will be displayed as if
++ * they were staged into the index.
++ */
+void wt_status_collect_changes_trees(struct wt_status *s,
+ const struct object_id *old_treeish,
+ const struct object_id *new_treeish);
++
/*
* Frees the buffers allocated by wt_status_collect.
*/
2: 71266f8ed78 ! 2: 3688a10ec38 replay: extract logic to pick commits
@@ builtin/replay.c
#include "revision.h"
#include "strmap.h"
#include <oidset.h>
- #include <tree.h>
+@@ builtin/replay.c: enum ref_action_mode {
+ REF_ACTION_PRINT
+ };
-static const char *short_commit_name(struct repository *repo,
- struct commit *commit)
@@ builtin/replay.c: static void determine_replay_mode(struct repository *repo,
- return create_commit(repo, result->tree, pickme, replayed_base);
-}
-
- static int handle_ref_update(const char *mode,
+ static int handle_ref_update(enum ref_action_mode mode,
struct ref_transaction *transaction,
const char *refname,
@@ builtin/replay.c: int cmd_replay(int argc,
@@ replay.c (new)
+ return NULL;
+}
+
-+static struct commit *create_commit(struct repository *repo,
++struct commit *replay_create_commit(struct repository *repo,
+ struct tree *tree,
+ struct commit *based_on,
+ struct commit *parent)
@@ replay.c (new)
+ merge_opt->ancestor = NULL;
+ if (!result->clean)
+ return NULL;
-+ return create_commit(repo, result->tree, pickme, replayed_base);
++ return replay_create_commit(repo, result->tree, pickme, replayed_base);
+}
## replay.h (new) ##
@@ replay.h (new)
+struct commit;
+struct tree;
+
++struct commit *replay_create_commit(struct repository *repo,
++ struct tree *tree,
++ struct commit *based_on,
++ struct commit *parent);
++
+struct commit *replay_pick_regular_commit(struct repository *repo,
+ struct commit *pickme,
+ kh_oid_map_t *replayed_commits,
3: 703fc59d4e1 ! 3: 7a5217797a0 replay: stop using `the_repository`
@@ Commit message
Signed-off-by: Patrick Steinhardt <ps@pks.im>
## replay.c ##
-@@ replay.c: static struct commit *create_commit(struct repository *repo,
+@@ replay.c: struct commit *replay_create_commit(struct repository *repo,
obj = parse_object(repo, &ret);
out:
4: f18a8ac67e0 < -: ----------- replay: parse commits before dereferencing them
5: 65892f7da77 ! 4: bc435f62d87 builtin: add new "history" command
@@ Documentation/git-history.adoc (new)
+Rewrite history by rearranging or modifying specific commits in the
+history.
+
++THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
++
+This command is similar to linkgit:git-rebase[1] and uses the same
+underlying machinery. You should use rebases if you want to reapply a range of
+commits onto a different base, or interactive rebases if you want to edit a
@@ Documentation/git-history.adoc (new)
+merges. You should use linkgit:git-rebase[1] with the `--rebase-merges`
+flag instead.
+
-+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
-+
+COMMANDS
+--------
+
-+This command requires a subcommand. Several subcommands are available to
-+rewrite history in different ways:
++Several commands are available to rewrite history in different ways:
+
+CONFIGURATION
+-------------
6: cefe1dbb847 ! 5: 39a21dc2bbd builtin/history: implement "reword" subcommand
@@ Commit message
Signed-off-by: Patrick Steinhardt <ps@pks.im>
## Documentation/git-history.adoc ##
-@@ Documentation/git-history.adoc: SYNOPSIS
+@@ Documentation/git-history.adoc: git-history - EXPERIMENTAL: Rewrite history of the current branch
+ SYNOPSIS
--------
[synopsis]
- git history [<options>]
-+git history reword [<options>] <commit>
+-git history [<options>]
++git history reword <commit>
DESCRIPTION
-----------
@@ Documentation/git-history.adoc: COMMANDS
- This command requires a subcommand. Several subcommands are available to
- rewrite history in different ways:
-+`reword <commit> [--message=<message>]`::
+ Several commands are available to rewrite history in different ways:
+
++`reword <commit>`::
+ Rewrite the commit message of the specified commit. All the other
-+ details of this commit remain unchanged. If no commit message is
-+ provided, then this command will spawn an editor with the current
-+ message of that commit.
++ details of this commit remain unchanged. This command will spawn an
++ editor with the current message of that commit.
+
CONFIGURATION
-------------
@@ builtin/history.c
+#include "tree.h"
+#include "wt-status.h"
+
-+#define GIT_HISTORY_REWORD_USAGE N_("git history reword [<options>] <commit>")
++#define GIT_HISTORY_REWORD_USAGE N_("git history reword <commit>")
+
+static int collect_commits(struct repository *repo,
+ struct commit *old_commit,
@@ builtin/history.c
+ const char *action)
+{
+ struct reset_head_opts reset_opts = { 0 };
-+ struct merge_options merge_opts = { 0 };
-+ struct merge_result result = { 0 };
+ struct strbuf buf = STRBUF_INIT;
-+ kh_oid_map_t *replayed_commits;
+ int ret;
+
-+ replayed_commits = kh_init_oid_map();
-+
-+ init_basic_merge_options(&merge_opts, repo);
-+ merge_opts.show_rename_progress = 0;
-+
+ for (size_t i = 0; i < commits->nr; i++) {
+ struct object_id commit_id;
+ struct commit *commit;
+ const char *end;
-+ int hash_result;
-+ khint_t pos;
+
+ if (parse_oid_hex_algop(commits->v[i], &commit_id, &end,
+ repo->hash_algo)) {
@@ builtin/history.c
+
+ if (!onto) {
+ onto = commit;
-+ result.clean = 1;
-+ result.tree = repo_get_commit_tree(repo, commit);
+ } else {
-+ onto = replay_pick_regular_commit(repo, commit, replayed_commits,
-+ onto, &merge_opts, &result);
++ struct tree *tree = repo_get_commit_tree(repo, commit);
++ onto = replay_create_commit(repo, tree, commit, onto);
+ if (!onto)
+ break;
+ }
-+
-+ pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hash_result);
-+ if (hash_result == 0) {
-+ ret = error(_("duplicate rewritten commit: %s\n"),
-+ oid_to_hex(&commit->object.oid));
-+ goto out;
-+ }
-+ kh_value(replayed_commits, pos) = onto;
-+ }
-+
-+ if (!result.clean) {
-+ ret = error(_("could not merge"));
-+ goto out;
+ }
+
+ reset_opts.oid = &onto->object.oid;
@@ builtin/history.c
+ ret = 0;
+
+out:
-+ kh_destroy_oid_map(replayed_commits);
-+ merge_finalize(&merge_opts, &result);
+ strbuf_release(&buf);
+ return ret;
+}
@@ builtin/history.c
+ const struct object_id *old_tree,
+ const struct object_id *new_tree,
+ const char *default_message,
-+ const char *provided_message,
+ const char *action,
+ struct strbuf *out)
+{
-+ if (!provided_message) {
-+ const char *path = git_path_commit_editmsg();
-+ const char *hint =
-+ _("Please enter the commit message for the %s changes."
-+ " Lines starting\nwith '%s' will be ignored.\n");
-+ struct wt_status s;
-+
-+ strbuf_addstr(out, default_message);
-+ strbuf_addch(out, '\n');
-+ strbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);
-+ write_file_buf(path, out->buf, out->len);
-+
-+ wt_status_prepare(repo, &s);
-+ FREE_AND_NULL(s.branch);
-+ s.ahead_behind_flags = AHEAD_BEHIND_QUICK;
-+ s.commit_template = 1;
-+ s.colopts = 0;
-+ s.display_comment_prefix = 1;
-+ s.hints = 0;
-+ s.use_color = 0;
-+ s.whence = FROM_COMMIT;
-+ s.committable = 1;
-+
-+ s.fp = fopen(git_path_commit_editmsg(), "a");
-+ if (!s.fp)
-+ return error_errno(_("could not open '%s'"), git_path_commit_editmsg());
-+
-+ wt_status_collect_changes_trees(&s, old_tree, new_tree);
-+ wt_status_print(&s);
-+ wt_status_collect_free_buffers(&s);
-+ string_list_clear_func(&s.change, change_data_free);
-+
-+ strbuf_reset(out);
-+ if (launch_editor(path, out, NULL)) {
-+ fprintf(stderr, _("Please supply the message using the -m option.\n"));
-+ return -1;
-+ }
-+ strbuf_stripspace(out, comment_line_str);
-+ } else {
-+ strbuf_addstr(out, provided_message);
++ const char *path = git_path_commit_editmsg();
++ const char *hint =
++ _("Please enter the commit message for the %s changes."
++ " Lines starting\nwith '%s' will be ignored.\n");
++ struct wt_status s;
++
++ strbuf_addstr(out, default_message);
++ strbuf_addch(out, '\n');
++ strbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);
++ write_file_buf(path, out->buf, out->len);
++
++ wt_status_prepare(repo, &s);
++ FREE_AND_NULL(s.branch);
++ s.ahead_behind_flags = AHEAD_BEHIND_QUICK;
++ s.commit_template = 1;
++ s.colopts = 0;
++ s.display_comment_prefix = 1;
++ s.hints = 0;
++ s.use_color = 0;
++ s.whence = FROM_COMMIT;
++ s.committable = 1;
++
++ s.fp = fopen(git_path_commit_editmsg(), "a");
++ if (!s.fp)
++ return error_errno(_("could not open '%s'"), git_path_commit_editmsg());
++
++ wt_status_collect_changes_trees(&s, old_tree, new_tree);
++ wt_status_print(&s);
++ wt_status_collect_free_buffers(&s);
++ string_list_clear_func(&s.change, change_data_free);
++
++ strbuf_reset(out);
++ if (launch_editor(path, out, NULL)) {
++ fprintf(stderr, _("Please supply the message using the -m option.\n"));
++ return -1;
+ }
++ strbuf_stripspace(out, comment_line_str);
+
+ cleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);
+
@@ builtin/history.c
+ GIT_HISTORY_REWORD_USAGE,
+ NULL,
+ };
-+ const char *commit_message = NULL;
+ struct option options[] = {
-+ OPT_STRING('m', "message", &commit_message, N_("message"), N_("commit message")),
+ OPT_END(),
+ };
+ struct strbuf final_message = STRBUF_INIT;
@@ builtin/history.c
+ find_commit_subject(original_message, &original_body);
+
+ ret = fill_commit_message(repo, &parent_tree_oid, &original_commit_tree_oid,
-+ original_body, commit_message, "reworded", &final_message);
++ original_body, "reworded", &final_message);
+ if (ret < 0)
+ goto out;
+
@@ builtin/history.c
+ struct repository *repo)
{
const char * const usage[] = {
- N_("git history [<options>]"),
+- N_("git history [<options>]"),
+ GIT_HISTORY_REWORD_USAGE,
NULL,
};
@@ t/t3451-history-reword.sh (new)
+
+. ./test-lib.sh
+
++reword_with_message () {
++ cat >message &&
++ write_script fake-editor.sh <<-EOF &&
++ cp "$(pwd)/message" "\$1"
++ EOF
++ test_set_editor "$(pwd)"/fake-editor.sh &&
++ git history reword "$@" &&
++ rm fake-editor.sh message
++}
++
+test_expect_success 'refuses to work with merge commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
@@ t/t3451-history-reword.sh (new)
+ test_commit third &&
+
+ git symbolic-ref HEAD >expect &&
-+ git history reword -m "third reworded" HEAD &&
++ reword_with_message HEAD <<-EOF &&
++ third reworded
++ EOF
+ git symbolic-ref HEAD >actual &&
+ test_cmp expect actual &&
+
@@ t/t3451-history-reword.sh (new)
+ test_commit third &&
+
+ git symbolic-ref HEAD >expect &&
-+ git history reword -m "second reworded" HEAD~ &&
++ reword_with_message HEAD~ <<-EOF &&
++ second reworded
++ EOF
+ git symbolic-ref HEAD >actual &&
+ test_cmp expect actual &&
+
@@ t/t3451-history-reword.sh (new)
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
-+ git history reword -m "first reworded" HEAD~2 &&
++ reword_with_message HEAD~2 <<-EOF &&
++ first reworded
++ EOF
+
+ cat >expect <<-EOF &&
+ third
@@ t/t3451-history-reword.sh (new)
+# change in the future, and if it does this test here is expected to start
+# failing. In other words, this test is not an endorsement of the current
+# status quo.
-+test_expect_success 'hooks are executed for rewritten commits' '
++test_expect_success 'hooks are not executed for rewritten commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
@@ t/t3451-history-reword.sh (new)
+ touch "$(pwd)/hooks.log
+ EOF
+
-+ git history reword -m "second reworded" HEAD~ &&
++ reword_with_message HEAD~ <<-EOF &&
++ second reworded
++ EOF
+
+ cat >expect <<-EOF &&
+ third
@@ t/t3451-history-reword.sh (new)
+ cd repo &&
+ test_commit first &&
+
-+ test_must_fail git history reword -m "" HEAD 2>err &&
++ ! reword_with_message HEAD 2>err </dev/null &&
+ test_grep "Aborting commit due to empty commit message." err
+ )
+'
@@ t/t3451-history-reword.sh (new)
+ echo foo >a &&
+ echo bar >b &&
+ git add b &&
-+ git history reword HEAD -m message &&
++ reword_with_message HEAD <<-EOF &&
++ message
++ EOF
+ cat >expect <<-\EOF &&
+ M a
+ M b
7: a064a8c7eca = 6: a1b1e5877bc add-patch: split out header from "add-interactive.h"
8: 39c231e726f = 7: 4262663aa50 add-patch: split out `struct interactive_options`
9: 104a42a552f = 8: c285918281f add-patch: remove dependency on "add-interactive" subsystem
10: 2e60b2b6b48 = 9: 3f6e32241df add-patch: add support for in-memory index patching
11: 52bdbb2ac6a = 10: c25564e1d16 cache-tree: allow writing in-memory index as tree
12: 052ee5bd58b ! 11: 221dd490af1 builtin/history: implement "split" subcommand
@@ Commit message
## Documentation/git-history.adoc ##
@@ Documentation/git-history.adoc: SYNOPSIS
+ --------
[synopsis]
- git history [<options>]
- git history reword [<options>] <commit>
-+git history split [<options>] <commit> [--] [<pathspec>...]
+ git history reword <commit>
++git history split <commit> [--] [<pathspec>...]
DESCRIPTION
-----------
-@@ Documentation/git-history.adoc: rewrite history in different ways:
- provided, then this command will spawn an editor with the current
- message of that commit.
+@@ Documentation/git-history.adoc: Several commands are available to rewrite history in different ways:
+ details of this commit remain unchanged. This command will spawn an
+ editor with the current message of that commit.
-+`split [--message=<message>] <commit> [--] [<pathspec>...]`::
++`split <commit> [--] [<pathspec>...]`::
+ Interactively split up <commit> into two commits by choosing
+ hunks introduced by it that will be moved into the new split-out
+ commit. These hunks will then be written into a new commit that
@@ Documentation/git-history.adoc: rewrite history in different ways:
+ commit.
++
+The commit message of the new commit will be asked for by launching the
-+configured editor, unless it has been specified with the `-m` option.
-+Authorship of the commit will be the same as for the original commit.
++configured editor. Authorship of the commit will be the same as for the
++original commit.
++
+If passed, _<pathspec>_ can be used to limit which changes shall be split out
+of the original commit. Files not matching any of the pathspecs will remain
@@ Documentation/git-history.adoc: include::includes/cmd-config-section-all.adoc[]
+ foo | 1 +
+ 2 files changed, 2 insertions(+)
+
-+$ git history split HEAD --message="split-out commit"
++$ git history split HEAD
+diff --git a/bar b/bar
+new file mode 100644
+index 0000000..5716ca5
@@ builtin/history.c
#include "tree.h"
#include "wt-status.h"
- #define GIT_HISTORY_REWORD_USAGE N_("git history reword [<options>] <commit>")
-+#define GIT_HISTORY_SPLIT_USAGE N_("git history split [<options>] <commit> [--] [<pathspec>...]")
+ #define GIT_HISTORY_REWORD_USAGE N_("git history reword <commit>")
++#define GIT_HISTORY_SPLIT_USAGE N_("git history split <commit> [--] [<pathspec>...]")
static int collect_commits(struct repository *repo,
struct commit *old_commit,
@@ builtin/history.c: static int cmd_history_reword(int argc,
+static int split_commit(struct repository *repo,
+ struct commit *original_commit,
+ struct pathspec *pathspec,
-+ const char *commit_message,
+ struct object_id *out)
+{
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
@@ builtin/history.c: static int cmd_history_reword(int argc,
+ original_author = xmemdupz(ptr, len);
+
+ ret = fill_commit_message(repo, &parent_tree_oid, &split_tree->object.oid,
-+ "", commit_message, "split-out", &split_message);
++ "", "split-out", &split_message);
+ if (ret < 0)
+ goto out;
+
@@ builtin/history.c: static int cmd_history_reword(int argc,
+ GIT_HISTORY_SPLIT_USAGE,
+ NULL,
+ };
-+ const char *commit_message = NULL;
+ struct option options[] = {
-+ OPT_STRING('m', "message", &commit_message, N_("message"), N_("commit message")),
+ OPT_END(),
+ };
+ struct oidmap rewritten_commits = OIDMAP_INIT;
@@ builtin/history.c: static int cmd_history_reword(int argc,
+ * Then we split up the commit and replace the original commit with the
+ * new ones.
+ */
-+ ret = split_commit(repo, original_commit, &pathspec,
-+ commit_message, split_commits);
++ ret = split_commit(repo, original_commit, &pathspec, split_commits);
+ if (ret < 0)
+ goto out;
+
@@ builtin/history.c: static int cmd_history_reword(int argc,
const char **argv,
const char *prefix,
@@ builtin/history.c: int cmd_history(int argc,
+ {
const char * const usage[] = {
- N_("git history [<options>]"),
GIT_HISTORY_REWORD_USAGE,
+ GIT_HISTORY_SPLIT_USAGE,
NULL,
@@ t/t3452-history-split.sh (new)
+. ./test-lib.sh
+
+set_fake_editor () {
-+ write_script fake-editor.sh <<-\EOF &&
-+ echo "split-out commit" >"$1"
++ write_script fake-editor.sh <<-EOF &&
++ echo "$@" >"\$1"
+ EOF
+ test_set_editor "$(pwd)"/fake-editor.sh
+}
@@ t/t3452-history-split.sh (new)
+ git commit -m split-me &&
+
+ git symbolic-ref HEAD >expect &&
-+ set_fake_editor &&
++ set_fake_editor "split-out commit" &&
+ git history split HEAD <<-EOF &&
+ y
+ n
@@ t/t3452-history-split.sh (new)
+ git commit -m root &&
+ test_commit tip &&
+
-+ set_fake_editor &&
++ set_fake_editor "split-out commit" &&
+ git history split HEAD~ <<-EOF &&
+ y
+ n
@@ t/t3452-history-split.sh (new)
+ git commit -m split-me &&
+ test_commit tip &&
+
-+ set_fake_editor &&
++ set_fake_editor "split-out commit" &&
+ git history split HEAD~ <<-EOF &&
+ y
+ n
@@ t/t3452-history-split.sh (new)
+ git add . &&
+ git commit -m split-me &&
+
-+ git history split HEAD -m "split-out commit" <<-EOF &&
++ set_fake_editor "split-out-commit" &&
++ git history split HEAD <<-EOF &&
+ y
+ n
+ y
@@ t/t3452-history-split.sh (new)
+ git add . &&
+ git commit -m split-me &&
+
-+ git history split HEAD -m "split-out commit" <<-EOF &&
++ set_fake_editor "split-out commit" &&
++ git history split HEAD <<-EOF &&
+ n
+ y
+ EOF
@@ t/t3452-history-split.sh (new)
+ git add . &&
+ git commit -m split-me &&
+
-+ test_must_fail git history split HEAD -m "" <<-EOF 2>err &&
++ set_fake_editor "" &&
++ test_must_fail git history split HEAD <<-EOF 2>err &&
+ y
+ n
+ EOF
@@ t/t3452-history-split.sh (new)
+ )
+'
+
-+test_expect_success 'can specify message via option' '
-+ test_when_finished "rm -rf repo" &&
-+ git init repo &&
-+ (
-+ cd repo &&
-+ touch bar foo &&
-+ git add . &&
-+ git commit -m split-me &&
-+
-+ git history split HEAD -m "message option" <<-EOF &&
-+ y
-+ n
-+ EOF
-+
-+ expect_log <<-EOF
-+ split-me
-+ message option
-+ EOF
-+ )
-+'
-+
+test_expect_success 'commit message editor sees split-out changes' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
@@ t/t3452-history-split.sh (new)
+ git add . &&
+ git commit -m split-me &&
+
-+ git history split HEAD -m "message option" -- foo <<-EOF &&
++ set_fake_editor "split-out commit" &&
++ git history split HEAD -- foo <<-EOF &&
+ y
+ EOF
+
@@ t/t3452-history-split.sh (new)
+ touch "$(pwd)/hooks.log"
+ EOF
+
-+ set_fake_editor &&
++ set_fake_editor "split-out commit" &&
+ git history split HEAD <<-EOF &&
+ y
+ n
@@ t/t3452-history-split.sh (new)
+ echo a-modified >a &&
+ echo b-modified >b &&
+ git add b &&
-+ git history split HEAD -m a-only <<-EOF &&
++ set_fake_editor "a-only" &&
++ git history split HEAD <<-EOF &&
+ y
+ n
+ EOF
@@ t/t3452-history-split.sh (new)
+ M b
+ ?? actual
+ ?? expect
++ ?? fake-editor.sh
+ EOF
+ git status --porcelain >actual &&
+ test_cmp expect actual
---
base-commit: a912335e972768e607159eb74a4c7b62f86ee38e
change-id: 20250819-b4-pks-history-builtin-83398f9a05f0
^ permalink raw reply [flat|nested] 278+ messages in thread* [PATCH v6 01/11] wt-status: provide function to expose status for trees
2025-10-27 11:33 ` [PATCH v6 00/11] " Patrick Steinhardt
@ 2025-10-27 11:33 ` Patrick Steinhardt
2025-10-27 11:33 ` [PATCH v6 02/11] replay: extract logic to pick commits Patrick Steinhardt
` (11 subsequent siblings)
12 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-27 11:33 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
The "wt-status" subsystem is responsible for printing status information
around the current state of the working tree. This most importantly
includes information around whether the working tree or the index have
any changes.
We're about to introduce a new command where the changes in neither of
them are actually relevant to us. Instead, what we want is to format the
changes between two different trees. While it is a little bit of a
stretch to add this as functionality to _working tree_ status, it
doesn't make any sense to open-code this functionality, either.
Implement a new function `wt_status_collect_changes_trees()` that diffs
two trees and formats the status accordingly. This function is not yet
used, but will be in a subsequent commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
wt-status.c | 24 ++++++++++++++++++++++++
wt-status.h | 9 +++++++++
2 files changed, 33 insertions(+)
diff --git a/wt-status.c b/wt-status.c
index e12adb26b9f..95942399f8c 100644
--- a/wt-status.c
+++ b/wt-status.c
@@ -612,6 +612,30 @@ static void wt_status_collect_updated_cb(struct diff_queue_struct *q,
}
}
+void wt_status_collect_changes_trees(struct wt_status *s,
+ const struct object_id *old_treeish,
+ const struct object_id *new_treeish)
+{
+ struct diff_options opts = { 0 };
+
+ repo_diff_setup(s->repo, &opts);
+ opts.output_format = DIFF_FORMAT_CALLBACK;
+ opts.format_callback = wt_status_collect_updated_cb;
+ opts.format_callback_data = s;
+ opts.detect_rename = s->detect_rename >= 0 ? s->detect_rename : opts.detect_rename;
+ opts.rename_limit = s->rename_limit >= 0 ? s->rename_limit : opts.rename_limit;
+ opts.rename_score = s->rename_score >= 0 ? s->rename_score : opts.rename_score;
+ opts.flags.recursive = 1;
+ diff_setup_done(&opts);
+
+ diff_tree_oid(old_treeish, new_treeish, "", &opts);
+ diffcore_std(&opts);
+ diff_flush(&opts);
+ wt_status_get_state(s->repo, &s->state, 0);
+
+ diff_free(&opts);
+}
+
static void wt_status_collect_changes_worktree(struct wt_status *s)
{
struct rev_info rev;
diff --git a/wt-status.h b/wt-status.h
index e40a27214a7..e9fe32e98cc 100644
--- a/wt-status.h
+++ b/wt-status.h
@@ -153,6 +153,15 @@ void wt_status_add_cut_line(struct wt_status *s);
void wt_status_prepare(struct repository *r, struct wt_status *s);
void wt_status_print(struct wt_status *s);
void wt_status_collect(struct wt_status *s);
+
+/*
+ * Collect all changes between the two trees. Changes will be displayed as if
+ * they were staged into the index.
+ */
+void wt_status_collect_changes_trees(struct wt_status *s,
+ const struct object_id *old_treeish,
+ const struct object_id *new_treeish);
+
/*
* Frees the buffers allocated by wt_status_collect.
*/
--
2.51.1.930.gacf6e81ea2.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH v6 02/11] replay: extract logic to pick commits
2025-10-27 11:33 ` [PATCH v6 00/11] " Patrick Steinhardt
2025-10-27 11:33 ` [PATCH v6 01/11] wt-status: provide function to expose status for trees Patrick Steinhardt
@ 2025-10-27 11:33 ` Patrick Steinhardt
2025-11-17 16:27 ` Phillip Wood
2025-10-27 11:33 ` [PATCH v6 03/11] replay: stop using `the_repository` Patrick Steinhardt
` (10 subsequent siblings)
12 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-27 11:33 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
We're about to add a new git-history(1) command that will reuse some of
the same infrastructure as git-replay(1). To prepare for this, extract
the logic to pick a commit into a new "replay.c" file so that it can be
shared between both commands.
Rename the function to have a "replay_" prefix to clearly indicate its
subsystem.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Makefile | 1 +
builtin/replay.c | 110 ++--------------------------------------------------
meson.build | 1 +
replay.c | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
replay.h | 23 +++++++++++
5 files changed, 143 insertions(+), 107 deletions(-)
diff --git a/Makefile b/Makefile
index 1919d35bf3f..01c171b4f03 100644
--- a/Makefile
+++ b/Makefile
@@ -1261,6 +1261,7 @@ LIB_OBJS += reftable/tree.o
LIB_OBJS += reftable/writer.o
LIB_OBJS += remote.o
LIB_OBJS += replace-object.o
+LIB_OBJS += replay.o
LIB_OBJS += repo-settings.o
LIB_OBJS += repository.o
LIB_OBJS += rerere.o
diff --git a/builtin/replay.c b/builtin/replay.c
index bb0420dc992..e39824912cd 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -2,7 +2,6 @@
* "git replay" builtin command
*/
-#define USE_THE_REPOSITORY_VARIABLE
#define DISABLE_SIGN_COMPARE_WARNINGS
#include "git-compat-util.h"
@@ -16,6 +15,7 @@
#include "object-name.h"
#include "parse-options.h"
#include "refs.h"
+#include "replay.h"
#include "revision.h"
#include "strmap.h"
#include <oidset.h>
@@ -26,13 +26,6 @@ enum ref_action_mode {
REF_ACTION_PRINT
};
-static const char *short_commit_name(struct repository *repo,
- struct commit *commit)
-{
- return repo_find_unique_abbrev(repo, &commit->object.oid,
- DEFAULT_ABBREV);
-}
-
static struct commit *peel_committish(struct repository *repo, const char *name)
{
struct object *obj;
@@ -45,59 +38,6 @@ static struct commit *peel_committish(struct repository *repo, const char *name)
OBJ_COMMIT);
}
-static char *get_author(const char *message)
-{
- size_t len;
- const char *a;
-
- a = find_commit_header(message, "author", &len);
- if (a)
- return xmemdupz(a, len);
-
- return NULL;
-}
-
-static struct commit *create_commit(struct repository *repo,
- struct tree *tree,
- struct commit *based_on,
- struct commit *parent)
-{
- struct object_id ret;
- struct object *obj = NULL;
- struct commit_list *parents = NULL;
- char *author;
- char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
- struct commit_extra_header *extra = NULL;
- struct strbuf msg = STRBUF_INIT;
- const char *out_enc = get_commit_output_encoding();
- const char *message = repo_logmsg_reencode(repo, based_on,
- NULL, out_enc);
- const char *orig_message = NULL;
- const char *exclude_gpgsig[] = { "gpgsig", NULL };
-
- commit_list_insert(parent, &parents);
- extra = read_commit_extra_headers(based_on, exclude_gpgsig);
- find_commit_subject(message, &orig_message);
- strbuf_addstr(&msg, orig_message);
- author = get_author(message);
- reset_ident_date();
- if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,
- &ret, author, NULL, sign_commit, extra)) {
- error(_("failed to write commit object"));
- goto out;
- }
-
- obj = parse_object(repo, &ret);
-
-out:
- repo_unuse_commit_buffer(the_repository, based_on, message);
- free_commit_extra_headers(extra);
- free_commit_list(parents);
- strbuf_release(&msg);
- free(author);
- return (struct commit *)obj;
-}
-
struct ref_info {
struct commit *onto;
struct strset positive_refs;
@@ -246,50 +186,6 @@ static void determine_replay_mode(struct repository *repo,
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 repository *repo,
- struct commit *pickme,
- kh_oid_map_t *replayed_commits,
- struct commit *onto,
- struct merge_options *merge_opt,
- struct merge_result *result)
-{
- 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(repo, replayed_base);
- pickme_tree = repo_get_commit_tree(repo, pickme);
- base_tree = repo_get_commit_tree(repo, base);
-
- merge_opt->branch1 = short_commit_name(repo, replayed_base);
- merge_opt->branch2 = short_commit_name(repo, 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(repo, result->tree, pickme, replayed_base);
-}
-
static int handle_ref_update(enum ref_action_mode mode,
struct ref_transaction *transaction,
const char *refname,
@@ -483,8 +379,8 @@ int cmd_replay(int argc,
if (commit->parents->next)
die(_("replaying merge commits is not supported yet!"));
- last_commit = pick_regular_commit(repo, commit, replayed_commits,
- onto, &merge_opt, &result);
+ last_commit = replay_pick_regular_commit(repo, commit, replayed_commits,
+ onto, &merge_opt, &result);
if (!last_commit)
break;
diff --git a/meson.build b/meson.build
index cee94244759..ae8d4fef059 100644
--- a/meson.build
+++ b/meson.build
@@ -464,6 +464,7 @@ libgit_sources = [
'reftable/writer.c',
'remote.c',
'replace-object.c',
+ 'replay.c',
'repo-settings.c',
'repository.c',
'rerere.c',
diff --git a/replay.c b/replay.c
new file mode 100644
index 00000000000..98be33b8545
--- /dev/null
+++ b/replay.c
@@ -0,0 +1,115 @@
+#define USE_THE_REPOSITORY_VARIABLE
+
+#include "git-compat-util.h"
+#include "commit.h"
+#include "environment.h"
+#include "gettext.h"
+#include "ident.h"
+#include "object.h"
+#include "object-name.h"
+#include "replay.h"
+#include "tree.h"
+
+static const char *short_commit_name(struct repository *repo,
+ struct commit *commit)
+{
+ return repo_find_unique_abbrev(repo, &commit->object.oid,
+ DEFAULT_ABBREV);
+}
+
+static char *get_author(const char *message)
+{
+ size_t len;
+ const char *a;
+
+ a = find_commit_header(message, "author", &len);
+ if (a)
+ return xmemdupz(a, len);
+
+ return NULL;
+}
+
+struct commit *replay_create_commit(struct repository *repo,
+ struct tree *tree,
+ struct commit *based_on,
+ struct commit *parent)
+{
+ struct object_id ret;
+ struct object *obj = NULL;
+ struct commit_list *parents = NULL;
+ char *author;
+ char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
+ struct commit_extra_header *extra = NULL;
+ struct strbuf msg = STRBUF_INIT;
+ const char *out_enc = get_commit_output_encoding();
+ const char *message = repo_logmsg_reencode(repo, based_on,
+ NULL, out_enc);
+ const char *orig_message = NULL;
+ const char *exclude_gpgsig[] = { "gpgsig", NULL };
+
+ commit_list_insert(parent, &parents);
+ extra = read_commit_extra_headers(based_on, exclude_gpgsig);
+ find_commit_subject(message, &orig_message);
+ strbuf_addstr(&msg, orig_message);
+ author = get_author(message);
+ reset_ident_date();
+ if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,
+ &ret, author, NULL, sign_commit, extra)) {
+ error(_("failed to write commit object"));
+ goto out;
+ }
+
+ obj = parse_object(repo, &ret);
+
+out:
+ repo_unuse_commit_buffer(the_repository, based_on, message);
+ free_commit_extra_headers(extra);
+ free_commit_list(parents);
+ strbuf_release(&msg);
+ free(author);
+ return (struct commit *)obj;
+}
+
+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);
+}
+
+struct commit *replay_pick_regular_commit(struct repository *repo,
+ struct commit *pickme,
+ kh_oid_map_t *replayed_commits,
+ struct commit *onto,
+ struct merge_options *merge_opt,
+ struct merge_result *result)
+{
+ 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(repo, replayed_base);
+ pickme_tree = repo_get_commit_tree(repo, pickme);
+ base_tree = repo_get_commit_tree(repo, base);
+
+ merge_opt->branch1 = short_commit_name(repo, replayed_base);
+ merge_opt->branch2 = short_commit_name(repo, 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 replay_create_commit(repo, result->tree, pickme, replayed_base);
+}
diff --git a/replay.h b/replay.h
new file mode 100644
index 00000000000..d6535ee56c9
--- /dev/null
+++ b/replay.h
@@ -0,0 +1,23 @@
+#ifndef REPLAY_H
+#define REPLAY_H
+
+#include "khash.h"
+#include "merge-ort.h"
+#include "repository.h"
+
+struct commit;
+struct tree;
+
+struct commit *replay_create_commit(struct repository *repo,
+ struct tree *tree,
+ struct commit *based_on,
+ struct commit *parent);
+
+struct commit *replay_pick_regular_commit(struct repository *repo,
+ struct commit *pickme,
+ kh_oid_map_t *replayed_commits,
+ struct commit *onto,
+ struct merge_options *merge_opt,
+ struct merge_result *result);
+
+#endif
--
2.51.1.930.gacf6e81ea2.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH v6 02/11] replay: extract logic to pick commits
2025-10-27 11:33 ` [PATCH v6 02/11] replay: extract logic to pick commits Patrick Steinhardt
@ 2025-11-17 16:27 ` Phillip Wood
2025-11-20 7:01 ` Elijah Newren
0 siblings, 1 reply; 278+ messages in thread
From: Phillip Wood @ 2025-11-17 16:27 UTC (permalink / raw)
To: Patrick Steinhardt, git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
Hi Patrick
On 27/10/2025 11:33, Patrick Steinhardt wrote:
> We're about to add a new git-history(1) command that will reuse some of
> the same infrastructure as git-replay(1). To prepare for this, extract
> the logic to pick a commit into a new "replay.c" file so that it can be
> shared between both commands.
>
> Rename the function to have a "replay_" prefix to clearly indicate its
> subsystem.
I'm sorry it has taken me so long to get round to looking at this, I've
been intending to read through this series ever since you re-rolled
after the contributor summit.
This patch looks good, the only changes to the moved code are to
namespace the function which become public. I'm very pleased to see us
switching to using the replay machinery.
Thanks
Phillip
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> Makefile | 1 +
> builtin/replay.c | 110 ++--------------------------------------------------
> meson.build | 1 +
> replay.c | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
> replay.h | 23 +++++++++++
> 5 files changed, 143 insertions(+), 107 deletions(-)
>
> diff --git a/Makefile b/Makefile
> index 1919d35bf3f..01c171b4f03 100644
> --- a/Makefile
> +++ b/Makefile
> @@ -1261,6 +1261,7 @@ LIB_OBJS += reftable/tree.o
> LIB_OBJS += reftable/writer.o
> LIB_OBJS += remote.o
> LIB_OBJS += replace-object.o
> +LIB_OBJS += replay.o
> LIB_OBJS += repo-settings.o
> LIB_OBJS += repository.o
> LIB_OBJS += rerere.o
> diff --git a/builtin/replay.c b/builtin/replay.c
> index bb0420dc992..e39824912cd 100644
> --- a/builtin/replay.c
> +++ b/builtin/replay.c
> @@ -2,7 +2,6 @@
> * "git replay" builtin command
> */
>
> -#define USE_THE_REPOSITORY_VARIABLE
> #define DISABLE_SIGN_COMPARE_WARNINGS
>
> #include "git-compat-util.h"
> @@ -16,6 +15,7 @@
> #include "object-name.h"
> #include "parse-options.h"
> #include "refs.h"
> +#include "replay.h"
> #include "revision.h"
> #include "strmap.h"
> #include <oidset.h>
> @@ -26,13 +26,6 @@ enum ref_action_mode {
> REF_ACTION_PRINT
> };
>
> -static const char *short_commit_name(struct repository *repo,
> - struct commit *commit)
> -{
> - return repo_find_unique_abbrev(repo, &commit->object.oid,
> - DEFAULT_ABBREV);
> -}
> -
> static struct commit *peel_committish(struct repository *repo, const char *name)
> {
> struct object *obj;
> @@ -45,59 +38,6 @@ static struct commit *peel_committish(struct repository *repo, const char *name)
> OBJ_COMMIT);
> }
>
> -static char *get_author(const char *message)
> -{
> - size_t len;
> - const char *a;
> -
> - a = find_commit_header(message, "author", &len);
> - if (a)
> - return xmemdupz(a, len);
> -
> - return NULL;
> -}
> -
> -static struct commit *create_commit(struct repository *repo,
> - struct tree *tree,
> - struct commit *based_on,
> - struct commit *parent)
> -{
> - struct object_id ret;
> - struct object *obj = NULL;
> - struct commit_list *parents = NULL;
> - char *author;
> - char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
> - struct commit_extra_header *extra = NULL;
> - struct strbuf msg = STRBUF_INIT;
> - const char *out_enc = get_commit_output_encoding();
> - const char *message = repo_logmsg_reencode(repo, based_on,
> - NULL, out_enc);
> - const char *orig_message = NULL;
> - const char *exclude_gpgsig[] = { "gpgsig", NULL };
> -
> - commit_list_insert(parent, &parents);
> - extra = read_commit_extra_headers(based_on, exclude_gpgsig);
> - find_commit_subject(message, &orig_message);
> - strbuf_addstr(&msg, orig_message);
> - author = get_author(message);
> - reset_ident_date();
> - if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,
> - &ret, author, NULL, sign_commit, extra)) {
> - error(_("failed to write commit object"));
> - goto out;
> - }
> -
> - obj = parse_object(repo, &ret);
> -
> -out:
> - repo_unuse_commit_buffer(the_repository, based_on, message);
> - free_commit_extra_headers(extra);
> - free_commit_list(parents);
> - strbuf_release(&msg);
> - free(author);
> - return (struct commit *)obj;
> -}
> -
> struct ref_info {
> struct commit *onto;
> struct strset positive_refs;
> @@ -246,50 +186,6 @@ static void determine_replay_mode(struct repository *repo,
> 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 repository *repo,
> - struct commit *pickme,
> - kh_oid_map_t *replayed_commits,
> - struct commit *onto,
> - struct merge_options *merge_opt,
> - struct merge_result *result)
> -{
> - 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(repo, replayed_base);
> - pickme_tree = repo_get_commit_tree(repo, pickme);
> - base_tree = repo_get_commit_tree(repo, base);
> -
> - merge_opt->branch1 = short_commit_name(repo, replayed_base);
> - merge_opt->branch2 = short_commit_name(repo, 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(repo, result->tree, pickme, replayed_base);
> -}
> -
> static int handle_ref_update(enum ref_action_mode mode,
> struct ref_transaction *transaction,
> const char *refname,
> @@ -483,8 +379,8 @@ int cmd_replay(int argc,
> if (commit->parents->next)
> die(_("replaying merge commits is not supported yet!"));
>
> - last_commit = pick_regular_commit(repo, commit, replayed_commits,
> - onto, &merge_opt, &result);
> + last_commit = replay_pick_regular_commit(repo, commit, replayed_commits,
> + onto, &merge_opt, &result);
> if (!last_commit)
> break;
>
> diff --git a/meson.build b/meson.build
> index cee94244759..ae8d4fef059 100644
> --- a/meson.build
> +++ b/meson.build
> @@ -464,6 +464,7 @@ libgit_sources = [
> 'reftable/writer.c',
> 'remote.c',
> 'replace-object.c',
> + 'replay.c',
> 'repo-settings.c',
> 'repository.c',
> 'rerere.c',
> diff --git a/replay.c b/replay.c
> new file mode 100644
> index 00000000000..98be33b8545
> --- /dev/null
> +++ b/replay.c
> @@ -0,0 +1,115 @@
> +#define USE_THE_REPOSITORY_VARIABLE
> +
> +#include "git-compat-util.h"
> +#include "commit.h"
> +#include "environment.h"
> +#include "gettext.h"
> +#include "ident.h"
> +#include "object.h"
> +#include "object-name.h"
> +#include "replay.h"
> +#include "tree.h"
> +
> +static const char *short_commit_name(struct repository *repo,
> + struct commit *commit)
> +{
> + return repo_find_unique_abbrev(repo, &commit->object.oid,
> + DEFAULT_ABBREV);
> +}
> +
> +static char *get_author(const char *message)
> +{
> + size_t len;
> + const char *a;
> +
> + a = find_commit_header(message, "author", &len);
> + if (a)
> + return xmemdupz(a, len);
> +
> + return NULL;
> +}
> +
> +struct commit *replay_create_commit(struct repository *repo,
> + struct tree *tree,
> + struct commit *based_on,
> + struct commit *parent)
> +{
> + struct object_id ret;
> + struct object *obj = NULL;
> + struct commit_list *parents = NULL;
> + char *author;
> + char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
> + struct commit_extra_header *extra = NULL;
> + struct strbuf msg = STRBUF_INIT;
> + const char *out_enc = get_commit_output_encoding();
> + const char *message = repo_logmsg_reencode(repo, based_on,
> + NULL, out_enc);
> + const char *orig_message = NULL;
> + const char *exclude_gpgsig[] = { "gpgsig", NULL };
> +
> + commit_list_insert(parent, &parents);
> + extra = read_commit_extra_headers(based_on, exclude_gpgsig);
> + find_commit_subject(message, &orig_message);
> + strbuf_addstr(&msg, orig_message);
> + author = get_author(message);
> + reset_ident_date();
> + if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,
> + &ret, author, NULL, sign_commit, extra)) {
> + error(_("failed to write commit object"));
> + goto out;
> + }
> +
> + obj = parse_object(repo, &ret);
> +
> +out:
> + repo_unuse_commit_buffer(the_repository, based_on, message);
> + free_commit_extra_headers(extra);
> + free_commit_list(parents);
> + strbuf_release(&msg);
> + free(author);
> + return (struct commit *)obj;
> +}
> +
> +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);
> +}
> +
> +struct commit *replay_pick_regular_commit(struct repository *repo,
> + struct commit *pickme,
> + kh_oid_map_t *replayed_commits,
> + struct commit *onto,
> + struct merge_options *merge_opt,
> + struct merge_result *result)
> +{
> + 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(repo, replayed_base);
> + pickme_tree = repo_get_commit_tree(repo, pickme);
> + base_tree = repo_get_commit_tree(repo, base);
> +
> + merge_opt->branch1 = short_commit_name(repo, replayed_base);
> + merge_opt->branch2 = short_commit_name(repo, 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 replay_create_commit(repo, result->tree, pickme, replayed_base);
> +}
> diff --git a/replay.h b/replay.h
> new file mode 100644
> index 00000000000..d6535ee56c9
> --- /dev/null
> +++ b/replay.h
> @@ -0,0 +1,23 @@
> +#ifndef REPLAY_H
> +#define REPLAY_H
> +
> +#include "khash.h"
> +#include "merge-ort.h"
> +#include "repository.h"
> +
> +struct commit;
> +struct tree;
> +
> +struct commit *replay_create_commit(struct repository *repo,
> + struct tree *tree,
> + struct commit *based_on,
> + struct commit *parent);
> +
> +struct commit *replay_pick_regular_commit(struct repository *repo,
> + struct commit *pickme,
> + kh_oid_map_t *replayed_commits,
> + struct commit *onto,
> + struct merge_options *merge_opt,
> + struct merge_result *result);
> +
> +#endif
>
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH v6 02/11] replay: extract logic to pick commits
2025-11-17 16:27 ` Phillip Wood
@ 2025-11-20 7:01 ` Elijah Newren
0 siblings, 0 replies; 278+ messages in thread
From: Elijah Newren @ 2025-11-20 7:01 UTC (permalink / raw)
To: phillip.wood
Cc: Patrick Steinhardt, git, D. Ben Knoble, Junio C Hamano,
Sergey Organov, Jean-Noël AVILA, Martin von Zweigbergk,
Kristoffer Haugsbakk, Karthik Nayak
On Mon, Nov 17, 2025 at 8:27 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
>
> Hi Patrick
>
> On 27/10/2025 11:33, Patrick Steinhardt wrote:
> > We're about to add a new git-history(1) command that will reuse some of
> > the same infrastructure as git-replay(1). To prepare for this, extract
> > the logic to pick a commit into a new "replay.c" file so that it can be
> > shared between both commands.
> >
> > Rename the function to have a "replay_" prefix to clearly indicate its
> > subsystem.
>
> I'm sorry it has taken me so long to get round to looking at this, I've
> been intending to read through this series ever since you re-rolled
> after the contributor summit.
>
> This patch looks good, the only changes to the moved code are to
> namespace the function which become public. I'm very pleased to see us
> switching to using the replay machinery.
The _two_ functions which become public; otherwise, agreed with
Phillip's review comments.
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH v6 03/11] replay: stop using `the_repository`
2025-10-27 11:33 ` [PATCH v6 00/11] " Patrick Steinhardt
2025-10-27 11:33 ` [PATCH v6 01/11] wt-status: provide function to expose status for trees Patrick Steinhardt
2025-10-27 11:33 ` [PATCH v6 02/11] replay: extract logic to pick commits Patrick Steinhardt
@ 2025-10-27 11:33 ` Patrick Steinhardt
2025-11-20 7:01 ` Elijah Newren
2025-10-27 11:33 ` [PATCH v6 04/11] builtin: add new "history" command Patrick Steinhardt
` (9 subsequent siblings)
12 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-27 11:33 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
In `create_commit()` we're using `the_repository` even though we already
have a repository passed to use as an argument. Fix this.
Note that we still cannot get rid of `USE_THE_REPOSITORY_VARIABLE`. This
is because we use `DEFAULT_ABBREV and `get_commit_output_encoding()`,
both of which are stored as global variables that can be modified via
the Git configuration.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
replay.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/replay.c b/replay.c
index 98be33b854..58fdc20140 100644
--- a/replay.c
+++ b/replay.c
@@ -62,7 +62,7 @@ struct commit *replay_create_commit(struct repository *repo,
obj = parse_object(repo, &ret);
out:
- repo_unuse_commit_buffer(the_repository, based_on, message);
+ repo_unuse_commit_buffer(repo, based_on, message);
free_commit_extra_headers(extra);
free_commit_list(parents);
strbuf_release(&msg);
--
2.51.1.930.gacf6e81ea2.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH v6 03/11] replay: stop using `the_repository`
2025-10-27 11:33 ` [PATCH v6 03/11] replay: stop using `the_repository` Patrick Steinhardt
@ 2025-11-20 7:01 ` Elijah Newren
2025-12-02 18:47 ` Patrick Steinhardt
0 siblings, 1 reply; 278+ messages in thread
From: Elijah Newren @ 2025-11-20 7:01 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Karthik Nayak
On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:
>
> In `create_commit()` we're using `the_repository` even though we already
> have a repository passed to use as an argument. Fix this.
I feel like I've fixed this multiple times, but it keeps coming back.
In fact, I have this same fix locally in my replay-edit work. Thanks
for fixing it.
>
> Note that we still cannot get rid of `USE_THE_REPOSITORY_VARIABLE`. This
> is because we use `DEFAULT_ABBREV and `get_commit_output_encoding()`,
> both of which are stored as global variables that can be modified via
> the Git configuration.
Indeed.
Going on a tangent for a second...I feel like I've had to remove
"the_repository" from builtin/replay.c multiple times. In my local
replay-edit work, I actually added a "#define the_repository
DO_NOT_USE_THE_REPOSITORY" in builtin/replay.c, after all the header
includes, because the_repository isn't what builtin/replay.c is using,
it's these other two things that are also only included if
USE_THE_REPOSITORY_VARIABALE is defined. That obviously doesn't need
to be part of your series, but what would you think if I were to
submit that? Is it too ugly/weird of a way to avoid the_repository
being added back to builtin/replay.c so we can stop having to remove
it again?
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> replay.c | 2 +-
> 1 file changed, 1 insertion(+), 1 deletion(-)
>
> diff --git a/replay.c b/replay.c
> index 98be33b854..58fdc20140 100644
> --- a/replay.c
> +++ b/replay.c
> @@ -62,7 +62,7 @@ struct commit *replay_create_commit(struct repository *repo,
> obj = parse_object(repo, &ret);
>
> out:
> - repo_unuse_commit_buffer(the_repository, based_on, message);
> + repo_unuse_commit_buffer(repo, based_on, message);
> free_commit_extra_headers(extra);
> free_commit_list(parents);
> strbuf_release(&msg);
>
> --
> 2.51.1.930.gacf6e81ea2.dirty
Patch looks good.
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v6 03/11] replay: stop using `the_repository`
2025-11-20 7:01 ` Elijah Newren
@ 2025-12-02 18:47 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-12-02 18:47 UTC (permalink / raw)
To: Elijah Newren
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Karthik Nayak
On Wed, Nov 19, 2025 at 11:01:29PM -0800, Elijah Newren wrote:
> On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:
> >
> > In `create_commit()` we're using `the_repository` even though we already
> > have a repository passed to use as an argument. Fix this.
>
> I feel like I've fixed this multiple times, but it keeps coming back.
> In fact, I have this same fix locally in my replay-edit work. Thanks
> for fixing it.
>
> >
> > Note that we still cannot get rid of `USE_THE_REPOSITORY_VARIABLE`. This
> > is because we use `DEFAULT_ABBREV and `get_commit_output_encoding()`,
> > both of which are stored as global variables that can be modified via
> > the Git configuration.
>
> Indeed.
>
> Going on a tangent for a second...I feel like I've had to remove
> "the_repository" from builtin/replay.c multiple times. In my local
> replay-edit work, I actually added a "#define the_repository
> DO_NOT_USE_THE_REPOSITORY" in builtin/replay.c, after all the header
> includes, because the_repository isn't what builtin/replay.c is using,
> it's these other two things that are also only included if
> USE_THE_REPOSITORY_VARIABALE is defined. That obviously doesn't need
> to be part of your series, but what would you think if I were to
> submit that? Is it too ugly/weird of a way to avoid the_repository
> being added back to builtin/replay.c so we can stop having to remove
> it again?
It does feel somewhat ugly, and the better solution would of course be
to refactor both `DEFAULT_ABBREV` and `get_commit_output_encoding()` to
accept a repository as input. But if I remember correctly that was a
nontrivial endeavour, so your proposed hack might be the next-best
solution.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH v6 04/11] builtin: add new "history" command
2025-10-27 11:33 ` [PATCH v6 00/11] " Patrick Steinhardt
` (2 preceding siblings ...)
2025-10-27 11:33 ` [PATCH v6 03/11] replay: stop using `the_repository` Patrick Steinhardt
@ 2025-10-27 11:33 ` Patrick Steinhardt
2025-11-17 16:28 ` Phillip Wood
2025-11-20 7:02 ` Elijah Newren
2025-10-27 11:33 ` [PATCH v6 05/11] builtin/history: implement "reword" subcommand Patrick Steinhardt
` (8 subsequent siblings)
12 siblings, 2 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-27 11:33 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
When rewriting history via git-rebase(1) there are a couple of very
common use cases:
- The ordering of two commits should be reversed.
- A commit should be split up into two commits.
- A commit should be dropped from the history completely.
- Multiple commits should be squashed into one.
While these operations are all doable, it often feels needlessly kludgey
to do so by doing an interactive rebase, using the editor to say what
one wants, and then perform the actions. Furthermore, some operations
like splitting up a commit into two are way more involved than that and
require a whole series of commands.
Add a new "history" command to plug this gap. This command will have
several different subcommands to imperatively rewrite history for common
use cases like the above. These subcommands will be implemented in
subsequent commits.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
.gitignore | 1 +
Documentation/git-history.adoc | 44 ++++++++++++++++++++++++++++++++++++++++++
Documentation/meson.build | 1 +
Makefile | 1 +
builtin.h | 1 +
builtin/history.c | 22 +++++++++++++++++++++
command-list.txt | 1 +
git.c | 1 +
meson.build | 1 +
t/meson.build | 1 +
t/t3450-history.sh | 17 ++++++++++++++++
11 files changed, 91 insertions(+)
diff --git a/.gitignore b/.gitignore
index 78a45cb5bec..24635cf2d6f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -79,6 +79,7 @@
/git-grep
/git-hash-object
/git-help
+/git-history
/git-hook
/git-http-backend
/git-http-fetch
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
new file mode 100644
index 00000000000..6bdfeb50e8b
--- /dev/null
+++ b/Documentation/git-history.adoc
@@ -0,0 +1,44 @@
+git-history(1)
+==============
+
+NAME
+----
+git-history - EXPERIMENTAL: Rewrite history of the current branch
+
+SYNOPSIS
+--------
+[synopsis]
+git history [<options>]
+
+DESCRIPTION
+-----------
+
+Rewrite history by rearranging or modifying specific commits in the
+history.
+
+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
+
+This command is similar to linkgit:git-rebase[1] and uses the same
+underlying machinery. You should use rebases if you want to reapply a range of
+commits onto a different base, or interactive rebases if you want to edit a
+range of commits.
+
+Note that this command does not (yet) work with histories that contain
+merges. You should use linkgit:git-rebase[1] with the `--rebase-merges`
+flag instead.
+
+COMMANDS
+--------
+
+Several commands are available to rewrite history in different ways:
+
+CONFIGURATION
+-------------
+
+include::includes/cmd-config-section-all.adoc[]
+
+include::config/sequencer.adoc[]
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Documentation/meson.build b/Documentation/meson.build
index 9d24f2da544..d1f6bde7c16 100644
--- a/Documentation/meson.build
+++ b/Documentation/meson.build
@@ -64,6 +64,7 @@ manpages = {
'git-gui.adoc' : 1,
'git-hash-object.adoc' : 1,
'git-help.adoc' : 1,
+ 'git-history.adoc' : 1,
'git-hook.adoc' : 1,
'git-http-backend.adoc' : 1,
'git-http-fetch.adoc' : 1,
diff --git a/Makefile b/Makefile
index 01c171b4f03..1380ee1e196 100644
--- a/Makefile
+++ b/Makefile
@@ -1395,6 +1395,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o
BUILTIN_OBJS += builtin/grep.o
BUILTIN_OBJS += builtin/hash-object.o
BUILTIN_OBJS += builtin/help.o
+BUILTIN_OBJS += builtin/history.o
BUILTIN_OBJS += builtin/hook.o
BUILTIN_OBJS += builtin/index-pack.o
BUILTIN_OBJS += builtin/init-db.o
diff --git a/builtin.h b/builtin.h
index 1b35565fbd9..93c91d07d4b 100644
--- a/builtin.h
+++ b/builtin.h
@@ -172,6 +172,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix, struc
int cmd_grep(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_hash_object(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_help(int argc, const char **argv, const char *prefix, struct repository *repo);
+int cmd_history(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_hook(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_index_pack(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_init_db(int argc, const char **argv, const char *prefix, struct repository *repo);
diff --git a/builtin/history.c b/builtin/history.c
new file mode 100644
index 00000000000..f6fe32610b0
--- /dev/null
+++ b/builtin/history.c
@@ -0,0 +1,22 @@
+#include "builtin.h"
+#include "gettext.h"
+#include "parse-options.h"
+
+int cmd_history(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo UNUSED)
+{
+ const char * const usage[] = {
+ N_("git history [<options>]"),
+ NULL,
+ };
+ struct option options[] = {
+ OPT_END(),
+ };
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc)
+ usagef("unrecognized argument: %s", argv[0]);
+ return 0;
+}
diff --git a/command-list.txt b/command-list.txt
index accd3d0c4b5..f9005cf4597 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -115,6 +115,7 @@ git-grep mainporcelain info
git-gui mainporcelain
git-hash-object plumbingmanipulators
git-help ancillaryinterrogators complete
+git-history mainporcelain history
git-hook purehelpers
git-http-backend synchingrepositories
git-http-fetch synchelpers
diff --git a/git.c b/git.c
index c5fad56813f..744cb6527e0 100644
--- a/git.c
+++ b/git.c
@@ -586,6 +586,7 @@ static struct cmd_struct commands[] = {
{ "grep", cmd_grep, RUN_SETUP_GENTLY },
{ "hash-object", cmd_hash_object },
{ "help", cmd_help },
+ { "history", cmd_history, RUN_SETUP },
{ "hook", cmd_hook, RUN_SETUP },
{ "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },
{ "init", cmd_init_db },
diff --git a/meson.build b/meson.build
index ae8d4fef059..2d789612a01 100644
--- a/meson.build
+++ b/meson.build
@@ -604,6 +604,7 @@ builtin_sources = [
'builtin/grep.c',
'builtin/hash-object.c',
'builtin/help.c',
+ 'builtin/history.c',
'builtin/hook.c',
'builtin/index-pack.c',
'builtin/init-db.c',
diff --git a/t/meson.build b/t/meson.build
index 401b24e50e0..019435918fa 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -384,6 +384,7 @@ integration_tests = [
't3436-rebase-more-options.sh',
't3437-rebase-fixup-options.sh',
't3438-rebase-broken-files.sh',
+ 't3450-history.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
diff --git a/t/t3450-history.sh b/t/t3450-history.sh
new file mode 100755
index 00000000000..417c343d43b
--- /dev/null
+++ b/t/t3450-history.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+test_description='tests for git-history command'
+
+. ./test-lib.sh
+
+test_expect_success 'does nothing without any arguments' '
+ git history >out 2>&1 &&
+ test_must_be_empty out
+'
+
+test_expect_success 'raises an error with unknown argument' '
+ test_must_fail git history garbage 2>err &&
+ test_grep "unrecognized argument: garbage" err
+'
+
+test_done
--
2.51.1.930.gacf6e81ea2.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH v6 04/11] builtin: add new "history" command
2025-10-27 11:33 ` [PATCH v6 04/11] builtin: add new "history" command Patrick Steinhardt
@ 2025-11-17 16:28 ` Phillip Wood
2025-12-02 18:48 ` Patrick Steinhardt
2025-11-20 7:02 ` Elijah Newren
1 sibling, 1 reply; 278+ messages in thread
From: Phillip Wood @ 2025-11-17 16:28 UTC (permalink / raw)
To: Patrick Steinhardt, git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
Hi Patrick
On 27/10/2025 11:33, Patrick Steinhardt wrote:
> When rewriting history via git-rebase(1) there are a couple of very
There's more than a couple of items in this list, s/couple of/few/?
> common use cases:
>
> - The ordering of two commits should be reversed.
>
> - A commit should be split up into two commits.
>
> - A commit should be dropped from the history completely.
>
> - Multiple commits should be squashed into one.
I'd add editing an existing commit to this list, even if we don't
implement it initially
> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
> new file mode 100644
> index 00000000000..6bdfeb50e8b
> --- /dev/null
> +++ b/Documentation/git-history.adoc
> @@ -0,0 +1,44 @@
> +git-history(1)
> +==============
> +
> +NAME
> +----
> +git-history - EXPERIMENTAL: Rewrite history of the current branch
> +
> +SYNOPSIS
> +--------
> +[synopsis]
> +git history [<options>]
> +
> +DESCRIPTION
> +-----------
> +
> +Rewrite history by rearranging or modifying specific commits in the
> +history.
> +
> +THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
Excellent - keeping our options open is a very good idea
> +This command is similar to linkgit:git-rebase[1] and uses the same
> +underlying machinery.
This isn't strictly true now that we're baisg "git history" on the
replay machinery.
> You should use rebases if you want to reapply a range of
> +commits onto a different base, or interactive rebases if you want to edit a
> +range of commits.
> +
> +Note that this command does not (yet) work with histories that contain
> +merges. You should use linkgit:git-rebase[1] with the `--rebase-merges`
> +flag instead.
> +
> +COMMANDS
> +--------
> +
> +Several commands are available to rewrite history in different ways:
> +
> +CONFIGURATION
> +-------------
> +
> +include::includes/cmd-config-section-all.adoc[]
> +
> +include::config/sequencer.adoc[]
This probably isn't relevant now we're not using the sequencer.
Thanks
Phillip
> +
> +GIT
> +---
> +Part of the linkgit:git[1] suite
> diff --git a/Documentation/meson.build b/Documentation/meson.build
> index 9d24f2da544..d1f6bde7c16 100644
> --- a/Documentation/meson.build
> +++ b/Documentation/meson.build
> @@ -64,6 +64,7 @@ manpages = {
> 'git-gui.adoc' : 1,
> 'git-hash-object.adoc' : 1,
> 'git-help.adoc' : 1,
> + 'git-history.adoc' : 1,
> 'git-hook.adoc' : 1,
> 'git-http-backend.adoc' : 1,
> 'git-http-fetch.adoc' : 1,
> diff --git a/Makefile b/Makefile
> index 01c171b4f03..1380ee1e196 100644
> --- a/Makefile
> +++ b/Makefile
> @@ -1395,6 +1395,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o
> BUILTIN_OBJS += builtin/grep.o
> BUILTIN_OBJS += builtin/hash-object.o
> BUILTIN_OBJS += builtin/help.o
> +BUILTIN_OBJS += builtin/history.o
> BUILTIN_OBJS += builtin/hook.o
> BUILTIN_OBJS += builtin/index-pack.o
> BUILTIN_OBJS += builtin/init-db.o
> diff --git a/builtin.h b/builtin.h
> index 1b35565fbd9..93c91d07d4b 100644
> --- a/builtin.h
> +++ b/builtin.h
> @@ -172,6 +172,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix, struc
> int cmd_grep(int argc, const char **argv, const char *prefix, struct repository *repo);
> int cmd_hash_object(int argc, const char **argv, const char *prefix, struct repository *repo);
> int cmd_help(int argc, const char **argv, const char *prefix, struct repository *repo);
> +int cmd_history(int argc, const char **argv, const char *prefix, struct repository *repo);
> int cmd_hook(int argc, const char **argv, const char *prefix, struct repository *repo);
> int cmd_index_pack(int argc, const char **argv, const char *prefix, struct repository *repo);
> int cmd_init_db(int argc, const char **argv, const char *prefix, struct repository *repo);
> diff --git a/builtin/history.c b/builtin/history.c
> new file mode 100644
> index 00000000000..f6fe32610b0
> --- /dev/null
> +++ b/builtin/history.c
> @@ -0,0 +1,22 @@
> +#include "builtin.h"
> +#include "gettext.h"
> +#include "parse-options.h"
> +
> +int cmd_history(int argc,
> + const char **argv,
> + const char *prefix,
> + struct repository *repo UNUSED)
> +{
> + const char * const usage[] = {
> + N_("git history [<options>]"),
> + NULL,
> + };
> + struct option options[] = {
> + OPT_END(),
> + };
> +
> + argc = parse_options(argc, argv, prefix, options, usage, 0);
> + if (argc)
> + usagef("unrecognized argument: %s", argv[0]);
> + return 0;
> +}
> diff --git a/command-list.txt b/command-list.txt
> index accd3d0c4b5..f9005cf4597 100644
> --- a/command-list.txt
> +++ b/command-list.txt
> @@ -115,6 +115,7 @@ git-grep mainporcelain info
> git-gui mainporcelain
> git-hash-object plumbingmanipulators
> git-help ancillaryinterrogators complete
> +git-history mainporcelain history
> git-hook purehelpers
> git-http-backend synchingrepositories
> git-http-fetch synchelpers
> diff --git a/git.c b/git.c
> index c5fad56813f..744cb6527e0 100644
> --- a/git.c
> +++ b/git.c
> @@ -586,6 +586,7 @@ static struct cmd_struct commands[] = {
> { "grep", cmd_grep, RUN_SETUP_GENTLY },
> { "hash-object", cmd_hash_object },
> { "help", cmd_help },
> + { "history", cmd_history, RUN_SETUP },
> { "hook", cmd_hook, RUN_SETUP },
> { "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },
> { "init", cmd_init_db },
> diff --git a/meson.build b/meson.build
> index ae8d4fef059..2d789612a01 100644
> --- a/meson.build
> +++ b/meson.build
> @@ -604,6 +604,7 @@ builtin_sources = [
> 'builtin/grep.c',
> 'builtin/hash-object.c',
> 'builtin/help.c',
> + 'builtin/history.c',
> 'builtin/hook.c',
> 'builtin/index-pack.c',
> 'builtin/init-db.c',
> diff --git a/t/meson.build b/t/meson.build
> index 401b24e50e0..019435918fa 100644
> --- a/t/meson.build
> +++ b/t/meson.build
> @@ -384,6 +384,7 @@ integration_tests = [
> 't3436-rebase-more-options.sh',
> 't3437-rebase-fixup-options.sh',
> 't3438-rebase-broken-files.sh',
> + 't3450-history.sh',
> 't3500-cherry.sh',
> 't3501-revert-cherry-pick.sh',
> 't3502-cherry-pick-merge.sh',
> diff --git a/t/t3450-history.sh b/t/t3450-history.sh
> new file mode 100755
> index 00000000000..417c343d43b
> --- /dev/null
> +++ b/t/t3450-history.sh
> @@ -0,0 +1,17 @@
> +#!/bin/sh
> +
> +test_description='tests for git-history command'
> +
> +. ./test-lib.sh
> +
> +test_expect_success 'does nothing without any arguments' '
> + git history >out 2>&1 &&
> + test_must_be_empty out
> +'
> +
> +test_expect_success 'raises an error with unknown argument' '
> + test_must_fail git history garbage 2>err &&
> + test_grep "unrecognized argument: garbage" err
> +'
> +
> +test_done
>
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH v6 04/11] builtin: add new "history" command
2025-11-17 16:28 ` Phillip Wood
@ 2025-12-02 18:48 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-12-02 18:48 UTC (permalink / raw)
To: phillip.wood
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
On Mon, Nov 17, 2025 at 04:28:06PM +0000, Phillip Wood wrote:
> Hi Patrick
>
> On 27/10/2025 11:33, Patrick Steinhardt wrote:
> > When rewriting history via git-rebase(1) there are a couple of very
>
> There's more than a couple of items in this list, s/couple of/few/?
>
> > common use cases:
> >
> > - The ordering of two commits should be reversed.
> >
> > - A commit should be split up into two commits.
> >
> > - A commit should be dropped from the history completely.
> >
> > - Multiple commits should be squashed into one.
>
> I'd add editing an existing commit to this list, even if we don't implement
> it initially
Fair indeed.
> > diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
> > new file mode 100644
> > index 00000000000..6bdfeb50e8b
> > --- /dev/null
> > +++ b/Documentation/git-history.adoc
> > @@ -0,0 +1,44 @@
> > +git-history(1)
> > +==============
> > +
> > +NAME
> > +----
> > +git-history - EXPERIMENTAL: Rewrite history of the current branch
> > +
> > +SYNOPSIS
> > +--------
> > +[synopsis]
> > +git history [<options>]
> > +
> > +DESCRIPTION
> > +-----------
> > +
> > +Rewrite history by rearranging or modifying specific commits in the
> > +history.
> > +
> > +THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
>
> Excellent - keeping our options open is a very good idea
>
> > +This command is similar to linkgit:git-rebase[1] and uses the same
> > +underlying machinery.
>
> This isn't strictly true now that we're baisg "git history" on the replay
> machinery.
True, this is a historic leftover.
> > You should use rebases if you want to reapply a range of
> > +commits onto a different base, or interactive rebases if you want to edit a
> > +range of commits.
> > +
> > +Note that this command does not (yet) work with histories that contain
> > +merges. You should use linkgit:git-rebase[1] with the `--rebase-merges`
> > +flag instead.
> > +
> > +COMMANDS
> > +--------
> > +
> > +Several commands are available to rewrite history in different ways:
> > +
> > +CONFIGURATION
> > +-------------
> > +
> > +include::includes/cmd-config-section-all.adoc[]
> > +
> > +include::config/sequencer.adoc[]
>
> This probably isn't relevant now we're not using the sequencer.
And this, too.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v6 04/11] builtin: add new "history" command
2025-10-27 11:33 ` [PATCH v6 04/11] builtin: add new "history" command Patrick Steinhardt
2025-11-17 16:28 ` Phillip Wood
@ 2025-11-20 7:02 ` Elijah Newren
2025-12-02 18:48 ` Patrick Steinhardt
1 sibling, 1 reply; 278+ messages in thread
From: Elijah Newren @ 2025-11-20 7:02 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Karthik Nayak
In addition to what Phillip commented on...
On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:
>
> When rewriting history via git-rebase(1) there are a couple of very
> common use cases:
>
> - The ordering of two commits should be reversed.
>
> - A commit should be split up into two commits.
>
> - A commit should be dropped from the history completely.
>
> - Multiple commits should be squashed into one.
>
> While these operations are all doable, it often feels needlessly kludgey
> to do so by doing an interactive rebase, using the editor to say what
> one wants, and then perform the actions. Furthermore, some operations
> like splitting up a commit into two are way more involved than that and
> require a whole series of commands.
>
> Add a new "history" command to plug this gap. This command will have
> several different subcommands to imperatively rewrite history for common
> use cases like the above. These subcommands will be implemented in
> subsequent commits.
"...*Some of* these subcommands will be implemented...", right? You
only implement two of them in this series, not all of them, or am I
reading wrong?
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v6 04/11] builtin: add new "history" command
2025-11-20 7:02 ` Elijah Newren
@ 2025-12-02 18:48 ` Patrick Steinhardt
2025-12-02 22:44 ` D. Ben Knoble
0 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-12-02 18:48 UTC (permalink / raw)
To: Elijah Newren
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Karthik Nayak
On Wed, Nov 19, 2025 at 11:02:20PM -0800, Elijah Newren wrote:
> In addition to what Phillip commented on...
>
> On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:
> >
> > When rewriting history via git-rebase(1) there are a couple of very
> > common use cases:
> >
> > - The ordering of two commits should be reversed.
> >
> > - A commit should be split up into two commits.
> >
> > - A commit should be dropped from the history completely.
> >
> > - Multiple commits should be squashed into one.
> >
> > While these operations are all doable, it often feels needlessly kludgey
> > to do so by doing an interactive rebase, using the editor to say what
> > one wants, and then perform the actions. Furthermore, some operations
> > like splitting up a commit into two are way more involved than that and
> > require a whole series of commands.
> >
> > Add a new "history" command to plug this gap. This command will have
> > several different subcommands to imperatively rewrite history for common
> > use cases like the above. These subcommands will be implemented in
> > subsequent commits.
>
> "...*Some of* these subcommands will be implemented...", right? You
> only implement two of them in this series, not all of them, or am I
> reading wrong?
No, you're right. The initial versions of this series implemented more
of the above commands, but at no point in time did we actually implement
all of them.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v6 04/11] builtin: add new "history" command
2025-12-02 18:48 ` Patrick Steinhardt
@ 2025-12-02 22:44 ` D. Ben Knoble
2025-12-03 10:48 ` Patrick Steinhardt
0 siblings, 1 reply; 278+ messages in thread
From: D. Ben Knoble @ 2025-12-02 22:44 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: Elijah Newren, git, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Karthik Nayak
On Tue, Dec 2, 2025 at 1:48 PM Patrick Steinhardt <ps@pks.im> wrote:
>
> On Wed, Nov 19, 2025 at 11:02:20PM -0800, Elijah Newren wrote:
> > In addition to what Phillip commented on...
> >
> > On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:
> > >
> > > When rewriting history via git-rebase(1) there are a couple of very
> > > common use cases:
> > >
> > > - The ordering of two commits should be reversed.
> > >
> > > - A commit should be split up into two commits.
> > >
> > > - A commit should be dropped from the history completely.
> > >
> > > - Multiple commits should be squashed into one.
> > >
> > > While these operations are all doable, it often feels needlessly kludgey
> > > to do so by doing an interactive rebase, using the editor to say what
> > > one wants, and then perform the actions. Furthermore, some operations
> > > like splitting up a commit into two are way more involved than that and
> > > require a whole series of commands.
> > >
> > > Add a new "history" command to plug this gap. This command will have
> > > several different subcommands to imperatively rewrite history for common
> > > use cases like the above. These subcommands will be implemented in
> > > subsequent commits.
> >
> > "...*Some of* these subcommands will be implemented...", right? You
> > only implement two of them in this series, not all of them, or am I
> > reading wrong?
>
> No, you're right. The initial versions of this series implemented more
> of the above commands, but at no point in time did we actually implement
> all of them.
>
> Patrick
While I'm thinking of it, at work today I had occasion to use "drop"
and "reorder" (from an old version of this series whose binary I
happened to still have laying around), and it was very convenient.
Looking forward to it ;)
- drop: I had made some changes on my tree that needed to be in a
separate branch. I didn't want to mess with stashes for some reason,
so I did "commit; switch -c …; cherry-pick @@{1}" (or something
similar). Then when coming back (switch -), I could just do "drop @".
I'm sure there's a better way to do this slicing that wouldn't have
needed drop, but I couldn't think of it at the time.
- reorder: I have a series of mostly logical, separate wip commits
that need some more explanation and might need further tweaks as I go;
I'm working on uncommitted changes that logically belong to the tip,
but spot something else that will be a separate commit. I make that
commit, then "reorder @ --before-commit=@~", and voilà, I'm ready to
amend again when I get around to committing the rest of what I have.
--
D. Ben Knoble
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH v6 04/11] builtin: add new "history" command
2025-12-02 22:44 ` D. Ben Knoble
@ 2025-12-03 10:48 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-12-03 10:48 UTC (permalink / raw)
To: D. Ben Knoble
Cc: Elijah Newren, git, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Karthik Nayak
On Tue, Dec 02, 2025 at 05:44:15PM -0500, D. Ben Knoble wrote:
> On Tue, Dec 2, 2025 at 1:48 PM Patrick Steinhardt <ps@pks.im> wrote:
> >
> > On Wed, Nov 19, 2025 at 11:02:20PM -0800, Elijah Newren wrote:
> > > In addition to what Phillip commented on...
> > >
> > > On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:
> > > >
> > > > When rewriting history via git-rebase(1) there are a couple of very
> > > > common use cases:
> > > >
> > > > - The ordering of two commits should be reversed.
> > > >
> > > > - A commit should be split up into two commits.
> > > >
> > > > - A commit should be dropped from the history completely.
> > > >
> > > > - Multiple commits should be squashed into one.
> > > >
> > > > While these operations are all doable, it often feels needlessly kludgey
> > > > to do so by doing an interactive rebase, using the editor to say what
> > > > one wants, and then perform the actions. Furthermore, some operations
> > > > like splitting up a commit into two are way more involved than that and
> > > > require a whole series of commands.
> > > >
> > > > Add a new "history" command to plug this gap. This command will have
> > > > several different subcommands to imperatively rewrite history for common
> > > > use cases like the above. These subcommands will be implemented in
> > > > subsequent commits.
> > >
> > > "...*Some of* these subcommands will be implemented...", right? You
> > > only implement two of them in this series, not all of them, or am I
> > > reading wrong?
> >
> > No, you're right. The initial versions of this series implemented more
> > of the above commands, but at no point in time did we actually implement
> > all of them.
> >
> > Patrick
>
> While I'm thinking of it, at work today I had occasion to use "drop"
> and "reorder" (from an old version of this series whose binary I
> happened to still have laying around), and it was very convenient.
> Looking forward to it ;)
>
> - drop: I had made some changes on my tree that needed to be in a
> separate branch. I didn't want to mess with stashes for some reason,
> so I did "commit; switch -c …; cherry-pick @@{1}" (or something
> similar). Then when coming back (switch -), I could just do "drop @".
> I'm sure there's a better way to do this slicing that wouldn't have
> needed drop, but I couldn't think of it at the time.
>
> - reorder: I have a series of mostly logical, separate wip commits
> that need some more explanation and might need further tweaks as I go;
> I'm working on uncommitted changes that logically belong to the tip,
> but spot something else that will be a separate commit. I make that
> commit, then "reorder @ --before-commit=@~", and voilà, I'm ready to
> amend again when I get around to committing the rest of what I have.
Yeah, I definitely do wish to also upstream these two going forward.
It's going to be a bit more complicated though now that we are building
on top of the replay infrastructure, because it doesn't know to handle
conflicts.
Maybe for an initial version we can get away with just bailing out on a
conflict and then iterate.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH v6 05/11] builtin/history: implement "reword" subcommand
2025-10-27 11:33 ` [PATCH v6 00/11] " Patrick Steinhardt
` (3 preceding siblings ...)
2025-10-27 11:33 ` [PATCH v6 04/11] builtin: add new "history" command Patrick Steinhardt
@ 2025-10-27 11:33 ` Patrick Steinhardt
2025-11-17 16:27 ` Phillip Wood
` (2 more replies)
2025-10-27 11:33 ` [PATCH v6 06/11] add-patch: split out header from "add-interactive.h" Patrick Steinhardt
` (7 subsequent siblings)
12 siblings, 3 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-27 11:33 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
Implement a new "reword" subcommand for git-history(1). This subcommand
is essentially the same as if a user performed an interactive rebase
with a single commit changed to use the "reword" verb.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-history.adoc | 7 +-
builtin/history.c | 331 ++++++++++++++++++++++++++++++++++++++++-
t/meson.build | 1 +
t/t3450-history.sh | 6 +-
t/t3451-history-reword.sh | 237 +++++++++++++++++++++++++++++
5 files changed, 573 insertions(+), 9 deletions(-)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index 6bdfeb50e8b..bd903875120 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -8,7 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history of the current branch
SYNOPSIS
--------
[synopsis]
-git history [<options>]
+git history reword <commit>
DESCRIPTION
-----------
@@ -32,6 +32,11 @@ COMMANDS
Several commands are available to rewrite history in different ways:
+`reword <commit>`::
+ Rewrite the commit message of the specified commit. All the other
+ details of this commit remain unchanged. This command will spawn an
+ editor with the current message of that commit.
+
CONFIGURATION
-------------
diff --git a/builtin/history.c b/builtin/history.c
index f6fe32610b0..cb251ae2e01 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -1,22 +1,343 @@
+#define USE_THE_REPOSITORY_VARIABLE
+
#include "builtin.h"
+#include "commit-reach.h"
+#include "commit.h"
+#include "config.h"
+#include "editor.h"
+#include "environment.h"
#include "gettext.h"
+#include "hex.h"
#include "parse-options.h"
+#include "refs.h"
+#include "replay.h"
+#include "reset.h"
+#include "revision.h"
+#include "sequencer.h"
+#include "strvec.h"
+#include "tree.h"
+#include "wt-status.h"
+
+#define GIT_HISTORY_REWORD_USAGE N_("git history reword <commit>")
+
+static int collect_commits(struct repository *repo,
+ struct commit *old_commit,
+ struct commit *new_commit,
+ struct strvec *out)
+{
+ struct setup_revision_opt revision_opts = {
+ .assume_dashdash = 1,
+ };
+ struct strvec revisions = STRVEC_INIT;
+ struct commit *child;
+ struct rev_info rev = { 0 };
+ int ret;
+
+ repo_init_revisions(repo, &rev, NULL);
+ strvec_push(&revisions, "");
+ strvec_push(&revisions, oid_to_hex(&new_commit->object.oid));
+ if (old_commit)
+ strvec_pushf(&revisions, "^%s", oid_to_hex(&old_commit->object.oid));
+
+ setup_revisions_from_strvec(&revisions, &rev, &revision_opts);
+ if (revisions.nr != 1 || prepare_revision_walk(&rev)) {
+ ret = error(_("revision walk setup failed"));
+ goto out;
+ }
+
+ while ((child = get_revision(&rev))) {
+ if (old_commit && !child->parents)
+ BUG("revision walk did not find child commit");
+ if (child->parents && child->parents->next) {
+ ret = error(_("cannot rearrange commit history with merges"));
+ goto out;
+ }
+
+ strvec_push(out, oid_to_hex(&child->object.oid));
+
+ if (child->parents && old_commit &&
+ commit_list_contains(old_commit, child->parents))
+ break;
+ }
+
+ /*
+ * Revisions are in newest-order-first. We have to reverse the
+ * array though so that we pick the oldest commits first.
+ */
+ for (size_t i = 0, j = out->nr - 1; i < j; i++, j--)
+ SWAP(out->v[i], out->v[j]);
+
+ ret = 0;
+
+out:
+ strvec_clear(&revisions);
+ release_revisions(&rev);
+ reset_revision_walk();
+ return ret;
+}
+
+static void replace_commits(struct strvec *commits,
+ const struct object_id *commit_to_replace,
+ const struct object_id *replacements,
+ size_t replacements_nr)
+{
+ char commit_to_replace_oid[GIT_MAX_HEXSZ + 1];
+ struct strvec replacement_oids = STRVEC_INIT;
+ bool found = false;
+
+ oid_to_hex_r(commit_to_replace_oid, commit_to_replace);
+ for (size_t i = 0; i < replacements_nr; i++)
+ strvec_push(&replacement_oids, oid_to_hex(&replacements[i]));
+
+ for (size_t i = 0; i < commits->nr; i++) {
+ if (strcmp(commits->v[i], commit_to_replace_oid))
+ continue;
+ strvec_splice(commits, i, 1, replacement_oids.v, replacement_oids.nr);
+ found = true;
+ break;
+ }
+ if (!found)
+ BUG("could not find commit to replace");
+
+ strvec_clear(&replacement_oids);
+}
+
+static int apply_commits(struct repository *repo,
+ const struct strvec *commits,
+ struct commit *onto,
+ struct commit *orig_head,
+ const char *action)
+{
+ struct reset_head_opts reset_opts = { 0 };
+ struct strbuf buf = STRBUF_INIT;
+ int ret;
+
+ for (size_t i = 0; i < commits->nr; i++) {
+ struct object_id commit_id;
+ struct commit *commit;
+ const char *end;
+
+ if (parse_oid_hex_algop(commits->v[i], &commit_id, &end,
+ repo->hash_algo)) {
+ ret = error(_("invalid object ID: %s"), commits->v[i]);
+ goto out;
+ }
+
+ commit = lookup_commit(repo, &commit_id);
+ if (!commit || repo_parse_commit(repo, commit)) {
+ ret = error(_("failed to look up commit: %s"), oid_to_hex(&commit_id));
+ goto out;
+ }
+
+ if (!onto) {
+ onto = commit;
+ } else {
+ struct tree *tree = repo_get_commit_tree(repo, commit);
+ onto = replay_create_commit(repo, tree, commit, onto);
+ if (!onto)
+ break;
+ }
+ }
+
+ reset_opts.oid = &onto->object.oid;
+ strbuf_addf(&buf, "%s: switch to rewritten %s", action, oid_to_hex(reset_opts.oid));
+ reset_opts.flags = RESET_HEAD_REFS_ONLY | RESET_ORIG_HEAD;
+ reset_opts.orig_head = &orig_head->object.oid;
+ reset_opts.default_reflog_action = action;
+ if (reset_head(repo, &reset_opts) < 0) {
+ ret = error(_("could not switch to %s"), oid_to_hex(reset_opts.oid));
+ goto out;
+ }
+
+ ret = 0;
+
+out:
+ strbuf_release(&buf);
+ return ret;
+}
+
+static void change_data_free(void *util, const char *str UNUSED)
+{
+ struct wt_status_change_data *d = util;
+ free(d->rename_source);
+ free(d);
+}
+
+static int fill_commit_message(struct repository *repo,
+ const struct object_id *old_tree,
+ const struct object_id *new_tree,
+ const char *default_message,
+ const char *action,
+ struct strbuf *out)
+{
+ const char *path = git_path_commit_editmsg();
+ const char *hint =
+ _("Please enter the commit message for the %s changes."
+ " Lines starting\nwith '%s' will be ignored.\n");
+ struct wt_status s;
+
+ strbuf_addstr(out, default_message);
+ strbuf_addch(out, '\n');
+ strbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);
+ write_file_buf(path, out->buf, out->len);
+
+ wt_status_prepare(repo, &s);
+ FREE_AND_NULL(s.branch);
+ s.ahead_behind_flags = AHEAD_BEHIND_QUICK;
+ s.commit_template = 1;
+ s.colopts = 0;
+ s.display_comment_prefix = 1;
+ s.hints = 0;
+ s.use_color = 0;
+ s.whence = FROM_COMMIT;
+ s.committable = 1;
+
+ s.fp = fopen(git_path_commit_editmsg(), "a");
+ if (!s.fp)
+ return error_errno(_("could not open '%s'"), git_path_commit_editmsg());
+
+ wt_status_collect_changes_trees(&s, old_tree, new_tree);
+ wt_status_print(&s);
+ wt_status_collect_free_buffers(&s);
+ string_list_clear_func(&s.change, change_data_free);
+
+ strbuf_reset(out);
+ if (launch_editor(path, out, NULL)) {
+ fprintf(stderr, _("Please supply the message using the -m option.\n"));
+ return -1;
+ }
+ strbuf_stripspace(out, comment_line_str);
+
+ cleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);
+
+ if (!out->len) {
+ fprintf(stderr, _("Aborting commit due to empty commit message.\n"));
+ return -1;
+ }
+
+ return 0;
+}
+
+static int cmd_history_reword(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo)
+{
+ const char * const usage[] = {
+ GIT_HISTORY_REWORD_USAGE,
+ NULL,
+ };
+ struct option options[] = {
+ OPT_END(),
+ };
+ struct strbuf final_message = STRBUF_INIT;
+ struct commit *original_commit, *parent, *head;
+ struct strvec commits = STRVEC_INIT;
+ struct object_id parent_tree_oid, original_commit_tree_oid;
+ struct object_id rewritten_commit;
+ struct commit_list *from_list = NULL;
+ const char *original_message, *original_body, *ptr;
+ char *original_author = NULL;
+ size_t len;
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc != 1) {
+ ret = error(_("command expects a single revision"));
+ goto out;
+ }
+ repo_config(repo, git_default_config, NULL);
+
+ original_commit = lookup_commit_reference_by_name(argv[0]);
+ if (!original_commit) {
+ ret = error(_("commit to be reworded cannot be found: %s"), argv[0]);
+ goto out;
+ }
+ original_commit_tree_oid = repo_get_commit_tree(repo, original_commit)->object.oid;
+
+ parent = original_commit->parents ? original_commit->parents->item : NULL;
+ if (parent) {
+ if (repo_parse_commit(repo, parent)) {
+ ret = error(_("unable to parse commit %s"),
+ oid_to_hex(&parent->object.oid));
+ goto out;
+ }
+ parent_tree_oid = repo_get_commit_tree(repo, parent)->object.oid;
+ } else {
+ oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
+ }
+
+ head = lookup_commit_reference_by_name("HEAD");
+ if (!head) {
+ ret = error(_("could not resolve HEAD to a commit"));
+ goto out;
+ }
+
+ commit_list_append(original_commit, &from_list);
+ if (!repo_is_descendant_of(repo, head, from_list)) {
+ ret = error (_("split commit must be reachable from current HEAD commit"));
+ goto out;
+ }
+
+ /*
+ * Collect the list of commits that we'll have to reapply now already.
+ * This ensures that we'll abort early on in case the range of commits
+ * contains merges, which we do not yet handle.
+ */
+ ret = collect_commits(repo, parent, head, &commits);
+ if (ret < 0)
+ goto out;
+
+ /* We retain authorship of the original commit. */
+ original_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);
+ ptr = find_commit_header(original_message, "author", &len);
+ if (ptr)
+ original_author = xmemdupz(ptr, len);
+ find_commit_subject(original_message, &original_body);
+
+ ret = fill_commit_message(repo, &parent_tree_oid, &original_commit_tree_oid,
+ original_body, "reworded", &final_message);
+ if (ret < 0)
+ goto out;
+
+ ret = commit_tree(final_message.buf, final_message.len, &original_commit_tree_oid,
+ original_commit->parents, &rewritten_commit, original_author, NULL);
+ if (ret < 0) {
+ ret = error(_("failed writing reworded commit"));
+ goto out;
+ }
+
+ replace_commits(&commits, &original_commit->object.oid, &rewritten_commit, 1);
+
+ ret = apply_commits(repo, &commits, parent, head, "reword");
+ if (ret < 0)
+ goto out;
+
+ ret = 0;
+
+out:
+ strbuf_release(&final_message);
+ free_commit_list(from_list);
+ strvec_clear(&commits);
+ free(original_author);
+ return ret;
+}
int cmd_history(int argc,
const char **argv,
const char *prefix,
- struct repository *repo UNUSED)
+ struct repository *repo)
{
const char * const usage[] = {
- N_("git history [<options>]"),
+ GIT_HISTORY_REWORD_USAGE,
NULL,
};
+ parse_opt_subcommand_fn *fn = NULL;
struct option options[] = {
+ OPT_SUBCOMMAND("reword", &fn, cmd_history_reword),
OPT_END(),
};
argc = parse_options(argc, argv, prefix, options, usage, 0);
- if (argc)
- usagef("unrecognized argument: %s", argv[0]);
- return 0;
+ return fn(argc, argv, prefix, repo);
}
diff --git a/t/meson.build b/t/meson.build
index 019435918fa..a3ec9199947 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -385,6 +385,7 @@ integration_tests = [
't3437-rebase-fixup-options.sh',
't3438-rebase-broken-files.sh',
't3450-history.sh',
+ 't3451-history-reword.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
diff --git a/t/t3450-history.sh b/t/t3450-history.sh
index 417c343d43b..f513463b92b 100755
--- a/t/t3450-history.sh
+++ b/t/t3450-history.sh
@@ -5,13 +5,13 @@ test_description='tests for git-history command'
. ./test-lib.sh
test_expect_success 'does nothing without any arguments' '
- git history >out 2>&1 &&
- test_must_be_empty out
+ test_must_fail git history 2>err &&
+ test_grep "need a subcommand" err
'
test_expect_success 'raises an error with unknown argument' '
test_must_fail git history garbage 2>err &&
- test_grep "unrecognized argument: garbage" err
+ test_grep "unknown subcommand: .garbage." err
'
test_done
diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
new file mode 100755
index 00000000000..09dbc463c59
--- /dev/null
+++ b/t/t3451-history-reword.sh
@@ -0,0 +1,237 @@
+#!/bin/sh
+
+test_description='tests for git-history reword subcommand'
+
+. ./test-lib.sh
+
+reword_with_message () {
+ cat >message &&
+ write_script fake-editor.sh <<-EOF &&
+ cp "$(pwd)/message" "\$1"
+ EOF
+ test_set_editor "$(pwd)"/fake-editor.sh &&
+ git history reword "$@" &&
+ rm fake-editor.sh message
+}
+
+test_expect_success 'refuses to work with merge commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ git branch branch &&
+ test_commit ours &&
+ git switch branch &&
+ test_commit theirs &&
+ git switch - &&
+ git merge theirs &&
+ test_must_fail git history reword HEAD~ 2>err &&
+ test_grep "cannot rearrange commit history with merges" err &&
+ test_must_fail git history reword HEAD 2>err &&
+ test_grep "cannot rearrange commit history with merges" err
+ )
+'
+
+test_expect_success 'refuses to work with unrelated commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ git branch branch &&
+ test_commit ours &&
+ git switch branch &&
+ test_commit theirs &&
+ test_must_fail git history reword ours 2>err &&
+ test_grep "split commit must be reachable from current HEAD commit" err
+ )
+'
+
+test_expect_success 'can reword tip of a branch' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ git symbolic-ref HEAD >expect &&
+ reword_with_message HEAD <<-EOF &&
+ third reworded
+ EOF
+ git symbolic-ref HEAD >actual &&
+ test_cmp expect actual &&
+
+ cat >expect <<-EOF &&
+ third reworded
+ second
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'can reword commit in the middle' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ git symbolic-ref HEAD >expect &&
+ reword_with_message HEAD~ <<-EOF &&
+ second reworded
+ EOF
+ git symbolic-ref HEAD >actual &&
+ test_cmp expect actual &&
+
+ cat >expect <<-EOF &&
+ third
+ second reworded
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'can reword root commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ reword_with_message HEAD~2 <<-EOF &&
+ first reworded
+ EOF
+
+ cat >expect <<-EOF &&
+ third
+ second
+ first reworded
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'can use editor to rewrite commit message' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+
+ write_script fake-editor.sh <<-\EOF &&
+ cp "$1" . &&
+ printf "\namend a comment\n" >>"$1"
+ EOF
+ test_set_editor "$(pwd)"/fake-editor.sh &&
+ git history reword HEAD &&
+
+ cat >expect <<-EOF &&
+ first
+
+ # Please enter the commit message for the reworded changes. Lines starting
+ # with ${SQ}#${SQ} will be ignored.
+ # Changes to be committed:
+ # new file: first.t
+ #
+ EOF
+ test_cmp expect COMMIT_EDITMSG &&
+
+ cat >expect <<-EOF &&
+ first
+
+ amend a comment
+
+ EOF
+ git log --format=%B >actual &&
+ test_cmp expect actual
+ )
+'
+
+# For now, git-history(1) does not yet execute any hooks. This is subject to
+# change in the future, and if it does this test here is expected to start
+# failing. In other words, this test is not an endorsement of the current
+# status quo.
+test_expect_success 'hooks are not executed for rewritten commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ write_script .git/hooks/prepare-commit-msg <<-EOF &&
+ touch "$(pwd)/hooks.log
+ EOF
+ write_script .git/hooks/post-commit <<-EOF &&
+ touch "$(pwd)/hooks.log
+ EOF
+ write_script .git/hooks/post-rewrite <<-EOF &&
+ touch "$(pwd)/hooks.log
+ EOF
+
+ reword_with_message HEAD~ <<-EOF &&
+ second reworded
+ EOF
+
+ cat >expect <<-EOF &&
+ third
+ second reworded
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual &&
+
+ test_path_is_missing hooks.log
+ )
+'
+
+test_expect_success 'aborts with empty commit message' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+
+ ! reword_with_message HEAD 2>err </dev/null &&
+ test_grep "Aborting commit due to empty commit message." err
+ )
+'
+
+test_expect_success 'retains changes in the worktree and index' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch a b &&
+ git add . &&
+ git commit -m "initial commit" &&
+ echo foo >a &&
+ echo bar >b &&
+ git add b &&
+ reword_with_message HEAD <<-EOF &&
+ message
+ EOF
+ cat >expect <<-\EOF &&
+ M a
+ M b
+ ?? actual
+ ?? expect
+ EOF
+ git status --porcelain >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_done
--
2.51.1.930.gacf6e81ea2.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH v6 05/11] builtin/history: implement "reword" subcommand
2025-10-27 11:33 ` [PATCH v6 05/11] builtin/history: implement "reword" subcommand Patrick Steinhardt
@ 2025-11-17 16:27 ` Phillip Wood
2025-12-02 18:50 ` Patrick Steinhardt
2025-11-20 7:03 ` Elijah Newren
2025-11-25 8:31 ` SZEDER Gábor
2 siblings, 1 reply; 278+ messages in thread
From: Phillip Wood @ 2025-11-17 16:27 UTC (permalink / raw)
To: Patrick Steinhardt, git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
Hi Patrick
On 27/10/2025 11:33, Patrick Steinhardt wrote:
> +static int collect_commits(struct repository *repo,
> + struct commit *old_commit,
> + struct commit *new_commit,
> + struct strvec *out)
Now that we're not using the sequencer it would be nice to stop messing
about converting object ids to and from strings and return an array of
"struct commit" instead of "struct strvec"
> +{
> + struct setup_revision_opt revision_opts = {
> + .assume_dashdash = 1,
> + };
> + struct strvec revisions = STRVEC_INIT;
> + struct commit *child;
> + struct rev_info rev = { 0 };
> + int ret;
> +
> + repo_init_revisions(repo, &rev, NULL);
> + strvec_push(&revisions, "");
> + strvec_push(&revisions, oid_to_hex(&new_commit->object.oid));
> + if (old_commit)
> + strvec_pushf(&revisions, "^%s", oid_to_hex(&old_commit->object.oid));
> +
> + setup_revisions_from_strvec(&revisions, &rev, &revision_opts);
> + if (revisions.nr != 1 || prepare_revision_walk(&rev)) {
I'm not that familiar with the revision walking api, what 'revisions.nr
!= 1' check for here?
> +static int apply_commits(struct repository *repo,
> + const struct strvec *commits,
> + struct commit *onto,
> + struct commit *orig_head,
> + const char *action)
> +{
> + struct reset_head_opts reset_opts = { 0 };
> + struct strbuf buf = STRBUF_INIT;
> + int ret;
> +
> + for (size_t i = 0; i < commits->nr; i++) {
> + struct object_id commit_id;
> + struct commit *commit;
> + const char *end;
> +
> + if (parse_oid_hex_algop(commits->v[i], &commit_id, &end,
> + repo->hash_algo)) {
> + ret = error(_("invalid object ID: %s"), commits->v[i]);
> + goto out;
> + }
> + commit = lookup_commit(repo, &commit_id);
> + if (!commit || repo_parse_commit(repo, commit)) {
> + ret = error(_("failed to look up commit: %s"), oid_to_hex(&commit_id));
> + goto out;
> + }
Using an array of "struct commit" rather than "struct strvec" would mean
we could delete everything in the loop body up to here.
> + if (!onto) {
> + onto = commit;
> + } else {
> + struct tree *tree = repo_get_commit_tree(repo, commit);
> + onto = replay_create_commit(repo, tree, commit, onto);
> + if (!onto)
> + break;
Don't we want to avoid updating HEAD if replay_create_commit() fails?
> + }
> + }
> +
> + reset_opts.oid = &onto->object.oid;
> + strbuf_addf(&buf, "%s: switch to rewritten %s", action, oid_to_hex(reset_opts.oid));
We're not switching branches so I wonder if saying "history: <action>
<oid> <commit subject>" might be a more useful reflog entry
> +static int fill_commit_message(struct repository *repo,
> + const struct object_id *old_tree,
> + const struct object_id *new_tree,
> + const char *default_message,
> + const char *action,
> + struct strbuf *out)
> +{
> + const char *path = git_path_commit_editmsg();
> + const char *hint =
> + _("Please enter the commit message for the %s changes."
Maybe "Please edit the commit message"? Also do we want to tell the user
they can abort by clearing the commit message?
> + " Lines starting\nwith '%s' will be ignored.\n");
> + struct wt_status s;
> +
> + strbuf_addstr(out, default_message);
> + strbuf_addch(out, '\n');
> + strbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);
> + write_file_buf(path, out->buf, out->len);
> +
> + wt_status_prepare(repo, &s);
> + FREE_AND_NULL(s.branch);
> + s.ahead_behind_flags = AHEAD_BEHIND_QUICK;
> + s.commit_template = 1;
> + s.colopts = 0;
> + s.display_comment_prefix = 1;
> + s.hints = 0;
> + s.use_color = 0;
> + s.whence = FROM_COMMIT;
> + s.committable = 1;
"git commit" reads a load of status related config settings, is any of
that relevant here?
> + s.fp = fopen(git_path_commit_editmsg(), "a");
> + if (!s.fp)
> + return error_errno(_("could not open '%s'"), git_path_commit_editmsg());
> +
> + wt_status_collect_changes_trees(&s, old_tree, new_tree);
> + wt_status_print(&s);
> + wt_status_collect_free_buffers(&s);
> + string_list_clear_func(&s.change, change_data_free);
> +
> + strbuf_reset(out);
> + if (launch_editor(path, out, NULL)) {
> + fprintf(stderr, _("Please supply the message using the -m option.\n"));
I'm not sure that's a very helpful suggestion as we don't support "-m"
(it's not very helpful when "git commit --amend" suggests it either). We
should just give up if the editor fails.
> +static int cmd_history_reword(int argc,
> + const char **argv,
> + const char *prefix,
> + struct repository *repo)
> +{
> + const char * const usage[] = {
> + GIT_HISTORY_REWORD_USAGE,
> + NULL,
> + };
> + struct option options[] = {
> + OPT_END(),
> + };
> + struct strbuf final_message = STRBUF_INIT;
> + struct commit *original_commit, *parent, *head;
> + struct strvec commits = STRVEC_INIT;
> + struct object_id parent_tree_oid, original_commit_tree_oid;
> + struct object_id rewritten_commit;
> + struct commit_list *from_list = NULL;
> + const char *original_message, *original_body, *ptr;
> + char *original_author = NULL;
> + size_t len;
> + int ret;
> +
> + argc = parse_options(argc, argv, prefix, options, usage, 0);
> + if (argc != 1) {
> + ret = error(_("command expects a single revision"));
> + goto out;
> + }
> + repo_config(repo, git_default_config, NULL);
> +
> + original_commit = lookup_commit_reference_by_name(argv[0]);
> + if (!original_commit) {
> + ret = error(_("commit to be reworded cannot be found: %s"), argv[0]);
> + goto out;
> + }
> + original_commit_tree_oid = repo_get_commit_tree(repo, original_commit)->object.oid;
Looking at the implementation of repo_get_commit_tree() it can return NULL
> +
> + parent = original_commit->parents ? original_commit->parents->item : NULL;
> + if (parent) {
> + if (repo_parse_commit(repo, parent)) {
> + ret = error(_("unable to parse commit %s"),
> + oid_to_hex(&parent->object.oid));
> + goto out;
> + }
> + parent_tree_oid = repo_get_commit_tree(repo, parent)->object.oid;
> + } else {
> + oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
> + }
> +
> + head = lookup_commit_reference_by_name("HEAD");
> + if (!head) {
> + ret = error(_("could not resolve HEAD to a commit"));
> + goto out;
> + }
> +
> + commit_list_append(original_commit, &from_list);
> + if (!repo_is_descendant_of(repo, head, from_list)) {
> + ret = error (_("split commit must be reachable from current HEAD commit"));
s/split/reword/
> + goto out;
> + }
> +
> + /*
> + * Collect the list of commits that we'll have to reapply now already.
> + * This ensures that we'll abort early on in case the range of commits
> + * contains merges, which we do not yet handle.
> + */
Ah, I wondered if we should be checking that the reworded commit wasn't
a merge when we look at the parents above but we don't need to because
we will error out here if it is.
> diff --git a/t/t3450-history.sh b/t/t3450-history.sh
> index 417c343d43b..f513463b92b 100755
> --- a/t/t3450-history.sh
> +++ b/t/t3450-history.sh
> @@ -5,13 +5,13 @@ test_description='tests for git-history command'
> . ./test-lib.sh
>
> test_expect_success 'does nothing without any arguments' '
> - git history >out 2>&1 &&
> - test_must_be_empty out
> + test_must_fail git history 2>err &&
> + test_grep "need a subcommand" err
> '
>
> test_expect_success 'raises an error with unknown argument' '
> test_must_fail git history garbage 2>err &&
> - test_grep "unrecognized argument: garbage" err
> + test_grep "unknown subcommand: .garbage." err
> '
>
> test_done
Do we really need a separate test file just for a couple of tests. I can
see that having a separate test file for each subcommand makes sense but
can't we just add these two tests to one of those?
> diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
> new file mode 100755
> index 00000000000..09dbc463c59
> --- /dev/null
> +++ b/t/t3451-history-reword.sh
> @@ -0,0 +1,237 @@
> +#!/bin/sh
> +
> +test_description='tests for git-history reword subcommand'
> +
> +. ./test-lib.sh
> +
> +reword_with_message () {
> + cat >message &&
> + write_script fake-editor.sh <<-EOF &&
> + cp "$(pwd)/message" "\$1"
Let's hope $(pwd) doesn't contain any dollar signs, backticks,
backslashes or double quotes. Doing
export MSG_PATH="$(pwd)/message"
write_script fake-editor.sh <<-\EOF &&
cp "$MSG_PATH" "$1"
EOF
would be safer
> +test_expect_success 'refuses to work with merge commits' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
Do we really need to set up a separate repo for each test? The test
suite is slow enough already without running "git init" followed by a
bunch calls to test_commit() in each test. Can we instead run "git reset
--hard <known-starting-point> at the beginning of each test? That
removes any interdependence between tests but saves a bunch of processes.
> +test_expect_success 'can reword root commit' '
It's nice that unlike "git rebase" this works without much effort from
the implementation.
> +test_expect_success 'can use editor to rewrite commit message' '
Don't all the other tests check that? This test is checking what's
presented to the user which is a good idea but I wouldn't have guessed
that from the test name.
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + test_commit first &&
> +
> + write_script fake-editor.sh <<-\EOF &&
> + cp "$1" . &&
> + printf "\namend a comment\n" >>"$1"
> + EOF
> + test_set_editor "$(pwd)"/fake-editor.sh &&
> + git history reword HEAD &&
> +
> + cat >expect <<-EOF &&
> + first
> +
> + # Please enter the commit message for the reworded changes. Lines starting
> + # with ${SQ}#${SQ} will be ignored.
> + # Changes to be committed:
> + # new file: first.t
> + #
> + EOF
> + test_cmp expect COMMIT_EDITMSG &&
> +
> + cat >expect <<-EOF &&
> + first
> +
> + amend a comment
> +
> + EOF
> + git log --format=%B >actual &&
> + test_cmp expect actual
We have test_commit_message() to do this which will accept the expected
message on stdin.
> + )
> +'
> +
> +# For now, git-history(1) does not yet execute any hooks. This is subject to
> +# change in the future, and if it does this test here is expected to start
> +# failing. In other words, this test is not an endorsement of the current
> +# status quo.
> +test_expect_success 'hooks are not executed for rewritten commits' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + test_commit first &&
> + test_commit second &&
> + test_commit third &&
> +
> + write_script .git/hooks/prepare-commit-msg <<-EOF &&
> + touch "$(pwd)/hooks.log
This has the same problem of expanding $(pwd) as fake-editor.sh. For
debugging it would be nicer if the hook scripts did
echo "$hook_name" >>hooks.log
so we can easily see which hooks are causing the test to fail.
> + EOF
> + write_script .git/hooks/post-commit <<-EOF &&
> + touch "$(pwd)/hooks.log
> + EOF
> + write_script .git/hooks/post-rewrite <<-EOF &&
> + touch "$(pwd)/hooks.log
> + EOF
This is good idea. We should add tests for the "pre-commit" and
"commit-msg" hooks as well.
Overall the test coverage looks good, the only thing we might want to
add is a check for the reflog message. Thanks for working on this, I'll
try and look at the rest of the patches sometime this week.
Phillip
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH v6 05/11] builtin/history: implement "reword" subcommand
2025-11-17 16:27 ` Phillip Wood
@ 2025-12-02 18:50 ` Patrick Steinhardt
2025-12-10 9:52 ` Phillip Wood
0 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-12-02 18:50 UTC (permalink / raw)
To: phillip.wood
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
On Mon, Nov 17, 2025 at 04:27:59PM +0000, Phillip Wood wrote:
> Hi Patrick
> On 27/10/2025 11:33, Patrick Steinhardt wrote:
>
> > +static int collect_commits(struct repository *repo,
> > + struct commit *old_commit,
> > + struct commit *new_commit,
> > + struct strvec *out)
>
> Now that we're not using the sequencer it would be nice to stop messing
> about converting object ids to and from strings and return an array of
> "struct commit" instead of "struct strvec"
I was trying to avoid using a strvec, but honestly that turned out to be
more pain than it is worth. We don't have functions like
`strvec_splice()` for simple arrays, and there is no commit array struct
that provides similar wrappers, either.
> > +{
> > + struct setup_revision_opt revision_opts = {
> > + .assume_dashdash = 1,
> > + };
> > + struct strvec revisions = STRVEC_INIT;
> > + struct commit *child;
> > + struct rev_info rev = { 0 };
> > + int ret;
> > +
> > + repo_init_revisions(repo, &rev, NULL);
> > + strvec_push(&revisions, "");
> > + strvec_push(&revisions, oid_to_hex(&new_commit->object.oid));
> > + if (old_commit)
> > + strvec_pushf(&revisions, "^%s", oid_to_hex(&old_commit->object.oid));
> > +
> > + setup_revisions_from_strvec(&revisions, &rev, &revision_opts);
> > + if (revisions.nr != 1 || prepare_revision_walk(&rev)) {
>
> I'm not that familiar with the revision walking api, what 'revisions.nr !=
> 1' check for here?
It's basically a check that the revision arguments have all been
consumed, except for the initial empty argument. The interface is a bit
weird.
[snip]
> > + if (!onto) {
> > + onto = commit;
> > + } else {
> > + struct tree *tree = repo_get_commit_tree(repo, commit);
> > + onto = replay_create_commit(repo, tree, commit, onto);
> > + if (!onto)
> > + break;
>
> Don't we want to avoid updating HEAD if replay_create_commit() fails?
Good point, yes.
> > + }
> > + }
> > +
> > + reset_opts.oid = &onto->object.oid;
> > + strbuf_addf(&buf, "%s: switch to rewritten %s", action, oid_to_hex(reset_opts.oid));
>
> We're not switching branches so I wonder if saying "history: <action> <oid>
> <commit subject>" might be a more useful reflog entry
We're not switching branches, true, but we do switch to the rewritten
commit. Also I'm not sure that printing the commit subject here would
make sense, as the question becomes which subject to print: the one
we're moving to, which is the new tip of the branch but may not be the
commit we have rewritten? Or do we print the subject of the rewritten
commit?
> > +static int fill_commit_message(struct repository *repo,
> > + const struct object_id *old_tree,
> > + const struct object_id *new_tree,
> > + const char *default_message,
> > + const char *action,
> > + struct strbuf *out)
> > +{
> > + const char *path = git_path_commit_editmsg();
> > + const char *hint =
> > + _("Please enter the commit message for the %s changes."
>
> Maybe "Please edit the commit message"? Also do we want to tell the user
> they can abort by clearing the commit message?
The "Please edit the commit message" thing is taken from other commands
that phrase it similarly. But it certainly does make sense to note that
clearing the commit message aborts, will add.
> > + " Lines starting\nwith '%s' will be ignored.\n");
> > + struct wt_status s;
> > +
> > + strbuf_addstr(out, default_message);
> > + strbuf_addch(out, '\n');
> > + strbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);
> > + write_file_buf(path, out->buf, out->len);
> > +
> > + wt_status_prepare(repo, &s);
> > + FREE_AND_NULL(s.branch);
> > + s.ahead_behind_flags = AHEAD_BEHIND_QUICK;
> > + s.commit_template = 1;
> > + s.colopts = 0;
> > + s.display_comment_prefix = 1;
> > + s.hints = 0;
> > + s.use_color = 0;
> > + s.whence = FROM_COMMIT;
> > + s.committable = 1;
>
> "git commit" reads a load of status related config settings, is any of that
> relevant here?
Yeah, some of it is. We don't handle them all yet, but this will be
backfilled in the future.
> > + s.fp = fopen(git_path_commit_editmsg(), "a");
> > + if (!s.fp)
> > + return error_errno(_("could not open '%s'"), git_path_commit_editmsg());
> > +
> > + wt_status_collect_changes_trees(&s, old_tree, new_tree);
> > + wt_status_print(&s);
> > + wt_status_collect_free_buffers(&s);
> > + string_list_clear_func(&s.change, change_data_free);
> > +
> > + strbuf_reset(out);
> > + if (launch_editor(path, out, NULL)) {
> > + fprintf(stderr, _("Please supply the message using the -m option.\n"));
>
> I'm not sure that's a very helpful suggestion as we don't support "-m" (it's
> not very helpful when "git commit --amend" suggests it either). We should
> just give up if the editor fails.
Ah, this is a leftover error message from previous iterations.
> > +static int cmd_history_reword(int argc,
> > + const char **argv,
> > + const char *prefix,
> > + struct repository *repo)
> > +{
> > + const char * const usage[] = {
> > + GIT_HISTORY_REWORD_USAGE,
> > + NULL,
> > + };
> > + struct option options[] = {
> > + OPT_END(),
> > + };
> > + struct strbuf final_message = STRBUF_INIT;
> > + struct commit *original_commit, *parent, *head;
> > + struct strvec commits = STRVEC_INIT;
> > + struct object_id parent_tree_oid, original_commit_tree_oid;
> > + struct object_id rewritten_commit;
> > + struct commit_list *from_list = NULL;
> > + const char *original_message, *original_body, *ptr;
> > + char *original_author = NULL;
> > + size_t len;
> > + int ret;
> > +
> > + argc = parse_options(argc, argv, prefix, options, usage, 0);
> > + if (argc != 1) {
> > + ret = error(_("command expects a single revision"));
> > + goto out;
> > + }
> > + repo_config(repo, git_default_config, NULL);
> > +
> > + original_commit = lookup_commit_reference_by_name(argv[0]);
> > + if (!original_commit) {
> > + ret = error(_("commit to be reworded cannot be found: %s"), argv[0]);
> > + goto out;
> > + }
> > + original_commit_tree_oid = repo_get_commit_tree(repo, original_commit)->object.oid;
>
> Looking at the implementation of repo_get_commit_tree() it can return NULL
> > diff --git a/t/t3450-history.sh b/t/t3450-history.sh
> > index 417c343d43b..f513463b92b 100755
> > --- a/t/t3450-history.sh
> > +++ b/t/t3450-history.sh
> > @@ -5,13 +5,13 @@ test_description='tests for git-history command'
> > . ./test-lib.sh
> > test_expect_success 'does nothing without any arguments' '
> > - git history >out 2>&1 &&
> > - test_must_be_empty out
> > + test_must_fail git history 2>err &&
> > + test_grep "need a subcommand" err
> > '
> > test_expect_success 'raises an error with unknown argument' '
> > test_must_fail git history garbage 2>err &&
> > - test_grep "unrecognized argument: garbage" err
> > + test_grep "unknown subcommand: .garbage." err
> > '
> > test_done
>
> Do we really need a separate test file just for a couple of tests. I can see
> that having a separate test file for each subcommand makes sense but can't
> we just add these two tests to one of those?
I felt it was dirty to randomly add it to any of the other test suites,
so I decided to instead have it in its own standalone file. It may also
become relevant in the future if we ever needed commands like for
example `git history --continue`, same as the sequencer-based commands
have.
> > diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
> > new file mode 100755
> > index 00000000000..09dbc463c59
> > --- /dev/null
> > +++ b/t/t3451-history-reword.sh
> > @@ -0,0 +1,237 @@
> > +#!/bin/sh
> > +
> > +test_description='tests for git-history reword subcommand'
> > +
> > +. ./test-lib.sh
> > +
> > +reword_with_message () {
> > + cat >message &&
> > + write_script fake-editor.sh <<-EOF &&
> > + cp "$(pwd)/message" "\$1"
>
> Let's hope $(pwd) doesn't contain any dollar signs, backticks, backslashes
> or double quotes. Doing
>
> export MSG_PATH="$(pwd)/message"
> write_script fake-editor.sh <<-\EOF &&
> cp "$MSG_PATH" "$1"
> EOF
>
> would be safer
True. Will use this:
diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
index 8b353e74dc..4c87953176 100755
--- a/t/t3451-history-reword.sh
+++ b/t/t3451-history-reword.sh
@@ -6,11 +6,11 @@ test_description='tests for git-history reword subcommand'
reword_with_message () {
cat >message &&
- write_script fake-editor.sh <<-EOF &&
- cp "$(pwd)/message" "\$1"
+ write_script fake-editor.sh <<-\EOF &&
+ cp "$ORIG_PATH/message" "$1"
EOF
test_set_editor "$(pwd)"/fake-editor.sh &&
- git history reword "$@" &&
+ ORIG_PATH="$(pwd)" git history reword "$@" &&
rm fake-editor.sh message
}
> > +test_expect_success 'refuses to work with merge commits' '
> > + test_when_finished "rm -rf repo" &&
> > + git init repo &&
> > + (
>
> Do we really need to set up a separate repo for each test? The test suite is
> slow enough already without running "git init" followed by a bunch calls to
> test_commit() in each test. Can we instead run "git reset --hard
> <known-starting-point> at the beginning of each test? That removes any
> interdependence between tests but saves a bunch of processes.
I prefer that style as it is extremely hard to reason about tests that
have interdependencies, and not all the state may be removed by a hard
reset.
> > + test_when_finished "rm -rf repo" &&
> > + git init repo &&
> > + (
> > + cd repo &&
> > + test_commit first &&
> > +
> > + write_script fake-editor.sh <<-\EOF &&
> > + cp "$1" . &&
> > + printf "\namend a comment\n" >>"$1"
> > + EOF
> > + test_set_editor "$(pwd)"/fake-editor.sh &&
> > + git history reword HEAD &&
> > +
> > + cat >expect <<-EOF &&
> > + first
> > +
> > + # Please enter the commit message for the reworded changes. Lines starting
> > + # with ${SQ}#${SQ} will be ignored.
> > + # Changes to be committed:
> > + # new file: first.t
> > + #
> > + EOF
> > + test_cmp expect COMMIT_EDITMSG &&
> > +
> > + cat >expect <<-EOF &&
> > + first
> > +
> > + amend a comment
> > +
> > + EOF
> > + git log --format=%B >actual &&
> > + test_cmp expect actual
>
> We have test_commit_message() to do this which will accept the expected
> message on stdin.
Ah, indeed.
> > + )
> > +'
> > +
> > +# For now, git-history(1) does not yet execute any hooks. This is subject to
> > +# change in the future, and if it does this test here is expected to start
> > +# failing. In other words, this test is not an endorsement of the current
> > +# status quo.
> > +test_expect_success 'hooks are not executed for rewritten commits' '
> > + test_when_finished "rm -rf repo" &&
> > + git init repo &&
> > + (
> > + cd repo &&
> > + test_commit first &&
> > + test_commit second &&
> > + test_commit third &&
> > +
> > + write_script .git/hooks/prepare-commit-msg <<-EOF &&
> > + touch "$(pwd)/hooks.log
>
> This has the same problem of expanding $(pwd) as fake-editor.sh. For
> debugging it would be nicer if the hook scripts did
>
> echo "$hook_name" >>hooks.log
>
> so we can easily see which hooks are causing the test to fail.
I'll rephrain from doing this as it would require `<<-EOF` instead of
`<<-\EOF`.
> > + EOF
> > + write_script .git/hooks/post-commit <<-EOF &&
> > + touch "$(pwd)/hooks.log
> > + EOF
> > + write_script .git/hooks/post-rewrite <<-EOF &&
> > + touch "$(pwd)/hooks.log
> > + EOF
>
> This is good idea. We should add tests for the "pre-commit" and "commit-msg"
> hooks as well.
>
> Overall the test coverage looks good, the only thing we might want to add is
> a check for the reflog message. Thanks for working on this, I'll try and
> look at the rest of the patches sometime this week.
Thanks!
Patrick
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH v6 05/11] builtin/history: implement "reword" subcommand
2025-12-02 18:50 ` Patrick Steinhardt
@ 2025-12-10 9:52 ` Phillip Wood
0 siblings, 0 replies; 278+ messages in thread
From: Phillip Wood @ 2025-12-10 9:52 UTC (permalink / raw)
To: Patrick Steinhardt, phillip.wood
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
On 02/12/2025 18:50, Patrick Steinhardt wrote:
> On Mon, Nov 17, 2025 at 04:27:59PM +0000, Phillip Wood wrote:
>> Hi Patrick
>> On 27/10/2025 11:33, Patrick Steinhardt wrote:
>>
>>> +static int collect_commits(struct repository *repo,
>>> + struct commit *old_commit,
>>> + struct commit *new_commit,
>>> + struct strvec *out)
>>
>> Now that we're not using the sequencer it would be nice to stop messing
>> about converting object ids to and from strings and return an array of
>> "struct commit" instead of "struct strvec"
>
> I was trying to avoid using a strvec, but honestly that turned out to be
> more pain than it is worth. We don't have functions like
> `strvec_splice()` for simple arrays, and there is no commit array struct
> that provides similar wrappers, either.
I'm surprised it was such a pain compared to the cost to using a strvec.
We're forever converting from a string to a struct commit and back again
which bloats the code and obscures the interesting and important parts.
That cost will be paid each time we add a new subcommand. An
implementation of 'struct commit_vec' that implements commit_vec_push(),
commit_vec_splice() and commit_vec_clear() is only going to be 30 or 40
lines of code and gives us a solid foundation for this series. Open
coding the array and adding a SPLICE_ARRAY macro would also be pretty
simple.
Thanks
Phillip
>> > +{
>>> + struct setup_revision_opt revision_opts = {
>>> + .assume_dashdash = 1,
>>> + };
>>> + struct strvec revisions = STRVEC_INIT;
>>> + struct commit *child;
>>> + struct rev_info rev = { 0 };
>>> + int ret;
>>> +
>>> + repo_init_revisions(repo, &rev, NULL);
>>> + strvec_push(&revisions, "");
>>> + strvec_push(&revisions, oid_to_hex(&new_commit->object.oid));
>>> + if (old_commit)
>>> + strvec_pushf(&revisions, "^%s", oid_to_hex(&old_commit->object.oid));
>>> +
>>> + setup_revisions_from_strvec(&revisions, &rev, &revision_opts);
>>> + if (revisions.nr != 1 || prepare_revision_walk(&rev)) {
>>
>> I'm not that familiar with the revision walking api, what 'revisions.nr !=
>> 1' check for here?
>
> It's basically a check that the revision arguments have all been
> consumed, except for the initial empty argument. The interface is a bit
> weird.
>
> [snip]
>>> + if (!onto) {
>>> + onto = commit;
>>> + } else {
>>> + struct tree *tree = repo_get_commit_tree(repo, commit);
>>> + onto = replay_create_commit(repo, tree, commit, onto);
>>> + if (!onto)
>>> + break;
>>
>> Don't we want to avoid updating HEAD if replay_create_commit() fails?
>
> Good point, yes.
>
>>> + }
>>> + }
>>> +
>>> + reset_opts.oid = &onto->object.oid;
>>> + strbuf_addf(&buf, "%s: switch to rewritten %s", action, oid_to_hex(reset_opts.oid));
>>
>> We're not switching branches so I wonder if saying "history: <action> <oid>
>> <commit subject>" might be a more useful reflog entry
>
> We're not switching branches, true, but we do switch to the rewritten
> commit. Also I'm not sure that printing the commit subject here would
> make sense, as the question becomes which subject to print: the one
> we're moving to, which is the new tip of the branch but may not be the
> commit we have rewritten? Or do we print the subject of the rewritten
> commit?
>
>>> +static int fill_commit_message(struct repository *repo,
>>> + const struct object_id *old_tree,
>>> + const struct object_id *new_tree,
>>> + const char *default_message,
>>> + const char *action,
>>> + struct strbuf *out)
>>> +{
>>> + const char *path = git_path_commit_editmsg();
>>> + const char *hint =
>>> + _("Please enter the commit message for the %s changes."
>>
>> Maybe "Please edit the commit message"? Also do we want to tell the user
>> they can abort by clearing the commit message?
>
> The "Please edit the commit message" thing is taken from other commands
> that phrase it similarly. But it certainly does make sense to note that
> clearing the commit message aborts, will add.
>
>>> + " Lines starting\nwith '%s' will be ignored.\n");
>>> + struct wt_status s;
>>> +
>>> + strbuf_addstr(out, default_message);
>>> + strbuf_addch(out, '\n');
>>> + strbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);
>>> + write_file_buf(path, out->buf, out->len);
>>> +
>>> + wt_status_prepare(repo, &s);
>>> + FREE_AND_NULL(s.branch);
>>> + s.ahead_behind_flags = AHEAD_BEHIND_QUICK;
>>> + s.commit_template = 1;
>>> + s.colopts = 0;
>>> + s.display_comment_prefix = 1;
>>> + s.hints = 0;
>>> + s.use_color = 0;
>>> + s.whence = FROM_COMMIT;
>>> + s.committable = 1;
>>
>> "git commit" reads a load of status related config settings, is any of that
>> relevant here?
>
> Yeah, some of it is. We don't handle them all yet, but this will be
> backfilled in the future.
>
>>> + s.fp = fopen(git_path_commit_editmsg(), "a");
>>> + if (!s.fp)
>>> + return error_errno(_("could not open '%s'"), git_path_commit_editmsg());
>>> +
>>> + wt_status_collect_changes_trees(&s, old_tree, new_tree);
>>> + wt_status_print(&s);
>>> + wt_status_collect_free_buffers(&s);
>>> + string_list_clear_func(&s.change, change_data_free);
>>> +
>>> + strbuf_reset(out);
>>> + if (launch_editor(path, out, NULL)) {
>>> + fprintf(stderr, _("Please supply the message using the -m option.\n"));
>>
>> I'm not sure that's a very helpful suggestion as we don't support "-m" (it's
>> not very helpful when "git commit --amend" suggests it either). We should
>> just give up if the editor fails.
>
> Ah, this is a leftover error message from previous iterations.
>
>>> +static int cmd_history_reword(int argc,
>>> + const char **argv,
>>> + const char *prefix,
>>> + struct repository *repo)
>>> +{
>>> + const char * const usage[] = {
>>> + GIT_HISTORY_REWORD_USAGE,
>>> + NULL,
>>> + };
>>> + struct option options[] = {
>>> + OPT_END(),
>>> + };
>>> + struct strbuf final_message = STRBUF_INIT;
>>> + struct commit *original_commit, *parent, *head;
>>> + struct strvec commits = STRVEC_INIT;
>>> + struct object_id parent_tree_oid, original_commit_tree_oid;
>>> + struct object_id rewritten_commit;
>>> + struct commit_list *from_list = NULL;
>>> + const char *original_message, *original_body, *ptr;
>>> + char *original_author = NULL;
>>> + size_t len;
>>> + int ret;
>>> +
>>> + argc = parse_options(argc, argv, prefix, options, usage, 0);
>>> + if (argc != 1) {
>>> + ret = error(_("command expects a single revision"));
>>> + goto out;
>>> + }
>>> + repo_config(repo, git_default_config, NULL);
>>> +
>>> + original_commit = lookup_commit_reference_by_name(argv[0]);
>>> + if (!original_commit) {
>>> + ret = error(_("commit to be reworded cannot be found: %s"), argv[0]);
>>> + goto out;
>>> + }
>>> + original_commit_tree_oid = repo_get_commit_tree(repo, original_commit)->object.oid;
>>
>> Looking at the implementation of repo_get_commit_tree() it can return NULL
>
>>> diff --git a/t/t3450-history.sh b/t/t3450-history.sh
>>> index 417c343d43b..f513463b92b 100755
>>> --- a/t/t3450-history.sh
>>> +++ b/t/t3450-history.sh
>>> @@ -5,13 +5,13 @@ test_description='tests for git-history command'
>>> . ./test-lib.sh
>>> test_expect_success 'does nothing without any arguments' '
>>> - git history >out 2>&1 &&
>>> - test_must_be_empty out
>>> + test_must_fail git history 2>err &&
>>> + test_grep "need a subcommand" err
>>> '
>>> test_expect_success 'raises an error with unknown argument' '
>>> test_must_fail git history garbage 2>err &&
>>> - test_grep "unrecognized argument: garbage" err
>>> + test_grep "unknown subcommand: .garbage." err
>>> '
>>> test_done
>>
>> Do we really need a separate test file just for a couple of tests. I can see
>> that having a separate test file for each subcommand makes sense but can't
>> we just add these two tests to one of those?
>
> I felt it was dirty to randomly add it to any of the other test suites,
> so I decided to instead have it in its own standalone file. It may also
> become relevant in the future if we ever needed commands like for
> example `git history --continue`, same as the sequencer-based commands
> have.
>
>>> diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
>>> new file mode 100755
>>> index 00000000000..09dbc463c59
>>> --- /dev/null
>>> +++ b/t/t3451-history-reword.sh
>>> @@ -0,0 +1,237 @@
>>> +#!/bin/sh
>>> +
>>> +test_description='tests for git-history reword subcommand'
>>> +
>>> +. ./test-lib.sh
>>> +
>>> +reword_with_message () {
>>> + cat >message &&
>>> + write_script fake-editor.sh <<-EOF &&
>>> + cp "$(pwd)/message" "\$1"
>>
>> Let's hope $(pwd) doesn't contain any dollar signs, backticks, backslashes
>> or double quotes. Doing
>>
>> export MSG_PATH="$(pwd)/message"
>> write_script fake-editor.sh <<-\EOF &&
>> cp "$MSG_PATH" "$1"
>> EOF
>>
>> would be safer
>
> True. Will use this:
>
> diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
> index 8b353e74dc..4c87953176 100755
> --- a/t/t3451-history-reword.sh
> +++ b/t/t3451-history-reword.sh
> @@ -6,11 +6,11 @@ test_description='tests for git-history reword subcommand'
>
> reword_with_message () {
> cat >message &&
> - write_script fake-editor.sh <<-EOF &&
> - cp "$(pwd)/message" "\$1"
> + write_script fake-editor.sh <<-\EOF &&
> + cp "$ORIG_PATH/message" "$1"
> EOF
> test_set_editor "$(pwd)"/fake-editor.sh &&
> - git history reword "$@" &&
> + ORIG_PATH="$(pwd)" git history reword "$@" &&
> rm fake-editor.sh message
> }
>
>
>>> +test_expect_success 'refuses to work with merge commits' '
>>> + test_when_finished "rm -rf repo" &&
>>> + git init repo &&
>>> + (
>>
>> Do we really need to set up a separate repo for each test? The test suite is
>> slow enough already without running "git init" followed by a bunch calls to
>> test_commit() in each test. Can we instead run "git reset --hard
>> <known-starting-point> at the beginning of each test? That removes any
>> interdependence between tests but saves a bunch of processes.
>
> I prefer that style as it is extremely hard to reason about tests that
> have interdependencies, and not all the state may be removed by a hard
> reset.
>
>>> + test_when_finished "rm -rf repo" &&
>>> + git init repo &&
>>> + (
>>> + cd repo &&
>>> + test_commit first &&
>>> +
>>> + write_script fake-editor.sh <<-\EOF &&
>>> + cp "$1" . &&
>>> + printf "\namend a comment\n" >>"$1"
>>> + EOF
>>> + test_set_editor "$(pwd)"/fake-editor.sh &&
>>> + git history reword HEAD &&
>>> +
>>> + cat >expect <<-EOF &&
>>> + first
>>> +
>>> + # Please enter the commit message for the reworded changes. Lines starting
>>> + # with ${SQ}#${SQ} will be ignored.
>>> + # Changes to be committed:
>>> + # new file: first.t
>>> + #
>>> + EOF
>>> + test_cmp expect COMMIT_EDITMSG &&
>>> +
>>> + cat >expect <<-EOF &&
>>> + first
>>> +
>>> + amend a comment
>>> +
>>> + EOF
>>> + git log --format=%B >actual &&
>>> + test_cmp expect actual
>>
>> We have test_commit_message() to do this which will accept the expected
>> message on stdin.
>
> Ah, indeed.
>
>>> + )
>>> +'
>>> +
>>> +# For now, git-history(1) does not yet execute any hooks. This is subject to
>>> +# change in the future, and if it does this test here is expected to start
>>> +# failing. In other words, this test is not an endorsement of the current
>>> +# status quo.
>>> +test_expect_success 'hooks are not executed for rewritten commits' '
>>> + test_when_finished "rm -rf repo" &&
>>> + git init repo &&
>>> + (
>>> + cd repo &&
>>> + test_commit first &&
>>> + test_commit second &&
>>> + test_commit third &&
>>> +
>>> + write_script .git/hooks/prepare-commit-msg <<-EOF &&
>>> + touch "$(pwd)/hooks.log
>>
>> This has the same problem of expanding $(pwd) as fake-editor.sh. For
>> debugging it would be nicer if the hook scripts did
>>
>> echo "$hook_name" >>hooks.log
>>
>> so we can easily see which hooks are causing the test to fail.
>
> I'll rephrain from doing this as it would require `<<-EOF` instead of
> `<<-\EOF`.
>
>>> + EOF
>>> + write_script .git/hooks/post-commit <<-EOF &&
>>> + touch "$(pwd)/hooks.log
>>> + EOF
>>> + write_script .git/hooks/post-rewrite <<-EOF &&
>>> + touch "$(pwd)/hooks.log
>>> + EOF
>>
>> This is good idea. We should add tests for the "pre-commit" and "commit-msg"
>> hooks as well.
>>
>> Overall the test coverage looks good, the only thing we might want to add is
>> a check for the reflog message. Thanks for working on this, I'll try and
>> look at the rest of the patches sometime this week.
>
> Thanks!
>
> Patrick
>
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v6 05/11] builtin/history: implement "reword" subcommand
2025-10-27 11:33 ` [PATCH v6 05/11] builtin/history: implement "reword" subcommand Patrick Steinhardt
2025-11-17 16:27 ` Phillip Wood
@ 2025-11-20 7:03 ` Elijah Newren
2025-12-02 18:50 ` Patrick Steinhardt
2025-11-25 8:31 ` SZEDER Gábor
2 siblings, 1 reply; 278+ messages in thread
From: Elijah Newren @ 2025-11-20 7:03 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Karthik Nayak
Phillip responded in good detail, but I wanted to comment on a few
additional things...
On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:
> +static int collect_commits(struct repository *repo,
> + struct commit *old_commit,
> + struct commit *new_commit,
> + struct strvec *out)
> +{
> + struct setup_revision_opt revision_opts = {
> + .assume_dashdash = 1,
> + };
> + struct strvec revisions = STRVEC_INIT;
> + struct commit *child;
> + struct rev_info rev = { 0 };
> + int ret;
> +
> + repo_init_revisions(repo, &rev, NULL);
> + strvec_push(&revisions, "");
> + strvec_push(&revisions, oid_to_hex(&new_commit->object.oid));
> + if (old_commit)
> + strvec_pushf(&revisions, "^%s", oid_to_hex(&old_commit->object.oid));
> +
> + setup_revisions_from_strvec(&revisions, &rev, &revision_opts);
> + if (revisions.nr != 1 || prepare_revision_walk(&rev)) {
> + ret = error(_("revision walk setup failed"));
> + goto out;
> + }
Don't we want to restrict the revision walk to descendants of
old_commit (which can be done with `--ancestry-path`)?
> +
> + while ((child = get_revision(&rev))) {
> + if (old_commit && !child->parents)
> + BUG("revision walk did not find child commit");
> + if (child->parents && child->parents->next) {
> + ret = error(_("cannot rearrange commit history with merges"));
> + goto out;
> + }
> +
> + strvec_push(out, oid_to_hex(&child->object.oid));
> +
> + if (child->parents && old_commit &&
> + commit_list_contains(old_commit, child->parents))
> + break;
Is this last if-check basically a workaround to not providing
--ancestry-path to the revision walk? And won't it sometimes still
get non-descendants of old_commit before reaching old_commit? Or, I
guess that's not an issue since you error out when you hit merges, but
once replay supports merges, there's more logic that needs changing
than one expects with the way this is coded.
> + }
> +
> + /*
> + * Revisions are in newest-order-first. We have to reverse the
> + * array though so that we pick the oldest commits first.
> + */
> + for (size_t i = 0, j = out->nr - 1; i < j; i++, j--)
> + SWAP(out->v[i], out->v[j]);
Setting rev.reverse would obviate the need for this...
> +
> + ret = 0;
> +
> +out:
> + strvec_clear(&revisions);
> + release_revisions(&rev);
> + reset_revision_walk();
> + return ret;
> +}
You've pulled out some functions from builtin/replay, but you've
decided to hand re-roll all the revision walking. Is that because you
first implemented on top of sequencer, and then transliterated to
replay? If so, I think we could restructure this; I think what you
need is:
* Create a new commit with an altered commit message.
* Invoking whatever function(s) would be invoked by "git replay
--onto ${NEW_COMMIT_ID} --ancestry-path ^${OLD_COMMIT_ID} --branches"
(or as a first cut, even shelling out to that subprocess).
The first bullet point would be your fill_commit_message().
The second bullet point would allow you to perhaps drop your
collect_commits(), replace_commits(), and apply_commits(), which feel
like they are just re-implementing replay logic, and replace them with
something like:
void replay_descendants(struct repository *repo,
const struct object_id *prev_head,
const struct object_id *new_head)
{
struct strvec args = STRVEC_INIT;
strvec_pushl(&args, "replay", "--onto", NULL);
strvec_push(&args, oid_to_hex(new_head));
strvec_push(&args, "--ancestry-path");
strvec_pushf(&args, "^%s", oid_to_hex(prev_head));
strvec_push(&args, "--branches");
reset_revision_walk();
cmd_replay(args.nr, args.v, NULL, repo);
}
...although maybe it's a little ugly to invoke cmd_replay() this way
and maybe we want to restructure that out.
But, I am really late in providing my review, so if you want to go
forward with your existing three functions and then perhaps we
restructure later, that's fine too. The command is experimental,
after all.
> + head = lookup_commit_reference_by_name("HEAD");
> + if (!head) {
> + ret = error(_("could not resolve HEAD to a commit"));
> + goto out;
> + }
> +
> + commit_list_append(original_commit, &from_list);
> + if (!repo_is_descendant_of(repo, head, from_list)) {
> + ret = error (_("split commit must be reachable from current HEAD commit"));
> + goto out;
> + }
Why should it be required to be reachable from HEAD? Shouldn't it be
possible to reword a commit from another branch?
Also, what about when a commit is reachable from both HEAD and other
branches? I know you started by basing on git-rebase, and git-rebase
restricts things to just one branch, but that was perhaps its biggest
design flaw that couldn't be backward compatibly fixed without
creating a new command. I'd rather avoid copying that flaw. (Maybe
the user needs an error by default if more than one branch is
affected, or they need to provide an additional flag to rewrite
multiple branches, but only rewriting one branch when more than one is
affected is just wrong to me unless the user explicitly specifies
that's what they want.)
> + /* We retain authorship of the original commit. */
> + original_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);
> + ptr = find_commit_header(original_message, "author", &len);
> + if (ptr)
> + original_author = xmemdupz(ptr, len);
> + find_commit_subject(original_message, &original_body);
> +
> + ret = fill_commit_message(repo, &parent_tree_oid, &original_commit_tree_oid,
> + original_body, "reworded", &final_message);
> + if (ret < 0)
> + goto out;
> +
> + ret = commit_tree(final_message.buf, final_message.len, &original_commit_tree_oid,
> + original_commit->parents, &rewritten_commit, original_author, NULL);
Does the use of commit_tree() instead of commit_tree_extended() mean
you discard additional headers on the reworded commit, such as
encoding?
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH v6 05/11] builtin/history: implement "reword" subcommand
2025-11-20 7:03 ` Elijah Newren
@ 2025-12-02 18:50 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-12-02 18:50 UTC (permalink / raw)
To: Elijah Newren
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Karthik Nayak
On Wed, Nov 19, 2025 at 11:03:20PM -0800, Elijah Newren wrote:
> Phillip responded in good detail, but I wanted to comment on a few
> additional things...
>
> On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:
>
> > +static int collect_commits(struct repository *repo,
> > + struct commit *old_commit,
> > + struct commit *new_commit,
> > + struct strvec *out)
> > +{
> > + struct setup_revision_opt revision_opts = {
> > + .assume_dashdash = 1,
> > + };
> > + struct strvec revisions = STRVEC_INIT;
> > + struct commit *child;
> > + struct rev_info rev = { 0 };
> > + int ret;
> > +
> > + repo_init_revisions(repo, &rev, NULL);
> > + strvec_push(&revisions, "");
> > + strvec_push(&revisions, oid_to_hex(&new_commit->object.oid));
> > + if (old_commit)
> > + strvec_pushf(&revisions, "^%s", oid_to_hex(&old_commit->object.oid));
> > +
> > + setup_revisions_from_strvec(&revisions, &rev, &revision_opts);
> > + if (revisions.nr != 1 || prepare_revision_walk(&rev)) {
> > + ret = error(_("revision walk setup failed"));
> > + goto out;
> > + }
>
> Don't we want to restrict the revision walk to descendants of
> old_commit (which can be done with `--ancestry-path`)?
We verify that both commits have direct ancestry and that there are no
merge commits in the history, so this shouldn't be needed. But indeed,
this makes the logic a bit easier to reason about.
> > +
> > + while ((child = get_revision(&rev))) {
> > + if (old_commit && !child->parents)
> > + BUG("revision walk did not find child commit");
> > + if (child->parents && child->parents->next) {
> > + ret = error(_("cannot rearrange commit history with merges"));
> > + goto out;
> > + }
> > +
> > + strvec_push(out, oid_to_hex(&child->object.oid));
> > +
> > + if (child->parents && old_commit &&
> > + commit_list_contains(old_commit, child->parents))
> > + break;
>
> Is this last if-check basically a workaround to not providing
> --ancestry-path to the revision walk? And won't it sometimes still
> get non-descendants of old_commit before reaching old_commit? Or, I
> guess that's not an issue since you error out when you hit merges, but
> once replay supports merges, there's more logic that needs changing
> than one expects with the way this is coded.
Yeah, this is not currently an issue as we explicitly rule out merges.
Anyway, I'm using the flag now, so this isn't needed anymore.
> > + }
> > +
> > + /*
> > + * Revisions are in newest-order-first. We have to reverse the
> > + * array though so that we pick the oldest commits first.
> > + */
> > + for (size_t i = 0, j = out->nr - 1; i < j; i++, j--)
> > + SWAP(out->v[i], out->v[j]);
>
> Setting rev.reverse would obviate the need for this...
Yup, true. I couldn't use 'reverse' before due to the way the loop was
handled.
> > +
> > + ret = 0;
> > +
> > +out:
> > + strvec_clear(&revisions);
> > + release_revisions(&rev);
> > + reset_revision_walk();
> > + return ret;
> > +}
>
> You've pulled out some functions from builtin/replay, but you've
> decided to hand re-roll all the revision walking. Is that because you
> first implemented on top of sequencer, and then transliterated to
> replay? If so, I think we could restructure this; I think what you
> need is:
> * Create a new commit with an altered commit message.
> * Invoking whatever function(s) would be invoked by "git replay
> --onto ${NEW_COMMIT_ID} --ancestry-path ^${OLD_COMMIT_ID} --branches"
> (or as a first cut, even shelling out to that subprocess).
>
> The first bullet point would be your fill_commit_message().
>
> The second bullet point would allow you to perhaps drop your
> collect_commits(), replace_commits(), and apply_commits(), which feel
> like they are just re-implementing replay logic, and replace them with
> something like:
>
> void replay_descendants(struct repository *repo,
> const struct object_id *prev_head,
> const struct object_id *new_head)
> {
> struct strvec args = STRVEC_INIT;
>
> strvec_pushl(&args, "replay", "--onto", NULL);
> strvec_push(&args, oid_to_hex(new_head));
> strvec_push(&args, "--ancestry-path");
> strvec_pushf(&args, "^%s", oid_to_hex(prev_head));
> strvec_push(&args, "--branches");
>
> reset_revision_walk();
> cmd_replay(args.nr, args.v, NULL, repo);
> }
>
> ...although maybe it's a little ugly to invoke cmd_replay() this way
> and maybe we want to restructure that out.
>
> But, I am really late in providing my review, so if you want to go
> forward with your existing three functions and then perhaps we
> restructure later, that's fine too. The command is experimental,
> after all.
I guess it's a combination of the transliteration and that I couldn't
figure out how to easily do some things without shelling out. I plan on
introducing features eventually that also allow for example to reorder
commits, and I'm not clear that this is easy to do with the replay
infra.
So for now I think I'd like to retain the current infra. But I certainly
agree that we should revisit and see whether we can further refactor the
interfaces provided by "replay.c" to cover more cases without shelling
out.
> > + head = lookup_commit_reference_by_name("HEAD");
> > + if (!head) {
> > + ret = error(_("could not resolve HEAD to a commit"));
> > + goto out;
> > + }
> > +
> > + commit_list_append(original_commit, &from_list);
> > + if (!repo_is_descendant_of(repo, head, from_list)) {
> > + ret = error (_("split commit must be reachable from current HEAD commit"));
> > + goto out;
> > + }
>
> Why should it be required to be reachable from HEAD? Shouldn't it be
> possible to reword a commit from another branch?
>
> Also, what about when a commit is reachable from both HEAD and other
> branches? I know you started by basing on git-rebase, and git-rebase
> restricts things to just one branch, but that was perhaps its biggest
> design flaw that couldn't be backward compatibly fixed without
> creating a new command. I'd rather avoid copying that flaw. (Maybe
> the user needs an error by default if more than one branch is
> affected, or they need to provide an additional flag to rewrite
> multiple branches, but only rewriting one branch when more than one is
> affected is just wrong to me unless the user explicitly specifies
> that's what they want.)
For now we the commands really only care about a single branch, the case
where a commit exists on multiple branches is not considered. I'm not
really sure whether I'd call this a flaw -- I think it's as easy way to
think about the command for the user.
That being said, I certainly think that we can eventually introduce a
flag to alter the behaviour so that it considers multiple branches in
case the commit exists on more than one branch.
> > + /* We retain authorship of the original commit. */
> > + original_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);
> > + ptr = find_commit_header(original_message, "author", &len);
> > + if (ptr)
> > + original_author = xmemdupz(ptr, len);
> > + find_commit_subject(original_message, &original_body);
> > +
> > + ret = fill_commit_message(repo, &parent_tree_oid, &original_commit_tree_oid,
> > + original_body, "reworded", &final_message);
> > + if (ret < 0)
> > + goto out;
> > +
> > + ret = commit_tree(final_message.buf, final_message.len, &original_commit_tree_oid,
> > + original_commit->parents, &rewritten_commit, original_author, NULL);
>
> Does the use of commit_tree() instead of commit_tree_extended() mean
> you discard additional headers on the reworded commit, such as
> encoding?
Good point, let me fix this.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v6 05/11] builtin/history: implement "reword" subcommand
2025-10-27 11:33 ` [PATCH v6 05/11] builtin/history: implement "reword" subcommand Patrick Steinhardt
2025-11-17 16:27 ` Phillip Wood
2025-11-20 7:03 ` Elijah Newren
@ 2025-11-25 8:31 ` SZEDER Gábor
2025-12-02 18:50 ` Patrick Steinhardt
2 siblings, 1 reply; 278+ messages in thread
From: SZEDER Gábor @ 2025-11-25 8:31 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
On Mon, Oct 27, 2025 at 12:33:53PM +0100, Patrick Steinhardt wrote:
> Implement a new "reword" subcommand for git-history(1). This subcommand
> is essentially the same as if a user performed an interactive rebase
> with a single commit changed to use the "reword" verb.
s/verb/instruction/
The behavior is not nearly "essentially the same", because 'git
history reword' doesn't check out the reworded commit.
This is a substantial drawback when writing a commit message for
anything non-trivial. I, for one, often like to take a look at the
"big picture", i.e. the actual file content in the reworded commit, in
case the diff included in the commit message template doesn't show
enough context, or even run Git commands built from that particular
revision to be able to accurately describe its behavior.
OTOH, I understand that this might be deemed desirable in some cases,
like when rewording a commit to correct a simple typo, because source
file mtimes stay intact, or when the worktree contains modified files.
It would be great if this new command could somehow support both use
cases.
In any case, this significant behavior difference (and its drawbacks)
is not mentioned let alone justified in the commit message, it is not
documented anywhere, and it is not really tested, either.
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> Documentation/git-history.adoc | 7 +-
> builtin/history.c | 331 ++++++++++++++++++++++++++++++++++++++++-
> t/meson.build | 1 +
> t/t3450-history.sh | 6 +-
> t/t3451-history-reword.sh | 237 +++++++++++++++++++++++++++++
> 5 files changed, 573 insertions(+), 9 deletions(-)
>
> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
> index 6bdfeb50e8b..bd903875120 100644
> --- a/Documentation/git-history.adoc
> +++ b/Documentation/git-history.adoc
> @@ -8,7 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history of the current branch
> SYNOPSIS
> --------
> [synopsis]
> -git history [<options>]
> +git history reword <commit>
I'm not sure I like this interface, because I have to know in advance
how to specify the revision I want to reword, and I usually don't know
that. Choosing the commit from the rebase instruction sheet seems to
be much simpler, more intuitive and less error prone.
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v6 05/11] builtin/history: implement "reword" subcommand
2025-11-25 8:31 ` SZEDER Gábor
@ 2025-12-02 18:50 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-12-02 18:50 UTC (permalink / raw)
To: SZEDER Gábor
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
On Tue, Nov 25, 2025 at 09:31:24AM +0100, SZEDER Gábor wrote:
> On Mon, Oct 27, 2025 at 12:33:53PM +0100, Patrick Steinhardt wrote:
> > Implement a new "reword" subcommand for git-history(1). This subcommand
> > is essentially the same as if a user performed an interactive rebase
> > with a single commit changed to use the "reword" verb.
>
> s/verb/instruction/
>
> The behavior is not nearly "essentially the same", because 'git
> history reword' doesn't check out the reworded commit.
True, this is a leftover from before. I'll reword this.
> This is a substantial drawback when writing a commit message for
> anything non-trivial. I, for one, often like to take a look at the
> "big picture", i.e. the actual file content in the reworded commit, in
> case the diff included in the commit message template doesn't show
> enough context, or even run Git commands built from that particular
> revision to be able to accurately describe its behavior.
>
> OTOH, I understand that this might be deemed desirable in some cases,
> like when rewording a commit to correct a simple typo, because source
> file mtimes stay intact, or when the worktree contains modified files.
>
> It would be great if this new command could somehow support both use
> cases.
>
> In any case, this significant behavior difference (and its drawbacks)
> is not mentioned let alone justified in the commit message, it is not
> documented anywhere, and it is not really tested, either.
I guess it's a mixed bag. The big benefit of not having to check out the
commit is that it's as fast as it gets. All we need to do is to rewrite
commit history, and we don't need to check anything out. This has the
obvious benefit of being fast, but also the less-obvious benefit of
being able to deal with changes that exist in the working tree, only.
> > Signed-off-by: Patrick Steinhardt <ps@pks.im>
> > ---
> > Documentation/git-history.adoc | 7 +-
> > builtin/history.c | 331 ++++++++++++++++++++++++++++++++++++++++-
> > t/meson.build | 1 +
> > t/t3450-history.sh | 6 +-
> > t/t3451-history-reword.sh | 237 +++++++++++++++++++++++++++++
> > 5 files changed, 573 insertions(+), 9 deletions(-)
> >
> > diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
> > index 6bdfeb50e8b..bd903875120 100644
> > --- a/Documentation/git-history.adoc
> > +++ b/Documentation/git-history.adoc
> > @@ -8,7 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history of the current branch
> > SYNOPSIS
> > --------
> > [synopsis]
> > -git history [<options>]
> > +git history reword <commit>
>
> I'm not sure I like this interface, because I have to know in advance
> how to specify the revision I want to reword, and I usually don't know
> that. Choosing the commit from the rebase instruction sheet seems to
> be much simpler, more intuitive and less error prone.
Fair. For me I would very much prefer this new interface, but it's
certainly a matter of taste.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH v6 06/11] add-patch: split out header from "add-interactive.h"
2025-10-27 11:33 ` [PATCH v6 00/11] " Patrick Steinhardt
` (4 preceding siblings ...)
2025-10-27 11:33 ` [PATCH v6 05/11] builtin/history: implement "reword" subcommand Patrick Steinhardt
@ 2025-10-27 11:33 ` Patrick Steinhardt
2025-11-20 7:03 ` Elijah Newren
2025-10-27 11:33 ` [PATCH v6 07/11] add-patch: split out `struct interactive_options` Patrick Steinhardt
` (6 subsequent siblings)
12 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-27 11:33 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
While we have a "add-patch.c" code file, its declarations are part of
"add-interactive.h". This makes it somewhat harder than necessary to
find relevant code and to identify clear boundaries between the two
subsystems.
Split up concerns and move declarations that relate to "add-patch.c"
into a new "add-patch.h" header.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
add-interactive.h | 23 +++--------------------
add-patch.c | 1 +
add-patch.h | 26 ++++++++++++++++++++++++++
3 files changed, 30 insertions(+), 20 deletions(-)
diff --git a/add-interactive.h b/add-interactive.h
index da49502b765..2e3d1d871d2 100644
--- a/add-interactive.h
+++ b/add-interactive.h
@@ -1,14 +1,11 @@
#ifndef ADD_INTERACTIVE_H
#define ADD_INTERACTIVE_H
+#include "add-patch.h"
#include "color.h"
-struct add_p_opt {
- int context;
- int interhunkcontext;
-};
-
-#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }
+struct pathspec;
+struct repository;
struct add_i_state {
struct repository *r;
@@ -35,21 +32,7 @@ void init_add_i_state(struct add_i_state *s, struct repository *r,
struct add_p_opt *add_p_opt);
void clear_add_i_state(struct add_i_state *s);
-struct repository;
-struct pathspec;
int run_add_i(struct repository *r, const struct pathspec *ps,
struct add_p_opt *add_p_opt);
-enum add_p_mode {
- ADD_P_ADD,
- ADD_P_STASH,
- ADD_P_RESET,
- ADD_P_CHECKOUT,
- ADD_P_WORKTREE,
-};
-
-int run_add_p(struct repository *r, enum add_p_mode mode,
- struct add_p_opt *o, const char *revision,
- const struct pathspec *ps);
-
#endif
diff --git a/add-patch.c b/add-patch.c
index ae9a20d8f23..3594dd22534 100644
--- a/add-patch.c
+++ b/add-patch.c
@@ -3,6 +3,7 @@
#include "git-compat-util.h"
#include "add-interactive.h"
+#include "add-patch.h"
#include "advice.h"
#include "editor.h"
#include "environment.h"
diff --git a/add-patch.h b/add-patch.h
new file mode 100644
index 00000000000..4394c741076
--- /dev/null
+++ b/add-patch.h
@@ -0,0 +1,26 @@
+#ifndef ADD_PATCH_H
+#define ADD_PATCH_H
+
+struct pathspec;
+struct repository;
+
+struct add_p_opt {
+ int context;
+ int interhunkcontext;
+};
+
+#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }
+
+enum add_p_mode {
+ ADD_P_ADD,
+ ADD_P_STASH,
+ ADD_P_RESET,
+ ADD_P_CHECKOUT,
+ ADD_P_WORKTREE,
+};
+
+int run_add_p(struct repository *r, enum add_p_mode mode,
+ struct add_p_opt *o, const char *revision,
+ const struct pathspec *ps);
+
+#endif
--
2.51.1.930.gacf6e81ea2.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH v6 06/11] add-patch: split out header from "add-interactive.h"
2025-10-27 11:33 ` [PATCH v6 06/11] add-patch: split out header from "add-interactive.h" Patrick Steinhardt
@ 2025-11-20 7:03 ` Elijah Newren
0 siblings, 0 replies; 278+ messages in thread
From: Elijah Newren @ 2025-11-20 7:03 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Karthik Nayak
On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:
>
> While we have a "add-patch.c" code file, its declarations are part of
> "add-interactive.h". This makes it somewhat harder than necessary to
> find relevant code and to identify clear boundaries between the two
> subsystems.
>
> Split up concerns and move declarations that relate to "add-patch.c"
> into a new "add-patch.h" header.
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> add-interactive.h | 23 +++--------------------
> add-patch.c | 1 +
> add-patch.h | 26 ++++++++++++++++++++++++++
> 3 files changed, 30 insertions(+), 20 deletions(-)
>
> diff --git a/add-interactive.h b/add-interactive.h
> index da49502b765..2e3d1d871d2 100644
> --- a/add-interactive.h
> +++ b/add-interactive.h
> @@ -1,14 +1,11 @@
> #ifndef ADD_INTERACTIVE_H
> #define ADD_INTERACTIVE_H
>
> +#include "add-patch.h"
> #include "color.h"
>
> -struct add_p_opt {
> - int context;
> - int interhunkcontext;
> -};
> -
> -#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }
> +struct pathspec;
> +struct repository;
>
> struct add_i_state {
> struct repository *r;
> @@ -35,21 +32,7 @@ void init_add_i_state(struct add_i_state *s, struct repository *r,
> struct add_p_opt *add_p_opt);
> void clear_add_i_state(struct add_i_state *s);
>
> -struct repository;
> -struct pathspec;
> int run_add_i(struct repository *r, const struct pathspec *ps,
> struct add_p_opt *add_p_opt);
>
> -enum add_p_mode {
> - ADD_P_ADD,
> - ADD_P_STASH,
> - ADD_P_RESET,
> - ADD_P_CHECKOUT,
> - ADD_P_WORKTREE,
> -};
> -
> -int run_add_p(struct repository *r, enum add_p_mode mode,
> - struct add_p_opt *o, const char *revision,
> - const struct pathspec *ps);
> -
> #endif
> diff --git a/add-patch.c b/add-patch.c
> index ae9a20d8f23..3594dd22534 100644
> --- a/add-patch.c
> +++ b/add-patch.c
> @@ -3,6 +3,7 @@
>
> #include "git-compat-util.h"
> #include "add-interactive.h"
> +#include "add-patch.h"
> #include "advice.h"
> #include "editor.h"
> #include "environment.h"
> diff --git a/add-patch.h b/add-patch.h
> new file mode 100644
> index 00000000000..4394c741076
> --- /dev/null
> +++ b/add-patch.h
> @@ -0,0 +1,26 @@
> +#ifndef ADD_PATCH_H
> +#define ADD_PATCH_H
> +
> +struct pathspec;
> +struct repository;
> +
> +struct add_p_opt {
> + int context;
> + int interhunkcontext;
> +};
> +
> +#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }
> +
> +enum add_p_mode {
> + ADD_P_ADD,
> + ADD_P_STASH,
> + ADD_P_RESET,
> + ADD_P_CHECKOUT,
> + ADD_P_WORKTREE,
> +};
> +
> +int run_add_p(struct repository *r, enum add_p_mode mode,
> + struct add_p_opt *o, const char *revision,
> + const struct pathspec *ps);
> +
> +#endif
>
> --
> 2.51.1.930.gacf6e81ea2.dirty
Simple enough.
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH v6 07/11] add-patch: split out `struct interactive_options`
2025-10-27 11:33 ` [PATCH v6 00/11] " Patrick Steinhardt
` (5 preceding siblings ...)
2025-10-27 11:33 ` [PATCH v6 06/11] add-patch: split out header from "add-interactive.h" Patrick Steinhardt
@ 2025-10-27 11:33 ` Patrick Steinhardt
2025-11-20 7:03 ` Elijah Newren
2025-11-20 15:05 ` Phillip Wood
2025-10-27 11:33 ` [PATCH v6 08/11] add-patch: remove dependency on "add-interactive" subsystem Patrick Steinhardt
` (5 subsequent siblings)
12 siblings, 2 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-27 11:33 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
The `struct add_p_opt` is reused both by our infra for "git add -p" and
"git add -i". Users of `run_add_i()` for example are expected to pass
`struct add_p_opt`. This is somewhat confusing and raises the question
of which options apply to what part of the stack.
But things are even more confusing than that: while callers are expected
to pass in `struct add_p_opt`, these options ultimately get used to
initialize a `struct add_i_state` that is used by both subsystems. So we
are basically going full circle here.
Refactor the code and split out a new `struct interactive_options` that
hosts common options used by both. These options are then applied to a
`struct interactive_config` that hosts common configuration.
This refactoring doesn't yet fully detangle the two subsystems from one
another, as we still end up calling `init_add_i_state()` in the "git add
-p" subsystem. This will be fixed in a subsequent commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
add-interactive.c | 174 +++++++++++------------------------------------------
add-interactive.h | 23 +------
add-patch.c | 170 +++++++++++++++++++++++++++++++++++++++++++--------
add-patch.h | 36 ++++++++++-
builtin/add.c | 22 +++----
builtin/checkout.c | 4 +-
builtin/commit.c | 16 ++---
builtin/reset.c | 16 ++---
builtin/stash.c | 46 +++++++-------
commit.h | 2 +-
10 files changed, 270 insertions(+), 239 deletions(-)
diff --git a/add-interactive.c b/add-interactive.c
index 68fc09547dd..05d2e7eefe3 100644
--- a/add-interactive.c
+++ b/add-interactive.c
@@ -3,7 +3,6 @@
#include "git-compat-util.h"
#include "add-interactive.h"
#include "color.h"
-#include "config.h"
#include "diffcore.h"
#include "gettext.h"
#include "hash.h"
@@ -20,119 +19,18 @@
#include "prompt.h"
#include "tree.h"
-static void init_color(struct repository *r, enum git_colorbool use_color,
- const char *section_and_slot, char *dst,
- const char *default_color)
-{
- char *key = xstrfmt("color.%s", section_and_slot);
- const char *value;
-
- if (!want_color(use_color))
- dst[0] = '\0';
- else if (repo_config_get_value(r, key, &value) ||
- color_parse(value, dst))
- strlcpy(dst, default_color, COLOR_MAXLEN);
-
- free(key);
-}
-
-static enum git_colorbool check_color_config(struct repository *r, const char *var)
-{
- const char *value;
- enum git_colorbool ret;
-
- if (repo_config_get_value(r, var, &value))
- ret = GIT_COLOR_UNKNOWN;
- else
- ret = git_config_colorbool(var, value);
-
- /*
- * Do not rely on want_color() to fall back to color.ui for us. It uses
- * the value parsed by git_color_config(), which may not have been
- * called by the main command.
- */
- if (ret == GIT_COLOR_UNKNOWN &&
- !repo_config_get_value(r, "color.ui", &value))
- ret = git_config_colorbool("color.ui", value);
-
- return ret;
-}
-
void init_add_i_state(struct add_i_state *s, struct repository *r,
- struct add_p_opt *add_p_opt)
+ struct interactive_options *opts)
{
s->r = r;
- s->context = -1;
- s->interhunkcontext = -1;
-
- s->use_color_interactive = check_color_config(r, "color.interactive");
-
- init_color(r, s->use_color_interactive, "interactive.header",
- s->header_color, GIT_COLOR_BOLD);
- init_color(r, s->use_color_interactive, "interactive.help",
- s->help_color, GIT_COLOR_BOLD_RED);
- init_color(r, s->use_color_interactive, "interactive.prompt",
- s->prompt_color, GIT_COLOR_BOLD_BLUE);
- init_color(r, s->use_color_interactive, "interactive.error",
- s->error_color, GIT_COLOR_BOLD_RED);
- strlcpy(s->reset_color_interactive,
- want_color(s->use_color_interactive) ? GIT_COLOR_RESET : "", COLOR_MAXLEN);
-
- s->use_color_diff = check_color_config(r, "color.diff");
-
- init_color(r, s->use_color_diff, "diff.frag", s->fraginfo_color,
- diff_get_color(s->use_color_diff, DIFF_FRAGINFO));
- init_color(r, s->use_color_diff, "diff.context", s->context_color,
- "fall back");
- if (!strcmp(s->context_color, "fall back"))
- init_color(r, s->use_color_diff, "diff.plain",
- s->context_color,
- diff_get_color(s->use_color_diff, DIFF_CONTEXT));
- init_color(r, s->use_color_diff, "diff.old", s->file_old_color,
- diff_get_color(s->use_color_diff, DIFF_FILE_OLD));
- init_color(r, s->use_color_diff, "diff.new", s->file_new_color,
- diff_get_color(s->use_color_diff, DIFF_FILE_NEW));
- strlcpy(s->reset_color_diff,
- want_color(s->use_color_diff) ? GIT_COLOR_RESET : "", COLOR_MAXLEN);
-
- FREE_AND_NULL(s->interactive_diff_filter);
- repo_config_get_string(r, "interactive.difffilter",
- &s->interactive_diff_filter);
-
- FREE_AND_NULL(s->interactive_diff_algorithm);
- repo_config_get_string(r, "diff.algorithm",
- &s->interactive_diff_algorithm);
-
- if (!repo_config_get_int(r, "diff.context", &s->context))
- if (s->context < 0)
- die(_("%s cannot be negative"), "diff.context");
- if (!repo_config_get_int(r, "diff.interHunkContext", &s->interhunkcontext))
- if (s->interhunkcontext < 0)
- die(_("%s cannot be negative"), "diff.interHunkContext");
-
- repo_config_get_bool(r, "interactive.singlekey", &s->use_single_key);
- if (s->use_single_key)
- setbuf(stdin, NULL);
-
- if (add_p_opt->context != -1) {
- if (add_p_opt->context < 0)
- die(_("%s cannot be negative"), "--unified");
- s->context = add_p_opt->context;
- }
- if (add_p_opt->interhunkcontext != -1) {
- if (add_p_opt->interhunkcontext < 0)
- die(_("%s cannot be negative"), "--inter-hunk-context");
- s->interhunkcontext = add_p_opt->interhunkcontext;
- }
+ interactive_config_init(&s->cfg, r, opts);
}
void clear_add_i_state(struct add_i_state *s)
{
- FREE_AND_NULL(s->interactive_diff_filter);
- FREE_AND_NULL(s->interactive_diff_algorithm);
+ interactive_config_clear(&s->cfg);
memset(s, 0, sizeof(*s));
- s->use_color_interactive = GIT_COLOR_UNKNOWN;
- s->use_color_diff = GIT_COLOR_UNKNOWN;
+ interactive_config_clear(&s->cfg);
}
/*
@@ -286,7 +184,7 @@ static void list(struct add_i_state *s, struct string_list *list, int *selected,
return;
if (opts->header)
- color_fprintf_ln(stdout, s->header_color,
+ color_fprintf_ln(stdout, s->cfg.header_color,
"%s", opts->header);
for (i = 0; i < list->nr; i++) {
@@ -354,7 +252,7 @@ static ssize_t list_and_choose(struct add_i_state *s,
list(s, &items->items, items->selected, &opts->list_opts);
- color_fprintf(stdout, s->prompt_color, "%s", opts->prompt);
+ color_fprintf(stdout, s->cfg.prompt_color, "%s", opts->prompt);
fputs(singleton ? "> " : ">> ", stdout);
fflush(stdout);
@@ -432,7 +330,7 @@ static ssize_t list_and_choose(struct add_i_state *s,
if (from < 0 || from >= items->items.nr ||
(singleton && from + 1 != to)) {
- color_fprintf_ln(stderr, s->error_color,
+ color_fprintf_ln(stderr, s->cfg.error_color,
_("Huh (%s)?"), p);
break;
} else if (singleton) {
@@ -992,7 +890,7 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps,
free(files->items.items[i].string);
} else if (item->index.unmerged ||
item->worktree.unmerged) {
- color_fprintf_ln(stderr, s->error_color,
+ color_fprintf_ln(stderr, s->cfg.error_color,
_("ignoring unmerged: %s"),
files->items.items[i].string);
free(item);
@@ -1014,9 +912,9 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps,
opts->prompt = N_("Patch update");
count = list_and_choose(s, files, opts);
if (count > 0) {
- struct add_p_opt add_p_opt = {
- .context = s->context,
- .interhunkcontext = s->interhunkcontext,
+ struct interactive_options opts = {
+ .context = s->cfg.context,
+ .interhunkcontext = s->cfg.interhunkcontext,
};
struct strvec args = STRVEC_INIT;
struct pathspec ps_selected = { 0 };
@@ -1028,7 +926,7 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps,
parse_pathspec(&ps_selected,
PATHSPEC_ALL_MAGIC & ~PATHSPEC_LITERAL,
PATHSPEC_LITERAL_PATH, "", args.v);
- res = run_add_p(s->r, ADD_P_ADD, &add_p_opt, NULL, &ps_selected);
+ res = run_add_p(s->r, ADD_P_ADD, &opts, NULL, &ps_selected);
strvec_clear(&args);
clear_pathspec(&ps_selected);
}
@@ -1064,10 +962,10 @@ static int run_diff(struct add_i_state *s, const struct pathspec *ps,
struct child_process cmd = CHILD_PROCESS_INIT;
strvec_pushl(&cmd.args, "git", "diff", "-p", "--cached", NULL);
- if (s->context != -1)
- strvec_pushf(&cmd.args, "--unified=%i", s->context);
- if (s->interhunkcontext != -1)
- strvec_pushf(&cmd.args, "--inter-hunk-context=%i", s->interhunkcontext);
+ if (s->cfg.context != -1)
+ strvec_pushf(&cmd.args, "--unified=%i", s->cfg.context);
+ if (s->cfg.interhunkcontext != -1)
+ strvec_pushf(&cmd.args, "--inter-hunk-context=%i", s->cfg.interhunkcontext);
strvec_pushl(&cmd.args, oid_to_hex(!is_initial ? &oid :
s->r->hash_algo->empty_tree), "--", NULL);
for (i = 0; i < files->items.nr; i++)
@@ -1085,17 +983,17 @@ static int run_help(struct add_i_state *s, const struct pathspec *ps UNUSED,
struct prefix_item_list *files UNUSED,
struct list_and_choose_options *opts UNUSED)
{
- color_fprintf_ln(stdout, s->help_color, "status - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "status - %s",
_("show paths with changes"));
- color_fprintf_ln(stdout, s->help_color, "update - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "update - %s",
_("add working tree state to the staged set of changes"));
- color_fprintf_ln(stdout, s->help_color, "revert - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "revert - %s",
_("revert staged set of changes back to the HEAD version"));
- color_fprintf_ln(stdout, s->help_color, "patch - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "patch - %s",
_("pick hunks and update selectively"));
- color_fprintf_ln(stdout, s->help_color, "diff - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "diff - %s",
_("view diff between HEAD and index"));
- color_fprintf_ln(stdout, s->help_color, "add untracked - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "add untracked - %s",
_("add contents of untracked files to the staged set of changes"));
return 0;
@@ -1103,21 +1001,21 @@ static int run_help(struct add_i_state *s, const struct pathspec *ps UNUSED,
static void choose_prompt_help(struct add_i_state *s)
{
- color_fprintf_ln(stdout, s->help_color, "%s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "%s",
_("Prompt help:"));
- color_fprintf_ln(stdout, s->help_color, "1 - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "1 - %s",
_("select a single item"));
- color_fprintf_ln(stdout, s->help_color, "3-5 - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "3-5 - %s",
_("select a range of items"));
- color_fprintf_ln(stdout, s->help_color, "2-3,6-9 - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "2-3,6-9 - %s",
_("select multiple ranges"));
- color_fprintf_ln(stdout, s->help_color, "foo - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "foo - %s",
_("select item based on unique prefix"));
- color_fprintf_ln(stdout, s->help_color, "-... - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "-... - %s",
_("unselect specified items"));
- color_fprintf_ln(stdout, s->help_color, "* - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "* - %s",
_("choose all items"));
- color_fprintf_ln(stdout, s->help_color, " - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, " - %s",
_("(empty) finish selecting"));
}
@@ -1152,7 +1050,7 @@ static void print_command_item(int i, int selected UNUSED,
static void command_prompt_help(struct add_i_state *s)
{
- const char *help_color = s->help_color;
+ const char *help_color = s->cfg.help_color;
color_fprintf_ln(stdout, help_color, "%s", _("Prompt help:"));
color_fprintf_ln(stdout, help_color, "1 - %s",
_("select a numbered item"));
@@ -1163,7 +1061,7 @@ static void command_prompt_help(struct add_i_state *s)
}
int run_add_i(struct repository *r, const struct pathspec *ps,
- struct add_p_opt *add_p_opt)
+ struct interactive_options *interactive_opts)
{
struct add_i_state s = { NULL };
struct print_command_item_data data = { "[", "]" };
@@ -1206,15 +1104,15 @@ int run_add_i(struct repository *r, const struct pathspec *ps,
->util = util;
}
- init_add_i_state(&s, r, add_p_opt);
+ init_add_i_state(&s, r, interactive_opts);
/*
* When color was asked for, use the prompt color for
* highlighting, otherwise use square brackets.
*/
- if (want_color(s.use_color_interactive)) {
- data.color = s.prompt_color;
- data.reset = s.reset_color_interactive;
+ if (want_color(s.cfg.use_color_interactive)) {
+ data.color = s.cfg.prompt_color;
+ data.reset = s.cfg.reset_color_interactive;
}
print_file_item_data.color = data.color;
print_file_item_data.reset = data.reset;
diff --git a/add-interactive.h b/add-interactive.h
index 2e3d1d871d2..eefa2edc7c1 100644
--- a/add-interactive.h
+++ b/add-interactive.h
@@ -2,37 +2,20 @@
#define ADD_INTERACTIVE_H
#include "add-patch.h"
-#include "color.h"
struct pathspec;
struct repository;
struct add_i_state {
struct repository *r;
- enum git_colorbool use_color_interactive;
- enum git_colorbool use_color_diff;
- char header_color[COLOR_MAXLEN];
- char help_color[COLOR_MAXLEN];
- char prompt_color[COLOR_MAXLEN];
- char error_color[COLOR_MAXLEN];
- char reset_color_interactive[COLOR_MAXLEN];
-
- char fraginfo_color[COLOR_MAXLEN];
- char context_color[COLOR_MAXLEN];
- char file_old_color[COLOR_MAXLEN];
- char file_new_color[COLOR_MAXLEN];
- char reset_color_diff[COLOR_MAXLEN];
-
- int use_single_key;
- char *interactive_diff_filter, *interactive_diff_algorithm;
- int context, interhunkcontext;
+ struct interactive_config cfg;
};
void init_add_i_state(struct add_i_state *s, struct repository *r,
- struct add_p_opt *add_p_opt);
+ struct interactive_options *opts);
void clear_add_i_state(struct add_i_state *s);
int run_add_i(struct repository *r, const struct pathspec *ps,
- struct add_p_opt *add_p_opt);
+ struct interactive_options *opts);
#endif
diff --git a/add-patch.c b/add-patch.c
index 3594dd22534..5c6969927ac 100644
--- a/add-patch.c
+++ b/add-patch.c
@@ -5,6 +5,8 @@
#include "add-interactive.h"
#include "add-patch.h"
#include "advice.h"
+#include "config.h"
+#include "diff.h"
#include "editor.h"
#include "environment.h"
#include "gettext.h"
@@ -279,6 +281,122 @@ struct add_p_state {
const char *revision;
};
+static void init_color(struct repository *r,
+ enum git_colorbool use_color,
+ const char *section_and_slot, char *dst,
+ const char *default_color)
+{
+ char *key = xstrfmt("color.%s", section_and_slot);
+ const char *value;
+
+ if (!want_color(use_color))
+ dst[0] = '\0';
+ else if (repo_config_get_value(r, key, &value) ||
+ color_parse(value, dst))
+ strlcpy(dst, default_color, COLOR_MAXLEN);
+
+ free(key);
+}
+
+static enum git_colorbool check_color_config(struct repository *r, const char *var)
+{
+ const char *value;
+ enum git_colorbool ret;
+
+ if (repo_config_get_value(r, var, &value))
+ ret = GIT_COLOR_UNKNOWN;
+ else
+ ret = git_config_colorbool(var, value);
+
+ /*
+ * Do not rely on want_color() to fall back to color.ui for us. It uses
+ * the value parsed by git_color_config(), which may not have been
+ * called by the main command.
+ */
+ if (ret == GIT_COLOR_UNKNOWN &&
+ !repo_config_get_value(r, "color.ui", &value))
+ ret = git_config_colorbool("color.ui", value);
+
+ return ret;
+}
+
+void interactive_config_init(struct interactive_config *cfg,
+ struct repository *r,
+ struct interactive_options *opts)
+{
+ cfg->context = -1;
+ cfg->interhunkcontext = -1;
+
+ cfg->use_color_interactive = check_color_config(r, "color.interactive");
+
+ init_color(r, cfg->use_color_interactive, "interactive.header",
+ cfg->header_color, GIT_COLOR_BOLD);
+ init_color(r, cfg->use_color_interactive, "interactive.help",
+ cfg->help_color, GIT_COLOR_BOLD_RED);
+ init_color(r, cfg->use_color_interactive, "interactive.prompt",
+ cfg->prompt_color, GIT_COLOR_BOLD_BLUE);
+ init_color(r, cfg->use_color_interactive, "interactive.error",
+ cfg->error_color, GIT_COLOR_BOLD_RED);
+ strlcpy(cfg->reset_color_interactive,
+ want_color(cfg->use_color_interactive) ? GIT_COLOR_RESET : "", COLOR_MAXLEN);
+
+ cfg->use_color_diff = check_color_config(r, "color.diff");
+
+ init_color(r, cfg->use_color_diff, "diff.frag", cfg->fraginfo_color,
+ diff_get_color(cfg->use_color_diff, DIFF_FRAGINFO));
+ init_color(r, cfg->use_color_diff, "diff.context", cfg->context_color,
+ "fall back");
+ if (!strcmp(cfg->context_color, "fall back"))
+ init_color(r, cfg->use_color_diff, "diff.plain",
+ cfg->context_color,
+ diff_get_color(cfg->use_color_diff, DIFF_CONTEXT));
+ init_color(r, cfg->use_color_diff, "diff.old", cfg->file_old_color,
+ diff_get_color(cfg->use_color_diff, DIFF_FILE_OLD));
+ init_color(r, cfg->use_color_diff, "diff.new", cfg->file_new_color,
+ diff_get_color(cfg->use_color_diff, DIFF_FILE_NEW));
+ strlcpy(cfg->reset_color_diff,
+ want_color(cfg->use_color_diff) ? GIT_COLOR_RESET : "", COLOR_MAXLEN);
+
+ FREE_AND_NULL(cfg->interactive_diff_filter);
+ repo_config_get_string(r, "interactive.difffilter",
+ &cfg->interactive_diff_filter);
+
+ FREE_AND_NULL(cfg->interactive_diff_algorithm);
+ repo_config_get_string(r, "diff.algorithm",
+ &cfg->interactive_diff_algorithm);
+
+ if (!repo_config_get_int(r, "diff.context", &cfg->context))
+ if (cfg->context < 0)
+ die(_("%s cannot be negative"), "diff.context");
+ if (!repo_config_get_int(r, "diff.interHunkContext", &cfg->interhunkcontext))
+ if (cfg->interhunkcontext < 0)
+ die(_("%s cannot be negative"), "diff.interHunkContext");
+
+ repo_config_get_bool(r, "interactive.singlekey", &cfg->use_single_key);
+ if (cfg->use_single_key)
+ setbuf(stdin, NULL);
+
+ if (opts->context != -1) {
+ if (opts->context < 0)
+ die(_("%s cannot be negative"), "--unified");
+ cfg->context = opts->context;
+ }
+ if (opts->interhunkcontext != -1) {
+ if (opts->interhunkcontext < 0)
+ die(_("%s cannot be negative"), "--inter-hunk-context");
+ cfg->interhunkcontext = opts->interhunkcontext;
+ }
+}
+
+void interactive_config_clear(struct interactive_config *cfg)
+{
+ FREE_AND_NULL(cfg->interactive_diff_filter);
+ FREE_AND_NULL(cfg->interactive_diff_algorithm);
+ memset(cfg, 0, sizeof(*cfg));
+ cfg->use_color_interactive = GIT_COLOR_UNKNOWN;
+ cfg->use_color_diff = GIT_COLOR_UNKNOWN;
+}
+
static void add_p_state_clear(struct add_p_state *s)
{
size_t i;
@@ -299,9 +417,9 @@ static void err(struct add_p_state *s, const char *fmt, ...)
va_list args;
va_start(args, fmt);
- fputs(s->s.error_color, stdout);
+ fputs(s->s.cfg.error_color, stdout);
vprintf(fmt, args);
- puts(s->s.reset_color_interactive);
+ puts(s->s.cfg.reset_color_interactive);
va_end(args);
}
@@ -424,12 +542,12 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
int res;
strvec_pushv(&args, s->mode->diff_cmd);
- if (s->s.context != -1)
- strvec_pushf(&args, "--unified=%i", s->s.context);
- if (s->s.interhunkcontext != -1)
- strvec_pushf(&args, "--inter-hunk-context=%i", s->s.interhunkcontext);
- if (s->s.interactive_diff_algorithm)
- strvec_pushf(&args, "--diff-algorithm=%s", s->s.interactive_diff_algorithm);
+ if (s->s.cfg.context != -1)
+ strvec_pushf(&args, "--unified=%i", s->s.cfg.context);
+ if (s->s.cfg.interhunkcontext != -1)
+ strvec_pushf(&args, "--inter-hunk-context=%i", s->s.cfg.interhunkcontext);
+ if (s->s.cfg.interactive_diff_algorithm)
+ strvec_pushf(&args, "--diff-algorithm=%s", s->s.cfg.interactive_diff_algorithm);
if (s->revision) {
struct object_id oid;
strvec_push(&args,
@@ -458,9 +576,9 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
}
strbuf_complete_line(plain);
- if (want_color_fd(1, s->s.use_color_diff)) {
+ if (want_color_fd(1, s->s.cfg.use_color_diff)) {
struct child_process colored_cp = CHILD_PROCESS_INIT;
- const char *diff_filter = s->s.interactive_diff_filter;
+ const char *diff_filter = s->s.cfg.interactive_diff_filter;
setup_child_process(s, &colored_cp, NULL);
xsnprintf((char *)args.v[color_arg_index], 8, "--color");
@@ -693,7 +811,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
hunk->colored_end - hunk->colored_start);
return;
} else {
- strbuf_addstr(out, s->s.fraginfo_color);
+ strbuf_addstr(out, s->s.cfg.fraginfo_color);
p = s->colored.buf + header->colored_extra_start;
len = header->colored_extra_end
- header->colored_extra_start;
@@ -715,7 +833,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
if (len)
strbuf_add(out, p, len);
else if (colored)
- strbuf_addf(out, "%s\n", s->s.reset_color_diff);
+ strbuf_addf(out, "%s\n", s->s.cfg.reset_color_diff);
else
strbuf_addch(out, '\n');
}
@@ -1104,12 +1222,12 @@ static void recolor_hunk(struct add_p_state *s, struct hunk *hunk)
strbuf_addstr(&s->colored,
plain[current] == '-' ?
- s->s.file_old_color :
+ s->s.cfg.file_old_color :
plain[current] == '+' ?
- s->s.file_new_color :
- s->s.context_color);
+ s->s.cfg.file_new_color :
+ s->s.cfg.context_color);
strbuf_add(&s->colored, plain + current, eol - current);
- strbuf_addstr(&s->colored, s->s.reset_color_diff);
+ strbuf_addstr(&s->colored, s->s.cfg.reset_color_diff);
if (next > eol)
strbuf_add(&s->colored, plain + eol, next - eol);
current = next;
@@ -1238,7 +1356,7 @@ static int run_apply_check(struct add_p_state *s,
static int read_single_character(struct add_p_state *s)
{
- if (s->s.use_single_key) {
+ if (s->s.cfg.use_single_key) {
int res = read_key_without_echo(&s->answer);
printf("%s\n", res == EOF ? "" : s->answer.buf);
return res;
@@ -1252,7 +1370,7 @@ static int read_single_character(struct add_p_state *s)
static int prompt_yesno(struct add_p_state *s, const char *prompt)
{
for (;;) {
- color_fprintf(stdout, s->s.prompt_color, "%s", _(prompt));
+ color_fprintf(stdout, s->s.cfg.prompt_color, "%s", _(prompt));
fflush(stdout);
if (read_single_character(s) == EOF)
return -1;
@@ -1560,15 +1678,15 @@ static int patch_update_file(struct add_p_state *s,
else
prompt_mode_type = PROMPT_HUNK;
- printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->s.prompt_color,
+ printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->s.cfg.prompt_color,
(uintmax_t)hunk_index + 1,
(uintmax_t)(file_diff->hunk_nr
? file_diff->hunk_nr
: 1));
printf(_(s->mode->prompt_mode[prompt_mode_type]),
s->buf.buf);
- if (*s->s.reset_color_interactive)
- fputs(s->s.reset_color_interactive, stdout);
+ if (*s->s.cfg.reset_color_interactive)
+ fputs(s->s.cfg.reset_color_interactive, stdout);
fflush(stdout);
if (read_single_character(s) == EOF)
break;
@@ -1730,7 +1848,7 @@ static int patch_update_file(struct add_p_state *s,
err(s, _("Sorry, cannot split this hunk"));
} else if (!split_hunk(s, file_diff,
hunk - file_diff->hunk)) {
- color_fprintf_ln(stdout, s->s.header_color,
+ color_fprintf_ln(stdout, s->s.cfg.header_color,
_("Split into %d hunks."),
(int)splittable_into);
rendered_hunk_index = -1;
@@ -1748,7 +1866,7 @@ static int patch_update_file(struct add_p_state *s,
} else if (s->answer.buf[0] == '?') {
const char *p = _(help_patch_remainder), *eol = p;
- color_fprintf(stdout, s->s.help_color, "%s",
+ color_fprintf(stdout, s->s.cfg.help_color, "%s",
_(s->mode->help_patch_text));
/*
@@ -1766,7 +1884,7 @@ static int patch_update_file(struct add_p_state *s,
if (*p != '?' && !strchr(s->buf.buf, *p))
continue;
- color_fprintf_ln(stdout, s->s.help_color,
+ color_fprintf_ln(stdout, s->s.cfg.help_color,
"%.*s", (int)(eol - p), p);
}
} else {
@@ -1807,7 +1925,7 @@ static int patch_update_file(struct add_p_state *s,
}
int run_add_p(struct repository *r, enum add_p_mode mode,
- struct add_p_opt *o, const char *revision,
+ struct interactive_options *opts, const char *revision,
const struct pathspec *ps)
{
struct add_p_state s = {
@@ -1815,7 +1933,7 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
};
size_t i, binary_count = 0;
- init_add_i_state(&s.s, r, o);
+ init_add_i_state(&s.s, r, opts);
if (mode == ADD_P_STASH)
s.mode = &patch_mode_stash;
diff --git a/add-patch.h b/add-patch.h
index 4394c741076..a4a05d9d145 100644
--- a/add-patch.h
+++ b/add-patch.h
@@ -1,15 +1,45 @@
#ifndef ADD_PATCH_H
#define ADD_PATCH_H
+#include "color.h"
+
struct pathspec;
struct repository;
-struct add_p_opt {
+struct interactive_options {
int context;
int interhunkcontext;
};
-#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }
+#define INTERACTIVE_OPTIONS_INIT { \
+ .context = -1, \
+ .interhunkcontext = -1, \
+}
+
+struct interactive_config {
+ enum git_colorbool use_color_interactive;
+ enum git_colorbool use_color_diff;
+ char header_color[COLOR_MAXLEN];
+ char help_color[COLOR_MAXLEN];
+ char prompt_color[COLOR_MAXLEN];
+ char error_color[COLOR_MAXLEN];
+ char reset_color_interactive[COLOR_MAXLEN];
+
+ char fraginfo_color[COLOR_MAXLEN];
+ char context_color[COLOR_MAXLEN];
+ char file_old_color[COLOR_MAXLEN];
+ char file_new_color[COLOR_MAXLEN];
+ char reset_color_diff[COLOR_MAXLEN];
+
+ int use_single_key;
+ char *interactive_diff_filter, *interactive_diff_algorithm;
+ int context, interhunkcontext;
+};
+
+void interactive_config_init(struct interactive_config *cfg,
+ struct repository *r,
+ struct interactive_options *opts);
+void interactive_config_clear(struct interactive_config *cfg);
enum add_p_mode {
ADD_P_ADD,
@@ -20,7 +50,7 @@ enum add_p_mode {
};
int run_add_p(struct repository *r, enum add_p_mode mode,
- struct add_p_opt *o, const char *revision,
+ struct interactive_options *opts, const char *revision,
const struct pathspec *ps);
#endif
diff --git a/builtin/add.c b/builtin/add.c
index 32709794b38..6f1e2130528 100644
--- a/builtin/add.c
+++ b/builtin/add.c
@@ -31,7 +31,7 @@ static const char * const builtin_add_usage[] = {
NULL
};
static int patch_interactive, add_interactive, edit_interactive;
-static struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+static struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
static int take_worktree_changes;
static int add_renormalize;
static int pathspec_file_nul;
@@ -160,7 +160,7 @@ static int refresh(struct repository *repo, int verbose, const struct pathspec *
int interactive_add(struct repository *repo,
const char **argv,
const char *prefix,
- int patch, struct add_p_opt *add_p_opt)
+ int patch, struct interactive_options *interactive_opts)
{
struct pathspec pathspec;
int ret;
@@ -172,9 +172,9 @@ int interactive_add(struct repository *repo,
prefix, argv);
if (patch)
- ret = !!run_add_p(repo, ADD_P_ADD, add_p_opt, NULL, &pathspec);
+ ret = !!run_add_p(repo, ADD_P_ADD, interactive_opts, NULL, &pathspec);
else
- ret = !!run_add_i(repo, &pathspec, add_p_opt);
+ ret = !!run_add_i(repo, &pathspec, interactive_opts);
clear_pathspec(&pathspec);
return ret;
@@ -256,8 +256,8 @@ static struct option builtin_add_options[] = {
OPT_GROUP(""),
OPT_BOOL('i', "interactive", &add_interactive, N_("interactive picking")),
OPT_BOOL('p', "patch", &patch_interactive, N_("select hunks interactively")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT_BOOL('e', "edit", &edit_interactive, N_("edit current diff and apply")),
OPT__FORCE(&ignored_too, N_("allow adding otherwise ignored files"), 0),
OPT_BOOL('u', "update", &take_worktree_changes, N_("update tracked files")),
@@ -400,9 +400,9 @@ int cmd_add(int argc,
prepare_repo_settings(repo);
repo->settings.command_requires_full_index = 0;
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
if (patch_interactive)
@@ -412,11 +412,11 @@ int cmd_add(int argc,
die(_("options '%s' and '%s' cannot be used together"), "--dry-run", "--interactive/--patch");
if (pathspec_from_file)
die(_("options '%s' and '%s' cannot be used together"), "--pathspec-from-file", "--interactive/--patch");
- exit(interactive_add(repo, argv + 1, prefix, patch_interactive, &add_p_opt));
+ exit(interactive_add(repo, argv + 1, prefix, patch_interactive, &interactive_opts));
} else {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--interactive/--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--interactive/--patch");
}
diff --git a/builtin/checkout.c b/builtin/checkout.c
index f9453473fe2..d230b1f8995 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -546,7 +546,7 @@ static int checkout_paths(const struct checkout_opts *opts,
if (opts->patch_mode) {
enum add_p_mode patch_mode;
- struct add_p_opt add_p_opt = {
+ struct interactive_options interactive_opts = {
.context = opts->patch_context,
.interhunkcontext = opts->patch_interhunk_context,
};
@@ -575,7 +575,7 @@ static int checkout_paths(const struct checkout_opts *opts,
else
BUG("either flag must have been set, worktree=%d, index=%d",
opts->checkout_worktree, opts->checkout_index);
- return !!run_add_p(the_repository, patch_mode, &add_p_opt,
+ return !!run_add_p(the_repository, patch_mode, &interactive_opts,
rev, &opts->pathspec);
}
diff --git a/builtin/commit.c b/builtin/commit.c
index 0243f17d53c..640495cc57e 100644
--- a/builtin/commit.c
+++ b/builtin/commit.c
@@ -123,7 +123,7 @@ static const char *edit_message, *use_message;
static char *fixup_message, *fixup_commit, *squash_message;
static const char *fixup_prefix;
static int all, also, interactive, patch_interactive, only, amend, signoff;
-static struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+static struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
static int edit_flag = -1; /* unspecified */
static int quiet, verbose, no_verify, allow_empty, dry_run, renew_authorship;
static int config_commit_verbose = -1; /* unspecified */
@@ -356,9 +356,9 @@ static const char *prepare_index(const char **argv, const char *prefix,
const char *ret;
char *path = NULL;
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
if (is_status)
@@ -407,7 +407,7 @@ static const char *prepare_index(const char **argv, const char *prefix,
old_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT));
setenv(INDEX_ENVIRONMENT, the_repository->index_file, 1);
- if (interactive_add(the_repository, argv, prefix, patch_interactive, &add_p_opt) != 0)
+ if (interactive_add(the_repository, argv, prefix, patch_interactive, &interactive_opts) != 0)
die(_("interactive add failed"));
the_repository->index_file = old_repo_index_file;
@@ -432,9 +432,9 @@ static const char *prepare_index(const char **argv, const char *prefix,
ret = get_lock_file_path(&index_lock);
goto out;
} else {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--interactive/--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--interactive/--patch");
}
@@ -1742,8 +1742,8 @@ int cmd_commit(int argc,
OPT_BOOL('i', "include", &also, N_("add specified files to index for commit")),
OPT_BOOL(0, "interactive", &interactive, N_("interactively add files")),
OPT_BOOL('p', "patch", &patch_interactive, N_("interactively add changes")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT_BOOL('o', "only", &only, N_("commit only specified files")),
OPT_BOOL('n', "no-verify", &no_verify, N_("bypass pre-commit and commit-msg hooks")),
OPT_BOOL(0, "dry-run", &dry_run, N_("show what would be committed")),
diff --git a/builtin/reset.c b/builtin/reset.c
index ed35802af15..088449e1209 100644
--- a/builtin/reset.c
+++ b/builtin/reset.c
@@ -346,7 +346,7 @@ int cmd_reset(int argc,
struct object_id oid;
struct pathspec pathspec;
int intent_to_add = 0;
- struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
const struct option options[] = {
OPT__QUIET(&quiet, N_("be quiet, only report errors")),
OPT_BOOL(0, "no-refresh", &no_refresh,
@@ -371,8 +371,8 @@ int cmd_reset(int argc,
PARSE_OPT_OPTARG,
option_parse_recurse_submodules_worktree_updater),
OPT_BOOL('p', "patch", &patch_mode, N_("select hunks interactively")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT_BOOL('N', "intent-to-add", &intent_to_add,
N_("record only the fact that removed paths will be added later")),
OPT_PATHSPEC_FROM_FILE(&pathspec_from_file),
@@ -423,9 +423,9 @@ int cmd_reset(int argc,
oidcpy(&oid, &tree->object.oid);
}
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
prepare_repo_settings(the_repository);
@@ -436,12 +436,12 @@ int cmd_reset(int argc,
die(_("options '%s' and '%s' cannot be used together"), "--patch", "--{hard,mixed,soft}");
trace2_cmd_mode("patch-interactive");
update_ref_status = !!run_add_p(the_repository, ADD_P_RESET,
- &add_p_opt, rev, &pathspec);
+ &interactive_opts, rev, &pathspec);
goto cleanup;
} else {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--patch");
}
diff --git a/builtin/stash.c b/builtin/stash.c
index 948eba06fbc..3b509052338 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -1306,7 +1306,7 @@ static int stash_staged(struct stash_info *info, struct strbuf *out_patch,
static int stash_patch(struct stash_info *info, const struct pathspec *ps,
struct strbuf *out_patch, int quiet,
- struct add_p_opt *add_p_opt)
+ struct interactive_options *interactive_opts)
{
int ret = 0;
struct child_process cp_read_tree = CHILD_PROCESS_INIT;
@@ -1331,7 +1331,7 @@ static int stash_patch(struct stash_info *info, const struct pathspec *ps,
old_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT));
setenv(INDEX_ENVIRONMENT, the_repository->index_file, 1);
- ret = !!run_add_p(the_repository, ADD_P_STASH, add_p_opt, NULL, ps);
+ ret = !!run_add_p(the_repository, ADD_P_STASH, interactive_opts, NULL, ps);
the_repository->index_file = old_repo_index_file;
if (old_index_env && *old_index_env)
@@ -1427,7 +1427,8 @@ static int stash_working_tree(struct stash_info *info, const struct pathspec *ps
}
static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_buf,
- int include_untracked, int patch_mode, struct add_p_opt *add_p_opt,
+ int include_untracked, int patch_mode,
+ struct interactive_options *interactive_opts,
int only_staged, struct stash_info *info, struct strbuf *patch,
int quiet)
{
@@ -1509,7 +1510,7 @@ static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_b
untracked_commit_option = 1;
}
if (patch_mode) {
- ret = stash_patch(info, ps, patch, quiet, add_p_opt);
+ ret = stash_patch(info, ps, patch, quiet, interactive_opts);
if (ret < 0) {
if (!quiet)
fprintf_ln(stderr, _("Cannot save the current "
@@ -1595,7 +1596,8 @@ static int create_stash(int argc, const char **argv, const char *prefix UNUSED,
}
static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int quiet,
- int keep_index, int patch_mode, struct add_p_opt *add_p_opt,
+ int keep_index, int patch_mode,
+ struct interactive_options *interactive_opts,
int include_untracked, int only_staged)
{
int ret = 0;
@@ -1667,7 +1669,7 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q
if (stash_msg)
strbuf_addstr(&stash_msg_buf, stash_msg);
if (do_create_stash(ps, &stash_msg_buf, include_untracked, patch_mode,
- add_p_opt, only_staged, &info, &patch, quiet)) {
+ interactive_opts, only_staged, &info, &patch, quiet)) {
ret = -1;
goto done;
}
@@ -1841,7 +1843,7 @@ static int push_stash(int argc, const char **argv, const char *prefix,
const char *stash_msg = NULL;
char *pathspec_from_file = NULL;
struct pathspec ps;
- struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
struct option options[] = {
OPT_BOOL('k', "keep-index", &keep_index,
N_("keep index")),
@@ -1849,8 +1851,8 @@ static int push_stash(int argc, const char **argv, const char *prefix,
N_("stash staged changes only")),
OPT_BOOL('p', "patch", &patch_mode,
N_("stash in patch mode")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT__QUIET(&quiet, N_("quiet mode")),
OPT_BOOL('u', "include-untracked", &include_untracked,
N_("include untracked files in stash")),
@@ -1907,19 +1909,19 @@ static int push_stash(int argc, const char **argv, const char *prefix,
}
if (!patch_mode) {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--patch");
}
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
ret = do_push_stash(&ps, stash_msg, quiet, keep_index, patch_mode,
- &add_p_opt, include_untracked, only_staged);
+ &interactive_opts, include_untracked, only_staged);
clear_pathspec(&ps);
free(pathspec_from_file);
@@ -1944,7 +1946,7 @@ static int save_stash(int argc, const char **argv, const char *prefix,
const char *stash_msg = NULL;
struct pathspec ps;
struct strbuf stash_msg_buf = STRBUF_INIT;
- struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
struct option options[] = {
OPT_BOOL('k', "keep-index", &keep_index,
N_("keep index")),
@@ -1952,8 +1954,8 @@ static int save_stash(int argc, const char **argv, const char *prefix,
N_("stash staged changes only")),
OPT_BOOL('p', "patch", &patch_mode,
N_("stash in patch mode")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT__QUIET(&quiet, N_("quiet mode")),
OPT_BOOL('u', "include-untracked", &include_untracked,
N_("include untracked files in stash")),
@@ -1973,20 +1975,20 @@ static int save_stash(int argc, const char **argv, const char *prefix,
memset(&ps, 0, sizeof(ps));
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
if (!patch_mode) {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--patch");
}
ret = do_push_stash(&ps, stash_msg, quiet, keep_index,
- patch_mode, &add_p_opt, include_untracked,
+ patch_mode, &interactive_opts, include_untracked,
only_staged);
strbuf_release(&stash_msg_buf);
diff --git a/commit.h b/commit.h
index 1d6e0c7518b..7b6e59d6c19 100644
--- a/commit.h
+++ b/commit.h
@@ -258,7 +258,7 @@ int for_each_commit_graft(each_commit_graft_fn, void *);
int interactive_add(struct repository *repo,
const char **argv,
const char *prefix,
- int patch, struct add_p_opt *add_p_opt);
+ int patch, struct interactive_options *opts);
struct commit_extra_header {
struct commit_extra_header *next;
--
2.51.1.930.gacf6e81ea2.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH v6 07/11] add-patch: split out `struct interactive_options`
2025-10-27 11:33 ` [PATCH v6 07/11] add-patch: split out `struct interactive_options` Patrick Steinhardt
@ 2025-11-20 7:03 ` Elijah Newren
2025-11-20 15:05 ` Phillip Wood
1 sibling, 0 replies; 278+ messages in thread
From: Elijah Newren @ 2025-11-20 7:03 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Karthik Nayak
On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:
>
> The `struct add_p_opt` is reused both by our infra for "git add -p" and
> "git add -i". Users of `run_add_i()` for example are expected to pass
> `struct add_p_opt`. This is somewhat confusing and raises the question
> of which options apply to what part of the stack.
>
> But things are even more confusing than that: while callers are expected
> to pass in `struct add_p_opt`, these options ultimately get used to
> initialize a `struct add_i_state` that is used by both subsystems. So we
> are basically going full circle here.
>
> Refactor the code and split out a new `struct interactive_options` that
> hosts common options used by both. These options are then applied to a
> `struct interactive_config` that hosts common configuration.
Makes sense.
--color-moved helped me view part of the patch, and --color-words=. in
some places showed just repeated additions of ".cfg". Still a pretty
long patch; I'm almost curious if it could be split up into more steps
to make the review easier (I admit to skimming), but scanning over it
looks reasonable.
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v6 07/11] add-patch: split out `struct interactive_options`
2025-10-27 11:33 ` [PATCH v6 07/11] add-patch: split out `struct interactive_options` Patrick Steinhardt
2025-11-20 7:03 ` Elijah Newren
@ 2025-11-20 15:05 ` Phillip Wood
2025-12-02 18:48 ` Patrick Steinhardt
1 sibling, 1 reply; 278+ messages in thread
From: Phillip Wood @ 2025-11-20 15:05 UTC (permalink / raw)
To: Patrick Steinhardt, git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
On 27/10/2025 11:33, Patrick Steinhardt wrote:
> The `struct add_p_opt` is reused both by our infra for "git add -p" and
> "git add -i". Users of `run_add_i()` for example are expected to pass
> `struct add_p_opt`. This is somewhat confusing and raises the question
> of which options apply to what part of the stack.
To some extent the config setting are intertwined because run_add_i() is
a superset of run_add_p(). Callers of run_add_i() needs to initalize an
instance of "struct add_p_opt" because they are indirectly calling
run_add_p().
> But things are even more confusing than that: while callers are expected
> to pass in `struct add_p_opt`, these options ultimately get used to
> initialize a `struct add_i_state` that is used by both subsystems. So we
> are basically going full circle here.
It is certainly confusing that we have to initalize a "struct
add_i_state" in run_add_p(). struct add_p_opt is only consumed in
add-patch.c, the reason it apperas in add-interactive.c is that
run_add_i() needs to pass it along to run_add_p().
> Refactor the code and split out a new `struct interactive_options` that
> hosts common options used by both. These options are then applied to a
> `struct interactive_config` that hosts common configuration.
I'm a little skeptical about renaming "sturct add_p_opt" as it only
holds members that are relavent to run_add_p(). Also if we're trying to
draw clear boundaries between the two subsystems hosting "struct
interactive_options" and "struct interactive_config" in add-patch.c
rather than add-interactive.c is potentially confusing.
> This refactoring doesn't yet fully detangle the two subsystems from one
> another, as we still end up calling `init_add_i_state()` in the "git add
> -p" subsystem. This will be fixed in a subsequent commit.
I think the ultimate aim of not having to initalize a "struct
add_i_state" in run_add_p() is a good idea. I'm not sure though that
having to pass a "struct interactive_options" to run_add_p() is any less
confusing than having to pass a "struct add_p_opt" to run_add_i().
Thanks
Phillip
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> add-interactive.c | 174 +++++++++++------------------------------------------
> add-interactive.h | 23 +------
> add-patch.c | 170 +++++++++++++++++++++++++++++++++++++++++++--------
> add-patch.h | 36 ++++++++++-
> builtin/add.c | 22 +++----
> builtin/checkout.c | 4 +-
> builtin/commit.c | 16 ++---
> builtin/reset.c | 16 ++---
> builtin/stash.c | 46 +++++++-------
> commit.h | 2 +-
> 10 files changed, 270 insertions(+), 239 deletions(-)
>
> diff --git a/add-interactive.c b/add-interactive.c
> index 68fc09547dd..05d2e7eefe3 100644
> --- a/add-interactive.c
> +++ b/add-interactive.c
> @@ -3,7 +3,6 @@
> #include "git-compat-util.h"
> #include "add-interactive.h"
> #include "color.h"
> -#include "config.h"
> #include "diffcore.h"
> #include "gettext.h"
> #include "hash.h"
> @@ -20,119 +19,18 @@
> #include "prompt.h"
> #include "tree.h"
>
> -static void init_color(struct repository *r, enum git_colorbool use_color,
> - const char *section_and_slot, char *dst,
> - const char *default_color)
> -{
> - char *key = xstrfmt("color.%s", section_and_slot);
> - const char *value;
> -
> - if (!want_color(use_color))
> - dst[0] = '\0';
> - else if (repo_config_get_value(r, key, &value) ||
> - color_parse(value, dst))
> - strlcpy(dst, default_color, COLOR_MAXLEN);
> -
> - free(key);
> -}
> -
> -static enum git_colorbool check_color_config(struct repository *r, const char *var)
> -{
> - const char *value;
> - enum git_colorbool ret;
> -
> - if (repo_config_get_value(r, var, &value))
> - ret = GIT_COLOR_UNKNOWN;
> - else
> - ret = git_config_colorbool(var, value);
> -
> - /*
> - * Do not rely on want_color() to fall back to color.ui for us. It uses
> - * the value parsed by git_color_config(), which may not have been
> - * called by the main command.
> - */
> - if (ret == GIT_COLOR_UNKNOWN &&
> - !repo_config_get_value(r, "color.ui", &value))
> - ret = git_config_colorbool("color.ui", value);
> -
> - return ret;
> -}
> -
> void init_add_i_state(struct add_i_state *s, struct repository *r,
> - struct add_p_opt *add_p_opt)
> + struct interactive_options *opts)
> {
> s->r = r;
> - s->context = -1;
> - s->interhunkcontext = -1;
> -
> - s->use_color_interactive = check_color_config(r, "color.interactive");
> -
> - init_color(r, s->use_color_interactive, "interactive.header",
> - s->header_color, GIT_COLOR_BOLD);
> - init_color(r, s->use_color_interactive, "interactive.help",
> - s->help_color, GIT_COLOR_BOLD_RED);
> - init_color(r, s->use_color_interactive, "interactive.prompt",
> - s->prompt_color, GIT_COLOR_BOLD_BLUE);
> - init_color(r, s->use_color_interactive, "interactive.error",
> - s->error_color, GIT_COLOR_BOLD_RED);
> - strlcpy(s->reset_color_interactive,
> - want_color(s->use_color_interactive) ? GIT_COLOR_RESET : "", COLOR_MAXLEN);
> -
> - s->use_color_diff = check_color_config(r, "color.diff");
> -
> - init_color(r, s->use_color_diff, "diff.frag", s->fraginfo_color,
> - diff_get_color(s->use_color_diff, DIFF_FRAGINFO));
> - init_color(r, s->use_color_diff, "diff.context", s->context_color,
> - "fall back");
> - if (!strcmp(s->context_color, "fall back"))
> - init_color(r, s->use_color_diff, "diff.plain",
> - s->context_color,
> - diff_get_color(s->use_color_diff, DIFF_CONTEXT));
> - init_color(r, s->use_color_diff, "diff.old", s->file_old_color,
> - diff_get_color(s->use_color_diff, DIFF_FILE_OLD));
> - init_color(r, s->use_color_diff, "diff.new", s->file_new_color,
> - diff_get_color(s->use_color_diff, DIFF_FILE_NEW));
> - strlcpy(s->reset_color_diff,
> - want_color(s->use_color_diff) ? GIT_COLOR_RESET : "", COLOR_MAXLEN);
> -
> - FREE_AND_NULL(s->interactive_diff_filter);
> - repo_config_get_string(r, "interactive.difffilter",
> - &s->interactive_diff_filter);
> -
> - FREE_AND_NULL(s->interactive_diff_algorithm);
> - repo_config_get_string(r, "diff.algorithm",
> - &s->interactive_diff_algorithm);
> -
> - if (!repo_config_get_int(r, "diff.context", &s->context))
> - if (s->context < 0)
> - die(_("%s cannot be negative"), "diff.context");
> - if (!repo_config_get_int(r, "diff.interHunkContext", &s->interhunkcontext))
> - if (s->interhunkcontext < 0)
> - die(_("%s cannot be negative"), "diff.interHunkContext");
> -
> - repo_config_get_bool(r, "interactive.singlekey", &s->use_single_key);
> - if (s->use_single_key)
> - setbuf(stdin, NULL);
> -
> - if (add_p_opt->context != -1) {
> - if (add_p_opt->context < 0)
> - die(_("%s cannot be negative"), "--unified");
> - s->context = add_p_opt->context;
> - }
> - if (add_p_opt->interhunkcontext != -1) {
> - if (add_p_opt->interhunkcontext < 0)
> - die(_("%s cannot be negative"), "--inter-hunk-context");
> - s->interhunkcontext = add_p_opt->interhunkcontext;
> - }
> + interactive_config_init(&s->cfg, r, opts);
> }
>
> void clear_add_i_state(struct add_i_state *s)
> {
> - FREE_AND_NULL(s->interactive_diff_filter);
> - FREE_AND_NULL(s->interactive_diff_algorithm);
> + interactive_config_clear(&s->cfg);
> memset(s, 0, sizeof(*s));
> - s->use_color_interactive = GIT_COLOR_UNKNOWN;
> - s->use_color_diff = GIT_COLOR_UNKNOWN;
> + interactive_config_clear(&s->cfg);
> }
>
> /*
> @@ -286,7 +184,7 @@ static void list(struct add_i_state *s, struct string_list *list, int *selected,
> return;
>
> if (opts->header)
> - color_fprintf_ln(stdout, s->header_color,
> + color_fprintf_ln(stdout, s->cfg.header_color,
> "%s", opts->header);
>
> for (i = 0; i < list->nr; i++) {
> @@ -354,7 +252,7 @@ static ssize_t list_and_choose(struct add_i_state *s,
>
> list(s, &items->items, items->selected, &opts->list_opts);
>
> - color_fprintf(stdout, s->prompt_color, "%s", opts->prompt);
> + color_fprintf(stdout, s->cfg.prompt_color, "%s", opts->prompt);
> fputs(singleton ? "> " : ">> ", stdout);
> fflush(stdout);
>
> @@ -432,7 +330,7 @@ static ssize_t list_and_choose(struct add_i_state *s,
>
> if (from < 0 || from >= items->items.nr ||
> (singleton && from + 1 != to)) {
> - color_fprintf_ln(stderr, s->error_color,
> + color_fprintf_ln(stderr, s->cfg.error_color,
> _("Huh (%s)?"), p);
> break;
> } else if (singleton) {
> @@ -992,7 +890,7 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps,
> free(files->items.items[i].string);
> } else if (item->index.unmerged ||
> item->worktree.unmerged) {
> - color_fprintf_ln(stderr, s->error_color,
> + color_fprintf_ln(stderr, s->cfg.error_color,
> _("ignoring unmerged: %s"),
> files->items.items[i].string);
> free(item);
> @@ -1014,9 +912,9 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps,
> opts->prompt = N_("Patch update");
> count = list_and_choose(s, files, opts);
> if (count > 0) {
> - struct add_p_opt add_p_opt = {
> - .context = s->context,
> - .interhunkcontext = s->interhunkcontext,
> + struct interactive_options opts = {
> + .context = s->cfg.context,
> + .interhunkcontext = s->cfg.interhunkcontext,
> };
> struct strvec args = STRVEC_INIT;
> struct pathspec ps_selected = { 0 };
> @@ -1028,7 +926,7 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps,
> parse_pathspec(&ps_selected,
> PATHSPEC_ALL_MAGIC & ~PATHSPEC_LITERAL,
> PATHSPEC_LITERAL_PATH, "", args.v);
> - res = run_add_p(s->r, ADD_P_ADD, &add_p_opt, NULL, &ps_selected);
> + res = run_add_p(s->r, ADD_P_ADD, &opts, NULL, &ps_selected);
> strvec_clear(&args);
> clear_pathspec(&ps_selected);
> }
> @@ -1064,10 +962,10 @@ static int run_diff(struct add_i_state *s, const struct pathspec *ps,
> struct child_process cmd = CHILD_PROCESS_INIT;
>
> strvec_pushl(&cmd.args, "git", "diff", "-p", "--cached", NULL);
> - if (s->context != -1)
> - strvec_pushf(&cmd.args, "--unified=%i", s->context);
> - if (s->interhunkcontext != -1)
> - strvec_pushf(&cmd.args, "--inter-hunk-context=%i", s->interhunkcontext);
> + if (s->cfg.context != -1)
> + strvec_pushf(&cmd.args, "--unified=%i", s->cfg.context);
> + if (s->cfg.interhunkcontext != -1)
> + strvec_pushf(&cmd.args, "--inter-hunk-context=%i", s->cfg.interhunkcontext);
> strvec_pushl(&cmd.args, oid_to_hex(!is_initial ? &oid :
> s->r->hash_algo->empty_tree), "--", NULL);
> for (i = 0; i < files->items.nr; i++)
> @@ -1085,17 +983,17 @@ static int run_help(struct add_i_state *s, const struct pathspec *ps UNUSED,
> struct prefix_item_list *files UNUSED,
> struct list_and_choose_options *opts UNUSED)
> {
> - color_fprintf_ln(stdout, s->help_color, "status - %s",
> + color_fprintf_ln(stdout, s->cfg.help_color, "status - %s",
> _("show paths with changes"));
> - color_fprintf_ln(stdout, s->help_color, "update - %s",
> + color_fprintf_ln(stdout, s->cfg.help_color, "update - %s",
> _("add working tree state to the staged set of changes"));
> - color_fprintf_ln(stdout, s->help_color, "revert - %s",
> + color_fprintf_ln(stdout, s->cfg.help_color, "revert - %s",
> _("revert staged set of changes back to the HEAD version"));
> - color_fprintf_ln(stdout, s->help_color, "patch - %s",
> + color_fprintf_ln(stdout, s->cfg.help_color, "patch - %s",
> _("pick hunks and update selectively"));
> - color_fprintf_ln(stdout, s->help_color, "diff - %s",
> + color_fprintf_ln(stdout, s->cfg.help_color, "diff - %s",
> _("view diff between HEAD and index"));
> - color_fprintf_ln(stdout, s->help_color, "add untracked - %s",
> + color_fprintf_ln(stdout, s->cfg.help_color, "add untracked - %s",
> _("add contents of untracked files to the staged set of changes"));
>
> return 0;
> @@ -1103,21 +1001,21 @@ static int run_help(struct add_i_state *s, const struct pathspec *ps UNUSED,
>
> static void choose_prompt_help(struct add_i_state *s)
> {
> - color_fprintf_ln(stdout, s->help_color, "%s",
> + color_fprintf_ln(stdout, s->cfg.help_color, "%s",
> _("Prompt help:"));
> - color_fprintf_ln(stdout, s->help_color, "1 - %s",
> + color_fprintf_ln(stdout, s->cfg.help_color, "1 - %s",
> _("select a single item"));
> - color_fprintf_ln(stdout, s->help_color, "3-5 - %s",
> + color_fprintf_ln(stdout, s->cfg.help_color, "3-5 - %s",
> _("select a range of items"));
> - color_fprintf_ln(stdout, s->help_color, "2-3,6-9 - %s",
> + color_fprintf_ln(stdout, s->cfg.help_color, "2-3,6-9 - %s",
> _("select multiple ranges"));
> - color_fprintf_ln(stdout, s->help_color, "foo - %s",
> + color_fprintf_ln(stdout, s->cfg.help_color, "foo - %s",
> _("select item based on unique prefix"));
> - color_fprintf_ln(stdout, s->help_color, "-... - %s",
> + color_fprintf_ln(stdout, s->cfg.help_color, "-... - %s",
> _("unselect specified items"));
> - color_fprintf_ln(stdout, s->help_color, "* - %s",
> + color_fprintf_ln(stdout, s->cfg.help_color, "* - %s",
> _("choose all items"));
> - color_fprintf_ln(stdout, s->help_color, " - %s",
> + color_fprintf_ln(stdout, s->cfg.help_color, " - %s",
> _("(empty) finish selecting"));
> }
>
> @@ -1152,7 +1050,7 @@ static void print_command_item(int i, int selected UNUSED,
>
> static void command_prompt_help(struct add_i_state *s)
> {
> - const char *help_color = s->help_color;
> + const char *help_color = s->cfg.help_color;
> color_fprintf_ln(stdout, help_color, "%s", _("Prompt help:"));
> color_fprintf_ln(stdout, help_color, "1 - %s",
> _("select a numbered item"));
> @@ -1163,7 +1061,7 @@ static void command_prompt_help(struct add_i_state *s)
> }
>
> int run_add_i(struct repository *r, const struct pathspec *ps,
> - struct add_p_opt *add_p_opt)
> + struct interactive_options *interactive_opts)
> {
> struct add_i_state s = { NULL };
> struct print_command_item_data data = { "[", "]" };
> @@ -1206,15 +1104,15 @@ int run_add_i(struct repository *r, const struct pathspec *ps,
> ->util = util;
> }
>
> - init_add_i_state(&s, r, add_p_opt);
> + init_add_i_state(&s, r, interactive_opts);
>
> /*
> * When color was asked for, use the prompt color for
> * highlighting, otherwise use square brackets.
> */
> - if (want_color(s.use_color_interactive)) {
> - data.color = s.prompt_color;
> - data.reset = s.reset_color_interactive;
> + if (want_color(s.cfg.use_color_interactive)) {
> + data.color = s.cfg.prompt_color;
> + data.reset = s.cfg.reset_color_interactive;
> }
> print_file_item_data.color = data.color;
> print_file_item_data.reset = data.reset;
> diff --git a/add-interactive.h b/add-interactive.h
> index 2e3d1d871d2..eefa2edc7c1 100644
> --- a/add-interactive.h
> +++ b/add-interactive.h
> @@ -2,37 +2,20 @@
> #define ADD_INTERACTIVE_H
>
> #include "add-patch.h"
> -#include "color.h"
>
> struct pathspec;
> struct repository;
>
> struct add_i_state {
> struct repository *r;
> - enum git_colorbool use_color_interactive;
> - enum git_colorbool use_color_diff;
> - char header_color[COLOR_MAXLEN];
> - char help_color[COLOR_MAXLEN];
> - char prompt_color[COLOR_MAXLEN];
> - char error_color[COLOR_MAXLEN];
> - char reset_color_interactive[COLOR_MAXLEN];
> -
> - char fraginfo_color[COLOR_MAXLEN];
> - char context_color[COLOR_MAXLEN];
> - char file_old_color[COLOR_MAXLEN];
> - char file_new_color[COLOR_MAXLEN];
> - char reset_color_diff[COLOR_MAXLEN];
> -
> - int use_single_key;
> - char *interactive_diff_filter, *interactive_diff_algorithm;
> - int context, interhunkcontext;
> + struct interactive_config cfg;
> };
>
> void init_add_i_state(struct add_i_state *s, struct repository *r,
> - struct add_p_opt *add_p_opt);
> + struct interactive_options *opts);
> void clear_add_i_state(struct add_i_state *s);
>
> int run_add_i(struct repository *r, const struct pathspec *ps,
> - struct add_p_opt *add_p_opt);
> + struct interactive_options *opts);
>
> #endif
> diff --git a/add-patch.c b/add-patch.c
> index 3594dd22534..5c6969927ac 100644
> --- a/add-patch.c
> +++ b/add-patch.c
> @@ -5,6 +5,8 @@
> #include "add-interactive.h"
> #include "add-patch.h"
> #include "advice.h"
> +#include "config.h"
> +#include "diff.h"
> #include "editor.h"
> #include "environment.h"
> #include "gettext.h"
> @@ -279,6 +281,122 @@ struct add_p_state {
> const char *revision;
> };
>
> +static void init_color(struct repository *r,
> + enum git_colorbool use_color,
> + const char *section_and_slot, char *dst,
> + const char *default_color)
> +{
> + char *key = xstrfmt("color.%s", section_and_slot);
> + const char *value;
> +
> + if (!want_color(use_color))
> + dst[0] = '\0';
> + else if (repo_config_get_value(r, key, &value) ||
> + color_parse(value, dst))
> + strlcpy(dst, default_color, COLOR_MAXLEN);
> +
> + free(key);
> +}
> +
> +static enum git_colorbool check_color_config(struct repository *r, const char *var)
> +{
> + const char *value;
> + enum git_colorbool ret;
> +
> + if (repo_config_get_value(r, var, &value))
> + ret = GIT_COLOR_UNKNOWN;
> + else
> + ret = git_config_colorbool(var, value);
> +
> + /*
> + * Do not rely on want_color() to fall back to color.ui for us. It uses
> + * the value parsed by git_color_config(), which may not have been
> + * called by the main command.
> + */
> + if (ret == GIT_COLOR_UNKNOWN &&
> + !repo_config_get_value(r, "color.ui", &value))
> + ret = git_config_colorbool("color.ui", value);
> +
> + return ret;
> +}
> +
> +void interactive_config_init(struct interactive_config *cfg,
> + struct repository *r,
> + struct interactive_options *opts)
> +{
> + cfg->context = -1;
> + cfg->interhunkcontext = -1;
> +
> + cfg->use_color_interactive = check_color_config(r, "color.interactive");
> +
> + init_color(r, cfg->use_color_interactive, "interactive.header",
> + cfg->header_color, GIT_COLOR_BOLD);
> + init_color(r, cfg->use_color_interactive, "interactive.help",
> + cfg->help_color, GIT_COLOR_BOLD_RED);
> + init_color(r, cfg->use_color_interactive, "interactive.prompt",
> + cfg->prompt_color, GIT_COLOR_BOLD_BLUE);
> + init_color(r, cfg->use_color_interactive, "interactive.error",
> + cfg->error_color, GIT_COLOR_BOLD_RED);
> + strlcpy(cfg->reset_color_interactive,
> + want_color(cfg->use_color_interactive) ? GIT_COLOR_RESET : "", COLOR_MAXLEN);
> +
> + cfg->use_color_diff = check_color_config(r, "color.diff");
> +
> + init_color(r, cfg->use_color_diff, "diff.frag", cfg->fraginfo_color,
> + diff_get_color(cfg->use_color_diff, DIFF_FRAGINFO));
> + init_color(r, cfg->use_color_diff, "diff.context", cfg->context_color,
> + "fall back");
> + if (!strcmp(cfg->context_color, "fall back"))
> + init_color(r, cfg->use_color_diff, "diff.plain",
> + cfg->context_color,
> + diff_get_color(cfg->use_color_diff, DIFF_CONTEXT));
> + init_color(r, cfg->use_color_diff, "diff.old", cfg->file_old_color,
> + diff_get_color(cfg->use_color_diff, DIFF_FILE_OLD));
> + init_color(r, cfg->use_color_diff, "diff.new", cfg->file_new_color,
> + diff_get_color(cfg->use_color_diff, DIFF_FILE_NEW));
> + strlcpy(cfg->reset_color_diff,
> + want_color(cfg->use_color_diff) ? GIT_COLOR_RESET : "", COLOR_MAXLEN);
> +
> + FREE_AND_NULL(cfg->interactive_diff_filter);
> + repo_config_get_string(r, "interactive.difffilter",
> + &cfg->interactive_diff_filter);
> +
> + FREE_AND_NULL(cfg->interactive_diff_algorithm);
> + repo_config_get_string(r, "diff.algorithm",
> + &cfg->interactive_diff_algorithm);
> +
> + if (!repo_config_get_int(r, "diff.context", &cfg->context))
> + if (cfg->context < 0)
> + die(_("%s cannot be negative"), "diff.context");
> + if (!repo_config_get_int(r, "diff.interHunkContext", &cfg->interhunkcontext))
> + if (cfg->interhunkcontext < 0)
> + die(_("%s cannot be negative"), "diff.interHunkContext");
> +
> + repo_config_get_bool(r, "interactive.singlekey", &cfg->use_single_key);
> + if (cfg->use_single_key)
> + setbuf(stdin, NULL);
> +
> + if (opts->context != -1) {
> + if (opts->context < 0)
> + die(_("%s cannot be negative"), "--unified");
> + cfg->context = opts->context;
> + }
> + if (opts->interhunkcontext != -1) {
> + if (opts->interhunkcontext < 0)
> + die(_("%s cannot be negative"), "--inter-hunk-context");
> + cfg->interhunkcontext = opts->interhunkcontext;
> + }
> +}
> +
> +void interactive_config_clear(struct interactive_config *cfg)
> +{
> + FREE_AND_NULL(cfg->interactive_diff_filter);
> + FREE_AND_NULL(cfg->interactive_diff_algorithm);
> + memset(cfg, 0, sizeof(*cfg));
> + cfg->use_color_interactive = GIT_COLOR_UNKNOWN;
> + cfg->use_color_diff = GIT_COLOR_UNKNOWN;
> +}
> +
> static void add_p_state_clear(struct add_p_state *s)
> {
> size_t i;
> @@ -299,9 +417,9 @@ static void err(struct add_p_state *s, const char *fmt, ...)
> va_list args;
>
> va_start(args, fmt);
> - fputs(s->s.error_color, stdout);
> + fputs(s->s.cfg.error_color, stdout);
> vprintf(fmt, args);
> - puts(s->s.reset_color_interactive);
> + puts(s->s.cfg.reset_color_interactive);
> va_end(args);
> }
>
> @@ -424,12 +542,12 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
> int res;
>
> strvec_pushv(&args, s->mode->diff_cmd);
> - if (s->s.context != -1)
> - strvec_pushf(&args, "--unified=%i", s->s.context);
> - if (s->s.interhunkcontext != -1)
> - strvec_pushf(&args, "--inter-hunk-context=%i", s->s.interhunkcontext);
> - if (s->s.interactive_diff_algorithm)
> - strvec_pushf(&args, "--diff-algorithm=%s", s->s.interactive_diff_algorithm);
> + if (s->s.cfg.context != -1)
> + strvec_pushf(&args, "--unified=%i", s->s.cfg.context);
> + if (s->s.cfg.interhunkcontext != -1)
> + strvec_pushf(&args, "--inter-hunk-context=%i", s->s.cfg.interhunkcontext);
> + if (s->s.cfg.interactive_diff_algorithm)
> + strvec_pushf(&args, "--diff-algorithm=%s", s->s.cfg.interactive_diff_algorithm);
> if (s->revision) {
> struct object_id oid;
> strvec_push(&args,
> @@ -458,9 +576,9 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
> }
> strbuf_complete_line(plain);
>
> - if (want_color_fd(1, s->s.use_color_diff)) {
> + if (want_color_fd(1, s->s.cfg.use_color_diff)) {
> struct child_process colored_cp = CHILD_PROCESS_INIT;
> - const char *diff_filter = s->s.interactive_diff_filter;
> + const char *diff_filter = s->s.cfg.interactive_diff_filter;
>
> setup_child_process(s, &colored_cp, NULL);
> xsnprintf((char *)args.v[color_arg_index], 8, "--color");
> @@ -693,7 +811,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
> hunk->colored_end - hunk->colored_start);
> return;
> } else {
> - strbuf_addstr(out, s->s.fraginfo_color);
> + strbuf_addstr(out, s->s.cfg.fraginfo_color);
> p = s->colored.buf + header->colored_extra_start;
> len = header->colored_extra_end
> - header->colored_extra_start;
> @@ -715,7 +833,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
> if (len)
> strbuf_add(out, p, len);
> else if (colored)
> - strbuf_addf(out, "%s\n", s->s.reset_color_diff);
> + strbuf_addf(out, "%s\n", s->s.cfg.reset_color_diff);
> else
> strbuf_addch(out, '\n');
> }
> @@ -1104,12 +1222,12 @@ static void recolor_hunk(struct add_p_state *s, struct hunk *hunk)
>
> strbuf_addstr(&s->colored,
> plain[current] == '-' ?
> - s->s.file_old_color :
> + s->s.cfg.file_old_color :
> plain[current] == '+' ?
> - s->s.file_new_color :
> - s->s.context_color);
> + s->s.cfg.file_new_color :
> + s->s.cfg.context_color);
> strbuf_add(&s->colored, plain + current, eol - current);
> - strbuf_addstr(&s->colored, s->s.reset_color_diff);
> + strbuf_addstr(&s->colored, s->s.cfg.reset_color_diff);
> if (next > eol)
> strbuf_add(&s->colored, plain + eol, next - eol);
> current = next;
> @@ -1238,7 +1356,7 @@ static int run_apply_check(struct add_p_state *s,
>
> static int read_single_character(struct add_p_state *s)
> {
> - if (s->s.use_single_key) {
> + if (s->s.cfg.use_single_key) {
> int res = read_key_without_echo(&s->answer);
> printf("%s\n", res == EOF ? "" : s->answer.buf);
> return res;
> @@ -1252,7 +1370,7 @@ static int read_single_character(struct add_p_state *s)
> static int prompt_yesno(struct add_p_state *s, const char *prompt)
> {
> for (;;) {
> - color_fprintf(stdout, s->s.prompt_color, "%s", _(prompt));
> + color_fprintf(stdout, s->s.cfg.prompt_color, "%s", _(prompt));
> fflush(stdout);
> if (read_single_character(s) == EOF)
> return -1;
> @@ -1560,15 +1678,15 @@ static int patch_update_file(struct add_p_state *s,
> else
> prompt_mode_type = PROMPT_HUNK;
>
> - printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->s.prompt_color,
> + printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->s.cfg.prompt_color,
> (uintmax_t)hunk_index + 1,
> (uintmax_t)(file_diff->hunk_nr
> ? file_diff->hunk_nr
> : 1));
> printf(_(s->mode->prompt_mode[prompt_mode_type]),
> s->buf.buf);
> - if (*s->s.reset_color_interactive)
> - fputs(s->s.reset_color_interactive, stdout);
> + if (*s->s.cfg.reset_color_interactive)
> + fputs(s->s.cfg.reset_color_interactive, stdout);
> fflush(stdout);
> if (read_single_character(s) == EOF)
> break;
> @@ -1730,7 +1848,7 @@ static int patch_update_file(struct add_p_state *s,
> err(s, _("Sorry, cannot split this hunk"));
> } else if (!split_hunk(s, file_diff,
> hunk - file_diff->hunk)) {
> - color_fprintf_ln(stdout, s->s.header_color,
> + color_fprintf_ln(stdout, s->s.cfg.header_color,
> _("Split into %d hunks."),
> (int)splittable_into);
> rendered_hunk_index = -1;
> @@ -1748,7 +1866,7 @@ static int patch_update_file(struct add_p_state *s,
> } else if (s->answer.buf[0] == '?') {
> const char *p = _(help_patch_remainder), *eol = p;
>
> - color_fprintf(stdout, s->s.help_color, "%s",
> + color_fprintf(stdout, s->s.cfg.help_color, "%s",
> _(s->mode->help_patch_text));
>
> /*
> @@ -1766,7 +1884,7 @@ static int patch_update_file(struct add_p_state *s,
> if (*p != '?' && !strchr(s->buf.buf, *p))
> continue;
>
> - color_fprintf_ln(stdout, s->s.help_color,
> + color_fprintf_ln(stdout, s->s.cfg.help_color,
> "%.*s", (int)(eol - p), p);
> }
> } else {
> @@ -1807,7 +1925,7 @@ static int patch_update_file(struct add_p_state *s,
> }
>
> int run_add_p(struct repository *r, enum add_p_mode mode,
> - struct add_p_opt *o, const char *revision,
> + struct interactive_options *opts, const char *revision,
> const struct pathspec *ps)
> {
> struct add_p_state s = {
> @@ -1815,7 +1933,7 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
> };
> size_t i, binary_count = 0;
>
> - init_add_i_state(&s.s, r, o);
> + init_add_i_state(&s.s, r, opts);
>
> if (mode == ADD_P_STASH)
> s.mode = &patch_mode_stash;
> diff --git a/add-patch.h b/add-patch.h
> index 4394c741076..a4a05d9d145 100644
> --- a/add-patch.h
> +++ b/add-patch.h
> @@ -1,15 +1,45 @@
> #ifndef ADD_PATCH_H
> #define ADD_PATCH_H
>
> +#include "color.h"
> +
> struct pathspec;
> struct repository;
>
> -struct add_p_opt {
> +struct interactive_options {
> int context;
> int interhunkcontext;
> };
>
> -#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }
> +#define INTERACTIVE_OPTIONS_INIT { \
> + .context = -1, \
> + .interhunkcontext = -1, \
> +}
> +
> +struct interactive_config {
> + enum git_colorbool use_color_interactive;
> + enum git_colorbool use_color_diff;
> + char header_color[COLOR_MAXLEN];
> + char help_color[COLOR_MAXLEN];
> + char prompt_color[COLOR_MAXLEN];
> + char error_color[COLOR_MAXLEN];
> + char reset_color_interactive[COLOR_MAXLEN];
> +
> + char fraginfo_color[COLOR_MAXLEN];
> + char context_color[COLOR_MAXLEN];
> + char file_old_color[COLOR_MAXLEN];
> + char file_new_color[COLOR_MAXLEN];
> + char reset_color_diff[COLOR_MAXLEN];
> +
> + int use_single_key;
> + char *interactive_diff_filter, *interactive_diff_algorithm;
> + int context, interhunkcontext;
> +};
> +
> +void interactive_config_init(struct interactive_config *cfg,
> + struct repository *r,
> + struct interactive_options *opts);
> +void interactive_config_clear(struct interactive_config *cfg);
>
> enum add_p_mode {
> ADD_P_ADD,
> @@ -20,7 +50,7 @@ enum add_p_mode {
> };
>
> int run_add_p(struct repository *r, enum add_p_mode mode,
> - struct add_p_opt *o, const char *revision,
> + struct interactive_options *opts, const char *revision,
> const struct pathspec *ps);
>
> #endif
> diff --git a/builtin/add.c b/builtin/add.c
> index 32709794b38..6f1e2130528 100644
> --- a/builtin/add.c
> +++ b/builtin/add.c
> @@ -31,7 +31,7 @@ static const char * const builtin_add_usage[] = {
> NULL
> };
> static int patch_interactive, add_interactive, edit_interactive;
> -static struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
> +static struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
> static int take_worktree_changes;
> static int add_renormalize;
> static int pathspec_file_nul;
> @@ -160,7 +160,7 @@ static int refresh(struct repository *repo, int verbose, const struct pathspec *
> int interactive_add(struct repository *repo,
> const char **argv,
> const char *prefix,
> - int patch, struct add_p_opt *add_p_opt)
> + int patch, struct interactive_options *interactive_opts)
> {
> struct pathspec pathspec;
> int ret;
> @@ -172,9 +172,9 @@ int interactive_add(struct repository *repo,
> prefix, argv);
>
> if (patch)
> - ret = !!run_add_p(repo, ADD_P_ADD, add_p_opt, NULL, &pathspec);
> + ret = !!run_add_p(repo, ADD_P_ADD, interactive_opts, NULL, &pathspec);
> else
> - ret = !!run_add_i(repo, &pathspec, add_p_opt);
> + ret = !!run_add_i(repo, &pathspec, interactive_opts);
>
> clear_pathspec(&pathspec);
> return ret;
> @@ -256,8 +256,8 @@ static struct option builtin_add_options[] = {
> OPT_GROUP(""),
> OPT_BOOL('i', "interactive", &add_interactive, N_("interactive picking")),
> OPT_BOOL('p', "patch", &patch_interactive, N_("select hunks interactively")),
> - OPT_DIFF_UNIFIED(&add_p_opt.context),
> - OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
> + OPT_DIFF_UNIFIED(&interactive_opts.context),
> + OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
> OPT_BOOL('e', "edit", &edit_interactive, N_("edit current diff and apply")),
> OPT__FORCE(&ignored_too, N_("allow adding otherwise ignored files"), 0),
> OPT_BOOL('u', "update", &take_worktree_changes, N_("update tracked files")),
> @@ -400,9 +400,9 @@ int cmd_add(int argc,
> prepare_repo_settings(repo);
> repo->settings.command_requires_full_index = 0;
>
> - if (add_p_opt.context < -1)
> + if (interactive_opts.context < -1)
> die(_("'%s' cannot be negative"), "--unified");
> - if (add_p_opt.interhunkcontext < -1)
> + if (interactive_opts.interhunkcontext < -1)
> die(_("'%s' cannot be negative"), "--inter-hunk-context");
>
> if (patch_interactive)
> @@ -412,11 +412,11 @@ int cmd_add(int argc,
> die(_("options '%s' and '%s' cannot be used together"), "--dry-run", "--interactive/--patch");
> if (pathspec_from_file)
> die(_("options '%s' and '%s' cannot be used together"), "--pathspec-from-file", "--interactive/--patch");
> - exit(interactive_add(repo, argv + 1, prefix, patch_interactive, &add_p_opt));
> + exit(interactive_add(repo, argv + 1, prefix, patch_interactive, &interactive_opts));
> } else {
> - if (add_p_opt.context != -1)
> + if (interactive_opts.context != -1)
> die(_("the option '%s' requires '%s'"), "--unified", "--interactive/--patch");
> - if (add_p_opt.interhunkcontext != -1)
> + if (interactive_opts.interhunkcontext != -1)
> die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--interactive/--patch");
> }
>
> diff --git a/builtin/checkout.c b/builtin/checkout.c
> index f9453473fe2..d230b1f8995 100644
> --- a/builtin/checkout.c
> +++ b/builtin/checkout.c
> @@ -546,7 +546,7 @@ static int checkout_paths(const struct checkout_opts *opts,
>
> if (opts->patch_mode) {
> enum add_p_mode patch_mode;
> - struct add_p_opt add_p_opt = {
> + struct interactive_options interactive_opts = {
> .context = opts->patch_context,
> .interhunkcontext = opts->patch_interhunk_context,
> };
> @@ -575,7 +575,7 @@ static int checkout_paths(const struct checkout_opts *opts,
> else
> BUG("either flag must have been set, worktree=%d, index=%d",
> opts->checkout_worktree, opts->checkout_index);
> - return !!run_add_p(the_repository, patch_mode, &add_p_opt,
> + return !!run_add_p(the_repository, patch_mode, &interactive_opts,
> rev, &opts->pathspec);
> }
>
> diff --git a/builtin/commit.c b/builtin/commit.c
> index 0243f17d53c..640495cc57e 100644
> --- a/builtin/commit.c
> +++ b/builtin/commit.c
> @@ -123,7 +123,7 @@ static const char *edit_message, *use_message;
> static char *fixup_message, *fixup_commit, *squash_message;
> static const char *fixup_prefix;
> static int all, also, interactive, patch_interactive, only, amend, signoff;
> -static struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
> +static struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
> static int edit_flag = -1; /* unspecified */
> static int quiet, verbose, no_verify, allow_empty, dry_run, renew_authorship;
> static int config_commit_verbose = -1; /* unspecified */
> @@ -356,9 +356,9 @@ static const char *prepare_index(const char **argv, const char *prefix,
> const char *ret;
> char *path = NULL;
>
> - if (add_p_opt.context < -1)
> + if (interactive_opts.context < -1)
> die(_("'%s' cannot be negative"), "--unified");
> - if (add_p_opt.interhunkcontext < -1)
> + if (interactive_opts.interhunkcontext < -1)
> die(_("'%s' cannot be negative"), "--inter-hunk-context");
>
> if (is_status)
> @@ -407,7 +407,7 @@ static const char *prepare_index(const char **argv, const char *prefix,
> old_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT));
> setenv(INDEX_ENVIRONMENT, the_repository->index_file, 1);
>
> - if (interactive_add(the_repository, argv, prefix, patch_interactive, &add_p_opt) != 0)
> + if (interactive_add(the_repository, argv, prefix, patch_interactive, &interactive_opts) != 0)
> die(_("interactive add failed"));
>
> the_repository->index_file = old_repo_index_file;
> @@ -432,9 +432,9 @@ static const char *prepare_index(const char **argv, const char *prefix,
> ret = get_lock_file_path(&index_lock);
> goto out;
> } else {
> - if (add_p_opt.context != -1)
> + if (interactive_opts.context != -1)
> die(_("the option '%s' requires '%s'"), "--unified", "--interactive/--patch");
> - if (add_p_opt.interhunkcontext != -1)
> + if (interactive_opts.interhunkcontext != -1)
> die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--interactive/--patch");
> }
>
> @@ -1742,8 +1742,8 @@ int cmd_commit(int argc,
> OPT_BOOL('i', "include", &also, N_("add specified files to index for commit")),
> OPT_BOOL(0, "interactive", &interactive, N_("interactively add files")),
> OPT_BOOL('p', "patch", &patch_interactive, N_("interactively add changes")),
> - OPT_DIFF_UNIFIED(&add_p_opt.context),
> - OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
> + OPT_DIFF_UNIFIED(&interactive_opts.context),
> + OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
> OPT_BOOL('o', "only", &only, N_("commit only specified files")),
> OPT_BOOL('n', "no-verify", &no_verify, N_("bypass pre-commit and commit-msg hooks")),
> OPT_BOOL(0, "dry-run", &dry_run, N_("show what would be committed")),
> diff --git a/builtin/reset.c b/builtin/reset.c
> index ed35802af15..088449e1209 100644
> --- a/builtin/reset.c
> +++ b/builtin/reset.c
> @@ -346,7 +346,7 @@ int cmd_reset(int argc,
> struct object_id oid;
> struct pathspec pathspec;
> int intent_to_add = 0;
> - struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
> + struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
> const struct option options[] = {
> OPT__QUIET(&quiet, N_("be quiet, only report errors")),
> OPT_BOOL(0, "no-refresh", &no_refresh,
> @@ -371,8 +371,8 @@ int cmd_reset(int argc,
> PARSE_OPT_OPTARG,
> option_parse_recurse_submodules_worktree_updater),
> OPT_BOOL('p', "patch", &patch_mode, N_("select hunks interactively")),
> - OPT_DIFF_UNIFIED(&add_p_opt.context),
> - OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
> + OPT_DIFF_UNIFIED(&interactive_opts.context),
> + OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
> OPT_BOOL('N', "intent-to-add", &intent_to_add,
> N_("record only the fact that removed paths will be added later")),
> OPT_PATHSPEC_FROM_FILE(&pathspec_from_file),
> @@ -423,9 +423,9 @@ int cmd_reset(int argc,
> oidcpy(&oid, &tree->object.oid);
> }
>
> - if (add_p_opt.context < -1)
> + if (interactive_opts.context < -1)
> die(_("'%s' cannot be negative"), "--unified");
> - if (add_p_opt.interhunkcontext < -1)
> + if (interactive_opts.interhunkcontext < -1)
> die(_("'%s' cannot be negative"), "--inter-hunk-context");
>
> prepare_repo_settings(the_repository);
> @@ -436,12 +436,12 @@ int cmd_reset(int argc,
> die(_("options '%s' and '%s' cannot be used together"), "--patch", "--{hard,mixed,soft}");
> trace2_cmd_mode("patch-interactive");
> update_ref_status = !!run_add_p(the_repository, ADD_P_RESET,
> - &add_p_opt, rev, &pathspec);
> + &interactive_opts, rev, &pathspec);
> goto cleanup;
> } else {
> - if (add_p_opt.context != -1)
> + if (interactive_opts.context != -1)
> die(_("the option '%s' requires '%s'"), "--unified", "--patch");
> - if (add_p_opt.interhunkcontext != -1)
> + if (interactive_opts.interhunkcontext != -1)
> die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--patch");
> }
>
> diff --git a/builtin/stash.c b/builtin/stash.c
> index 948eba06fbc..3b509052338 100644
> --- a/builtin/stash.c
> +++ b/builtin/stash.c
> @@ -1306,7 +1306,7 @@ static int stash_staged(struct stash_info *info, struct strbuf *out_patch,
>
> static int stash_patch(struct stash_info *info, const struct pathspec *ps,
> struct strbuf *out_patch, int quiet,
> - struct add_p_opt *add_p_opt)
> + struct interactive_options *interactive_opts)
> {
> int ret = 0;
> struct child_process cp_read_tree = CHILD_PROCESS_INIT;
> @@ -1331,7 +1331,7 @@ static int stash_patch(struct stash_info *info, const struct pathspec *ps,
> old_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT));
> setenv(INDEX_ENVIRONMENT, the_repository->index_file, 1);
>
> - ret = !!run_add_p(the_repository, ADD_P_STASH, add_p_opt, NULL, ps);
> + ret = !!run_add_p(the_repository, ADD_P_STASH, interactive_opts, NULL, ps);
>
> the_repository->index_file = old_repo_index_file;
> if (old_index_env && *old_index_env)
> @@ -1427,7 +1427,8 @@ static int stash_working_tree(struct stash_info *info, const struct pathspec *ps
> }
>
> static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_buf,
> - int include_untracked, int patch_mode, struct add_p_opt *add_p_opt,
> + int include_untracked, int patch_mode,
> + struct interactive_options *interactive_opts,
> int only_staged, struct stash_info *info, struct strbuf *patch,
> int quiet)
> {
> @@ -1509,7 +1510,7 @@ static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_b
> untracked_commit_option = 1;
> }
> if (patch_mode) {
> - ret = stash_patch(info, ps, patch, quiet, add_p_opt);
> + ret = stash_patch(info, ps, patch, quiet, interactive_opts);
> if (ret < 0) {
> if (!quiet)
> fprintf_ln(stderr, _("Cannot save the current "
> @@ -1595,7 +1596,8 @@ static int create_stash(int argc, const char **argv, const char *prefix UNUSED,
> }
>
> static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int quiet,
> - int keep_index, int patch_mode, struct add_p_opt *add_p_opt,
> + int keep_index, int patch_mode,
> + struct interactive_options *interactive_opts,
> int include_untracked, int only_staged)
> {
> int ret = 0;
> @@ -1667,7 +1669,7 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q
> if (stash_msg)
> strbuf_addstr(&stash_msg_buf, stash_msg);
> if (do_create_stash(ps, &stash_msg_buf, include_untracked, patch_mode,
> - add_p_opt, only_staged, &info, &patch, quiet)) {
> + interactive_opts, only_staged, &info, &patch, quiet)) {
> ret = -1;
> goto done;
> }
> @@ -1841,7 +1843,7 @@ static int push_stash(int argc, const char **argv, const char *prefix,
> const char *stash_msg = NULL;
> char *pathspec_from_file = NULL;
> struct pathspec ps;
> - struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
> + struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
> struct option options[] = {
> OPT_BOOL('k', "keep-index", &keep_index,
> N_("keep index")),
> @@ -1849,8 +1851,8 @@ static int push_stash(int argc, const char **argv, const char *prefix,
> N_("stash staged changes only")),
> OPT_BOOL('p', "patch", &patch_mode,
> N_("stash in patch mode")),
> - OPT_DIFF_UNIFIED(&add_p_opt.context),
> - OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
> + OPT_DIFF_UNIFIED(&interactive_opts.context),
> + OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
> OPT__QUIET(&quiet, N_("quiet mode")),
> OPT_BOOL('u', "include-untracked", &include_untracked,
> N_("include untracked files in stash")),
> @@ -1907,19 +1909,19 @@ static int push_stash(int argc, const char **argv, const char *prefix,
> }
>
> if (!patch_mode) {
> - if (add_p_opt.context != -1)
> + if (interactive_opts.context != -1)
> die(_("the option '%s' requires '%s'"), "--unified", "--patch");
> - if (add_p_opt.interhunkcontext != -1)
> + if (interactive_opts.interhunkcontext != -1)
> die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--patch");
> }
>
> - if (add_p_opt.context < -1)
> + if (interactive_opts.context < -1)
> die(_("'%s' cannot be negative"), "--unified");
> - if (add_p_opt.interhunkcontext < -1)
> + if (interactive_opts.interhunkcontext < -1)
> die(_("'%s' cannot be negative"), "--inter-hunk-context");
>
> ret = do_push_stash(&ps, stash_msg, quiet, keep_index, patch_mode,
> - &add_p_opt, include_untracked, only_staged);
> + &interactive_opts, include_untracked, only_staged);
>
> clear_pathspec(&ps);
> free(pathspec_from_file);
> @@ -1944,7 +1946,7 @@ static int save_stash(int argc, const char **argv, const char *prefix,
> const char *stash_msg = NULL;
> struct pathspec ps;
> struct strbuf stash_msg_buf = STRBUF_INIT;
> - struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
> + struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
> struct option options[] = {
> OPT_BOOL('k', "keep-index", &keep_index,
> N_("keep index")),
> @@ -1952,8 +1954,8 @@ static int save_stash(int argc, const char **argv, const char *prefix,
> N_("stash staged changes only")),
> OPT_BOOL('p', "patch", &patch_mode,
> N_("stash in patch mode")),
> - OPT_DIFF_UNIFIED(&add_p_opt.context),
> - OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
> + OPT_DIFF_UNIFIED(&interactive_opts.context),
> + OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
> OPT__QUIET(&quiet, N_("quiet mode")),
> OPT_BOOL('u', "include-untracked", &include_untracked,
> N_("include untracked files in stash")),
> @@ -1973,20 +1975,20 @@ static int save_stash(int argc, const char **argv, const char *prefix,
>
> memset(&ps, 0, sizeof(ps));
>
> - if (add_p_opt.context < -1)
> + if (interactive_opts.context < -1)
> die(_("'%s' cannot be negative"), "--unified");
> - if (add_p_opt.interhunkcontext < -1)
> + if (interactive_opts.interhunkcontext < -1)
> die(_("'%s' cannot be negative"), "--inter-hunk-context");
>
> if (!patch_mode) {
> - if (add_p_opt.context != -1)
> + if (interactive_opts.context != -1)
> die(_("the option '%s' requires '%s'"), "--unified", "--patch");
> - if (add_p_opt.interhunkcontext != -1)
> + if (interactive_opts.interhunkcontext != -1)
> die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--patch");
> }
>
> ret = do_push_stash(&ps, stash_msg, quiet, keep_index,
> - patch_mode, &add_p_opt, include_untracked,
> + patch_mode, &interactive_opts, include_untracked,
> only_staged);
>
> strbuf_release(&stash_msg_buf);
> diff --git a/commit.h b/commit.h
> index 1d6e0c7518b..7b6e59d6c19 100644
> --- a/commit.h
> +++ b/commit.h
> @@ -258,7 +258,7 @@ int for_each_commit_graft(each_commit_graft_fn, void *);
> int interactive_add(struct repository *repo,
> const char **argv,
> const char *prefix,
> - int patch, struct add_p_opt *add_p_opt);
> + int patch, struct interactive_options *opts);
>
> struct commit_extra_header {
> struct commit_extra_header *next;
>
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH v6 07/11] add-patch: split out `struct interactive_options`
2025-11-20 15:05 ` Phillip Wood
@ 2025-12-02 18:48 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-12-02 18:48 UTC (permalink / raw)
To: phillip.wood
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
On Thu, Nov 20, 2025 at 03:05:17PM +0000, Phillip Wood wrote:
> On 27/10/2025 11:33, Patrick Steinhardt wrote:
> > Refactor the code and split out a new `struct interactive_options` that
> > hosts common options used by both. These options are then applied to a
> > `struct interactive_config` that hosts common configuration.
>
> I'm a little skeptical about renaming "sturct add_p_opt" as it only holds
> members that are relavent to run_add_p(). Also if we're trying to draw clear
> boundaries between the two subsystems hosting "struct interactive_options"
> and "struct interactive_config" in add-patch.c rather than add-interactive.c
> is potentially confusing.
I didn't want to add it to "add-interactive.c" though because of the
direction of the dependency: "add-interactive.c" will depend on
"add-patch.c", not the other way round.
We could of course split out the new options into a separate file
altogether. But that felt a bit heavy-handed to me.
> > This refactoring doesn't yet fully detangle the two subsystems from one
> > another, as we still end up calling `init_add_i_state()` in the "git add
> > -p" subsystem. This will be fixed in a subsequent commit.
>
> I think the ultimate aim of not having to initalize a "struct add_i_state"
> in run_add_p() is a good idea. I'm not sure though that having to pass a
> "struct interactive_options" to run_add_p() is any less confusing than
> having to pass a "struct add_p_opt" to run_add_i().
I agree that the end result is still a bit confusing. But what I wanted
to achieve is that at least the two subsystems are clearly separated
from so that one doesn't have to wonder anymore which parts interact
with one another. It was extremely puzzling to me at first, and the end
result here is significantly easier to understand from my point of view.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH v6 08/11] add-patch: remove dependency on "add-interactive" subsystem
2025-10-27 11:33 ` [PATCH v6 00/11] " Patrick Steinhardt
` (6 preceding siblings ...)
2025-10-27 11:33 ` [PATCH v6 07/11] add-patch: split out `struct interactive_options` Patrick Steinhardt
@ 2025-10-27 11:33 ` Patrick Steinhardt
2025-11-20 7:03 ` Elijah Newren
2025-11-20 15:05 ` Phillip Wood
2025-10-27 11:33 ` [PATCH v6 09/11] add-patch: add support for in-memory index patching Patrick Steinhardt
` (4 subsequent siblings)
12 siblings, 2 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-27 11:33 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
With the preceding commit we have split out interactive configuration
that is used by both "git add -p" and "git add -i". But we still
initialize that configuration in the "add -p" subsystem by calling
`init_add_i_state()`, even though we only do so to initialize the
interactive configuration as well as a repository pointer.
Stop doing so and instead store and initialize the interactive
configuration in `struct add_p_state` directly.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
add-patch.c | 70 ++++++++++++++++++++++++++++++++-----------------------------
1 file changed, 37 insertions(+), 33 deletions(-)
diff --git a/add-patch.c b/add-patch.c
index 5c6969927a..790c848e79 100644
--- a/add-patch.c
+++ b/add-patch.c
@@ -2,7 +2,6 @@
#define DISABLE_SIGN_COMPARE_WARNINGS
#include "git-compat-util.h"
-#include "add-interactive.h"
#include "add-patch.h"
#include "advice.h"
#include "config.h"
@@ -263,7 +262,8 @@ struct hunk {
};
struct add_p_state {
- struct add_i_state s;
+ struct repository *r;
+ struct interactive_config cfg;
struct strbuf answer, buf;
/* parsed diff */
@@ -408,7 +408,7 @@ static void add_p_state_clear(struct add_p_state *s)
for (i = 0; i < s->file_diff_nr; i++)
free(s->file_diff[i].hunk);
free(s->file_diff);
- clear_add_i_state(&s->s);
+ interactive_config_clear(&s->cfg);
}
__attribute__((format (printf, 2, 3)))
@@ -417,9 +417,9 @@ static void err(struct add_p_state *s, const char *fmt, ...)
va_list args;
va_start(args, fmt);
- fputs(s->s.cfg.error_color, stdout);
+ fputs(s->cfg.error_color, stdout);
vprintf(fmt, args);
- puts(s->s.cfg.reset_color_interactive);
+ puts(s->cfg.reset_color_interactive);
va_end(args);
}
@@ -437,7 +437,7 @@ static void setup_child_process(struct add_p_state *s,
cp->git_cmd = 1;
strvec_pushf(&cp->env,
- INDEX_ENVIRONMENT "=%s", s->s.r->index_file);
+ INDEX_ENVIRONMENT "=%s", s->r->index_file);
}
static int parse_range(const char **p,
@@ -542,12 +542,12 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
int res;
strvec_pushv(&args, s->mode->diff_cmd);
- if (s->s.cfg.context != -1)
- strvec_pushf(&args, "--unified=%i", s->s.cfg.context);
- if (s->s.cfg.interhunkcontext != -1)
- strvec_pushf(&args, "--inter-hunk-context=%i", s->s.cfg.interhunkcontext);
- if (s->s.cfg.interactive_diff_algorithm)
- strvec_pushf(&args, "--diff-algorithm=%s", s->s.cfg.interactive_diff_algorithm);
+ if (s->cfg.context != -1)
+ strvec_pushf(&args, "--unified=%i", s->cfg.context);
+ if (s->cfg.interhunkcontext != -1)
+ strvec_pushf(&args, "--inter-hunk-context=%i", s->cfg.interhunkcontext);
+ if (s->cfg.interactive_diff_algorithm)
+ strvec_pushf(&args, "--diff-algorithm=%s", s->cfg.interactive_diff_algorithm);
if (s->revision) {
struct object_id oid;
strvec_push(&args,
@@ -576,9 +576,9 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
}
strbuf_complete_line(plain);
- if (want_color_fd(1, s->s.cfg.use_color_diff)) {
+ if (want_color_fd(1, s->cfg.use_color_diff)) {
struct child_process colored_cp = CHILD_PROCESS_INIT;
- const char *diff_filter = s->s.cfg.interactive_diff_filter;
+ const char *diff_filter = s->cfg.interactive_diff_filter;
setup_child_process(s, &colored_cp, NULL);
xsnprintf((char *)args.v[color_arg_index], 8, "--color");
@@ -811,7 +811,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
hunk->colored_end - hunk->colored_start);
return;
} else {
- strbuf_addstr(out, s->s.cfg.fraginfo_color);
+ strbuf_addstr(out, s->cfg.fraginfo_color);
p = s->colored.buf + header->colored_extra_start;
len = header->colored_extra_end
- header->colored_extra_start;
@@ -833,7 +833,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
if (len)
strbuf_add(out, p, len);
else if (colored)
- strbuf_addf(out, "%s\n", s->s.cfg.reset_color_diff);
+ strbuf_addf(out, "%s\n", s->cfg.reset_color_diff);
else
strbuf_addch(out, '\n');
}
@@ -1222,12 +1222,12 @@ static void recolor_hunk(struct add_p_state *s, struct hunk *hunk)
strbuf_addstr(&s->colored,
plain[current] == '-' ?
- s->s.cfg.file_old_color :
+ s->cfg.file_old_color :
plain[current] == '+' ?
- s->s.cfg.file_new_color :
- s->s.cfg.context_color);
+ s->cfg.file_new_color :
+ s->cfg.context_color);
strbuf_add(&s->colored, plain + current, eol - current);
- strbuf_addstr(&s->colored, s->s.cfg.reset_color_diff);
+ strbuf_addstr(&s->colored, s->cfg.reset_color_diff);
if (next > eol)
strbuf_add(&s->colored, plain + eol, next - eol);
current = next;
@@ -1356,7 +1356,7 @@ static int run_apply_check(struct add_p_state *s,
static int read_single_character(struct add_p_state *s)
{
- if (s->s.cfg.use_single_key) {
+ if (s->cfg.use_single_key) {
int res = read_key_without_echo(&s->answer);
printf("%s\n", res == EOF ? "" : s->answer.buf);
return res;
@@ -1370,7 +1370,7 @@ static int read_single_character(struct add_p_state *s)
static int prompt_yesno(struct add_p_state *s, const char *prompt)
{
for (;;) {
- color_fprintf(stdout, s->s.cfg.prompt_color, "%s", _(prompt));
+ color_fprintf(stdout, s->cfg.prompt_color, "%s", _(prompt));
fflush(stdout);
if (read_single_character(s) == EOF)
return -1;
@@ -1678,15 +1678,15 @@ static int patch_update_file(struct add_p_state *s,
else
prompt_mode_type = PROMPT_HUNK;
- printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->s.cfg.prompt_color,
+ printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->cfg.prompt_color,
(uintmax_t)hunk_index + 1,
(uintmax_t)(file_diff->hunk_nr
? file_diff->hunk_nr
: 1));
printf(_(s->mode->prompt_mode[prompt_mode_type]),
s->buf.buf);
- if (*s->s.cfg.reset_color_interactive)
- fputs(s->s.cfg.reset_color_interactive, stdout);
+ if (*s->cfg.reset_color_interactive)
+ fputs(s->cfg.reset_color_interactive, stdout);
fflush(stdout);
if (read_single_character(s) == EOF)
break;
@@ -1848,7 +1848,7 @@ static int patch_update_file(struct add_p_state *s,
err(s, _("Sorry, cannot split this hunk"));
} else if (!split_hunk(s, file_diff,
hunk - file_diff->hunk)) {
- color_fprintf_ln(stdout, s->s.cfg.header_color,
+ color_fprintf_ln(stdout, s->cfg.header_color,
_("Split into %d hunks."),
(int)splittable_into);
rendered_hunk_index = -1;
@@ -1866,7 +1866,7 @@ static int patch_update_file(struct add_p_state *s,
} else if (s->answer.buf[0] == '?') {
const char *p = _(help_patch_remainder), *eol = p;
- color_fprintf(stdout, s->s.cfg.help_color, "%s",
+ color_fprintf(stdout, s->cfg.help_color, "%s",
_(s->mode->help_patch_text));
/*
@@ -1884,7 +1884,7 @@ static int patch_update_file(struct add_p_state *s,
if (*p != '?' && !strchr(s->buf.buf, *p))
continue;
- color_fprintf_ln(stdout, s->s.cfg.help_color,
+ color_fprintf_ln(stdout, s->cfg.help_color,
"%.*s", (int)(eol - p), p);
}
} else {
@@ -1904,7 +1904,7 @@ static int patch_update_file(struct add_p_state *s,
strbuf_reset(&s->buf);
reassemble_patch(s, file_diff, 0, &s->buf);
- discard_index(s->s.r->index);
+ discard_index(s->r->index);
if (s->mode->apply_for_checkout)
apply_for_checkout(s, &s->buf,
s->mode->is_reverse);
@@ -1915,8 +1915,8 @@ static int patch_update_file(struct add_p_state *s,
NULL, 0, NULL, 0))
error(_("'git apply' failed"));
}
- if (repo_read_index(s->s.r) >= 0)
- repo_refresh_and_write_index(s->s.r, REFRESH_QUIET, 0,
+ if (repo_read_index(s->r) >= 0)
+ repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,
1, NULL, NULL, NULL);
}
@@ -1929,11 +1929,15 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
const struct pathspec *ps)
{
struct add_p_state s = {
- { r }, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
+ .r = r,
+ .answer = STRBUF_INIT,
+ .buf = STRBUF_INIT,
+ .plain = STRBUF_INIT,
+ .colored = STRBUF_INIT,
};
size_t i, binary_count = 0;
- init_add_i_state(&s.s, r, opts);
+ interactive_config_init(&s.cfg, r, opts);
if (mode == ADD_P_STASH)
s.mode = &patch_mode_stash;
--
2.51.1.930.gacf6e81ea2.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH v6 08/11] add-patch: remove dependency on "add-interactive" subsystem
2025-10-27 11:33 ` [PATCH v6 08/11] add-patch: remove dependency on "add-interactive" subsystem Patrick Steinhardt
@ 2025-11-20 7:03 ` Elijah Newren
2025-11-20 15:05 ` Phillip Wood
1 sibling, 0 replies; 278+ messages in thread
From: Elijah Newren @ 2025-11-20 7:03 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Karthik Nayak
On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:
>
> With the preceding commit we have split out interactive configuration
> that is used by both "git add -p" and "git add -i". But we still
> initialize that configuration in the "add -p" subsystem by calling
> `init_add_i_state()`, even though we only do so to initialize the
> interactive configuration as well as a repository pointer.
>
> Stop doing so and instead store and initialize the interactive
> configuration in `struct add_p_state` directly.
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> add-patch.c | 70 ++++++++++++++++++++++++++++++++-----------------------------
> 1 file changed, 37 insertions(+), 33 deletions(-)
>
> diff --git a/add-patch.c b/add-patch.c
> index 5c6969927a..790c848e79 100644
> --- a/add-patch.c
> +++ b/add-patch.c
> @@ -2,7 +2,6 @@
> #define DISABLE_SIGN_COMPARE_WARNINGS
>
> #include "git-compat-util.h"
> -#include "add-interactive.h"
> #include "add-patch.h"
> #include "advice.h"
> #include "config.h"
> @@ -263,7 +262,8 @@ struct hunk {
> };
>
> struct add_p_state {
> - struct add_i_state s;
> + struct repository *r;
> + struct interactive_config cfg;
> struct strbuf answer, buf;
>
> /* parsed diff */
> @@ -408,7 +408,7 @@ static void add_p_state_clear(struct add_p_state *s)
> for (i = 0; i < s->file_diff_nr; i++)
> free(s->file_diff[i].hunk);
> free(s->file_diff);
> - clear_add_i_state(&s->s);
> + interactive_config_clear(&s->cfg);
> }
>
> __attribute__((format (printf, 2, 3)))
> @@ -417,9 +417,9 @@ static void err(struct add_p_state *s, const char *fmt, ...)
> va_list args;
>
> va_start(args, fmt);
> - fputs(s->s.cfg.error_color, stdout);
> + fputs(s->cfg.error_color, stdout);
> vprintf(fmt, args);
> - puts(s->s.cfg.reset_color_interactive);
> + puts(s->cfg.reset_color_interactive);
> va_end(args);
> }
>
> @@ -437,7 +437,7 @@ static void setup_child_process(struct add_p_state *s,
>
> cp->git_cmd = 1;
> strvec_pushf(&cp->env,
> - INDEX_ENVIRONMENT "=%s", s->s.r->index_file);
> + INDEX_ENVIRONMENT "=%s", s->r->index_file);
> }
>
> static int parse_range(const char **p,
> @@ -542,12 +542,12 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
> int res;
>
> strvec_pushv(&args, s->mode->diff_cmd);
> - if (s->s.cfg.context != -1)
> - strvec_pushf(&args, "--unified=%i", s->s.cfg.context);
> - if (s->s.cfg.interhunkcontext != -1)
> - strvec_pushf(&args, "--inter-hunk-context=%i", s->s.cfg.interhunkcontext);
> - if (s->s.cfg.interactive_diff_algorithm)
> - strvec_pushf(&args, "--diff-algorithm=%s", s->s.cfg.interactive_diff_algorithm);
> + if (s->cfg.context != -1)
> + strvec_pushf(&args, "--unified=%i", s->cfg.context);
> + if (s->cfg.interhunkcontext != -1)
> + strvec_pushf(&args, "--inter-hunk-context=%i", s->cfg.interhunkcontext);
> + if (s->cfg.interactive_diff_algorithm)
> + strvec_pushf(&args, "--diff-algorithm=%s", s->cfg.interactive_diff_algorithm);
> if (s->revision) {
> struct object_id oid;
> strvec_push(&args,
> @@ -576,9 +576,9 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
> }
> strbuf_complete_line(plain);
>
> - if (want_color_fd(1, s->s.cfg.use_color_diff)) {
> + if (want_color_fd(1, s->cfg.use_color_diff)) {
> struct child_process colored_cp = CHILD_PROCESS_INIT;
> - const char *diff_filter = s->s.cfg.interactive_diff_filter;
> + const char *diff_filter = s->cfg.interactive_diff_filter;
>
> setup_child_process(s, &colored_cp, NULL);
> xsnprintf((char *)args.v[color_arg_index], 8, "--color");
> @@ -811,7 +811,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
> hunk->colored_end - hunk->colored_start);
> return;
> } else {
> - strbuf_addstr(out, s->s.cfg.fraginfo_color);
> + strbuf_addstr(out, s->cfg.fraginfo_color);
> p = s->colored.buf + header->colored_extra_start;
> len = header->colored_extra_end
> - header->colored_extra_start;
> @@ -833,7 +833,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
> if (len)
> strbuf_add(out, p, len);
> else if (colored)
> - strbuf_addf(out, "%s\n", s->s.cfg.reset_color_diff);
> + strbuf_addf(out, "%s\n", s->cfg.reset_color_diff);
> else
> strbuf_addch(out, '\n');
> }
> @@ -1222,12 +1222,12 @@ static void recolor_hunk(struct add_p_state *s, struct hunk *hunk)
>
> strbuf_addstr(&s->colored,
> plain[current] == '-' ?
> - s->s.cfg.file_old_color :
> + s->cfg.file_old_color :
> plain[current] == '+' ?
> - s->s.cfg.file_new_color :
> - s->s.cfg.context_color);
> + s->cfg.file_new_color :
> + s->cfg.context_color);
> strbuf_add(&s->colored, plain + current, eol - current);
> - strbuf_addstr(&s->colored, s->s.cfg.reset_color_diff);
> + strbuf_addstr(&s->colored, s->cfg.reset_color_diff);
> if (next > eol)
> strbuf_add(&s->colored, plain + eol, next - eol);
> current = next;
> @@ -1356,7 +1356,7 @@ static int run_apply_check(struct add_p_state *s,
>
> static int read_single_character(struct add_p_state *s)
> {
> - if (s->s.cfg.use_single_key) {
> + if (s->cfg.use_single_key) {
> int res = read_key_without_echo(&s->answer);
> printf("%s\n", res == EOF ? "" : s->answer.buf);
> return res;
> @@ -1370,7 +1370,7 @@ static int read_single_character(struct add_p_state *s)
> static int prompt_yesno(struct add_p_state *s, const char *prompt)
> {
> for (;;) {
> - color_fprintf(stdout, s->s.cfg.prompt_color, "%s", _(prompt));
> + color_fprintf(stdout, s->cfg.prompt_color, "%s", _(prompt));
> fflush(stdout);
> if (read_single_character(s) == EOF)
> return -1;
> @@ -1678,15 +1678,15 @@ static int patch_update_file(struct add_p_state *s,
> else
> prompt_mode_type = PROMPT_HUNK;
>
> - printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->s.cfg.prompt_color,
> + printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->cfg.prompt_color,
> (uintmax_t)hunk_index + 1,
> (uintmax_t)(file_diff->hunk_nr
> ? file_diff->hunk_nr
> : 1));
> printf(_(s->mode->prompt_mode[prompt_mode_type]),
> s->buf.buf);
> - if (*s->s.cfg.reset_color_interactive)
> - fputs(s->s.cfg.reset_color_interactive, stdout);
> + if (*s->cfg.reset_color_interactive)
> + fputs(s->cfg.reset_color_interactive, stdout);
> fflush(stdout);
> if (read_single_character(s) == EOF)
> break;
> @@ -1848,7 +1848,7 @@ static int patch_update_file(struct add_p_state *s,
> err(s, _("Sorry, cannot split this hunk"));
> } else if (!split_hunk(s, file_diff,
> hunk - file_diff->hunk)) {
> - color_fprintf_ln(stdout, s->s.cfg.header_color,
> + color_fprintf_ln(stdout, s->cfg.header_color,
> _("Split into %d hunks."),
> (int)splittable_into);
> rendered_hunk_index = -1;
> @@ -1866,7 +1866,7 @@ static int patch_update_file(struct add_p_state *s,
> } else if (s->answer.buf[0] == '?') {
> const char *p = _(help_patch_remainder), *eol = p;
>
> - color_fprintf(stdout, s->s.cfg.help_color, "%s",
> + color_fprintf(stdout, s->cfg.help_color, "%s",
> _(s->mode->help_patch_text));
>
> /*
> @@ -1884,7 +1884,7 @@ static int patch_update_file(struct add_p_state *s,
> if (*p != '?' && !strchr(s->buf.buf, *p))
> continue;
>
> - color_fprintf_ln(stdout, s->s.cfg.help_color,
> + color_fprintf_ln(stdout, s->cfg.help_color,
> "%.*s", (int)(eol - p), p);
> }
> } else {
> @@ -1904,7 +1904,7 @@ static int patch_update_file(struct add_p_state *s,
> strbuf_reset(&s->buf);
> reassemble_patch(s, file_diff, 0, &s->buf);
>
> - discard_index(s->s.r->index);
> + discard_index(s->r->index);
> if (s->mode->apply_for_checkout)
> apply_for_checkout(s, &s->buf,
> s->mode->is_reverse);
> @@ -1915,8 +1915,8 @@ static int patch_update_file(struct add_p_state *s,
> NULL, 0, NULL, 0))
> error(_("'git apply' failed"));
> }
> - if (repo_read_index(s->s.r) >= 0)
> - repo_refresh_and_write_index(s->s.r, REFRESH_QUIET, 0,
> + if (repo_read_index(s->r) >= 0)
> + repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,
> 1, NULL, NULL, NULL);
> }
>
> @@ -1929,11 +1929,15 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
> const struct pathspec *ps)
> {
> struct add_p_state s = {
> - { r }, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
> + .r = r,
> + .answer = STRBUF_INIT,
> + .buf = STRBUF_INIT,
> + .plain = STRBUF_INIT,
> + .colored = STRBUF_INIT,
> };
> size_t i, binary_count = 0;
>
> - init_add_i_state(&s.s, r, opts);
> + interactive_config_init(&s.cfg, r, opts);
>
> if (mode == ADD_P_STASH)
> s.mode = &patch_mode_stash;
>
> --
> 2.51.1.930.gacf6e81ea2.dirty
Viewing the patch under --color-words=. makes it clear that in most
places in the patch you were just removing "s." for accessing fields.
Just the beginning and end of the patch are better viewed without that
field. Anyway, all looks reasonable to me.
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH v6 08/11] add-patch: remove dependency on "add-interactive" subsystem
2025-10-27 11:33 ` [PATCH v6 08/11] add-patch: remove dependency on "add-interactive" subsystem Patrick Steinhardt
2025-11-20 7:03 ` Elijah Newren
@ 2025-11-20 15:05 ` Phillip Wood
1 sibling, 0 replies; 278+ messages in thread
From: Phillip Wood @ 2025-11-20 15:05 UTC (permalink / raw)
To: Patrick Steinhardt, git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
On 27/10/2025 11:33, Patrick Steinhardt wrote:
> With the preceding commit we have split out interactive configuration
> that is used by both "git add -p" and "git add -i". But we still
> initialize that configuration in the "add -p" subsystem by calling
> `init_add_i_state()`, even though we only do so to initialize the
> interactive configuration as well as a repository pointer.
>
> Stop doing so and instead store and initialize the interactive
> configuration in `struct add_p_state` directly.
Makes sense
Thanks
Phillip
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> add-patch.c | 70 ++++++++++++++++++++++++++++++++-----------------------------
> 1 file changed, 37 insertions(+), 33 deletions(-)
>
> diff --git a/add-patch.c b/add-patch.c
> index 5c6969927a..790c848e79 100644
> --- a/add-patch.c
> +++ b/add-patch.c
> @@ -2,7 +2,6 @@
> #define DISABLE_SIGN_COMPARE_WARNINGS
>
> #include "git-compat-util.h"
> -#include "add-interactive.h"
> #include "add-patch.h"
> #include "advice.h"
> #include "config.h"
> @@ -263,7 +262,8 @@ struct hunk {
> };
>
> struct add_p_state {
> - struct add_i_state s;
> + struct repository *r;
> + struct interactive_config cfg;
> struct strbuf answer, buf;
>
> /* parsed diff */
> @@ -408,7 +408,7 @@ static void add_p_state_clear(struct add_p_state *s)
> for (i = 0; i < s->file_diff_nr; i++)
> free(s->file_diff[i].hunk);
> free(s->file_diff);
> - clear_add_i_state(&s->s);
> + interactive_config_clear(&s->cfg);
> }
>
> __attribute__((format (printf, 2, 3)))
> @@ -417,9 +417,9 @@ static void err(struct add_p_state *s, const char *fmt, ...)
> va_list args;
>
> va_start(args, fmt);
> - fputs(s->s.cfg.error_color, stdout);
> + fputs(s->cfg.error_color, stdout);
> vprintf(fmt, args);
> - puts(s->s.cfg.reset_color_interactive);
> + puts(s->cfg.reset_color_interactive);
> va_end(args);
> }
>
> @@ -437,7 +437,7 @@ static void setup_child_process(struct add_p_state *s,
>
> cp->git_cmd = 1;
> strvec_pushf(&cp->env,
> - INDEX_ENVIRONMENT "=%s", s->s.r->index_file);
> + INDEX_ENVIRONMENT "=%s", s->r->index_file);
> }
>
> static int parse_range(const char **p,
> @@ -542,12 +542,12 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
> int res;
>
> strvec_pushv(&args, s->mode->diff_cmd);
> - if (s->s.cfg.context != -1)
> - strvec_pushf(&args, "--unified=%i", s->s.cfg.context);
> - if (s->s.cfg.interhunkcontext != -1)
> - strvec_pushf(&args, "--inter-hunk-context=%i", s->s.cfg.interhunkcontext);
> - if (s->s.cfg.interactive_diff_algorithm)
> - strvec_pushf(&args, "--diff-algorithm=%s", s->s.cfg.interactive_diff_algorithm);
> + if (s->cfg.context != -1)
> + strvec_pushf(&args, "--unified=%i", s->cfg.context);
> + if (s->cfg.interhunkcontext != -1)
> + strvec_pushf(&args, "--inter-hunk-context=%i", s->cfg.interhunkcontext);
> + if (s->cfg.interactive_diff_algorithm)
> + strvec_pushf(&args, "--diff-algorithm=%s", s->cfg.interactive_diff_algorithm);
> if (s->revision) {
> struct object_id oid;
> strvec_push(&args,
> @@ -576,9 +576,9 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
> }
> strbuf_complete_line(plain);
>
> - if (want_color_fd(1, s->s.cfg.use_color_diff)) {
> + if (want_color_fd(1, s->cfg.use_color_diff)) {
> struct child_process colored_cp = CHILD_PROCESS_INIT;
> - const char *diff_filter = s->s.cfg.interactive_diff_filter;
> + const char *diff_filter = s->cfg.interactive_diff_filter;
>
> setup_child_process(s, &colored_cp, NULL);
> xsnprintf((char *)args.v[color_arg_index], 8, "--color");
> @@ -811,7 +811,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
> hunk->colored_end - hunk->colored_start);
> return;
> } else {
> - strbuf_addstr(out, s->s.cfg.fraginfo_color);
> + strbuf_addstr(out, s->cfg.fraginfo_color);
> p = s->colored.buf + header->colored_extra_start;
> len = header->colored_extra_end
> - header->colored_extra_start;
> @@ -833,7 +833,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
> if (len)
> strbuf_add(out, p, len);
> else if (colored)
> - strbuf_addf(out, "%s\n", s->s.cfg.reset_color_diff);
> + strbuf_addf(out, "%s\n", s->cfg.reset_color_diff);
> else
> strbuf_addch(out, '\n');
> }
> @@ -1222,12 +1222,12 @@ static void recolor_hunk(struct add_p_state *s, struct hunk *hunk)
>
> strbuf_addstr(&s->colored,
> plain[current] == '-' ?
> - s->s.cfg.file_old_color :
> + s->cfg.file_old_color :
> plain[current] == '+' ?
> - s->s.cfg.file_new_color :
> - s->s.cfg.context_color);
> + s->cfg.file_new_color :
> + s->cfg.context_color);
> strbuf_add(&s->colored, plain + current, eol - current);
> - strbuf_addstr(&s->colored, s->s.cfg.reset_color_diff);
> + strbuf_addstr(&s->colored, s->cfg.reset_color_diff);
> if (next > eol)
> strbuf_add(&s->colored, plain + eol, next - eol);
> current = next;
> @@ -1356,7 +1356,7 @@ static int run_apply_check(struct add_p_state *s,
>
> static int read_single_character(struct add_p_state *s)
> {
> - if (s->s.cfg.use_single_key) {
> + if (s->cfg.use_single_key) {
> int res = read_key_without_echo(&s->answer);
> printf("%s\n", res == EOF ? "" : s->answer.buf);
> return res;
> @@ -1370,7 +1370,7 @@ static int read_single_character(struct add_p_state *s)
> static int prompt_yesno(struct add_p_state *s, const char *prompt)
> {
> for (;;) {
> - color_fprintf(stdout, s->s.cfg.prompt_color, "%s", _(prompt));
> + color_fprintf(stdout, s->cfg.prompt_color, "%s", _(prompt));
> fflush(stdout);
> if (read_single_character(s) == EOF)
> return -1;
> @@ -1678,15 +1678,15 @@ static int patch_update_file(struct add_p_state *s,
> else
> prompt_mode_type = PROMPT_HUNK;
>
> - printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->s.cfg.prompt_color,
> + printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->cfg.prompt_color,
> (uintmax_t)hunk_index + 1,
> (uintmax_t)(file_diff->hunk_nr
> ? file_diff->hunk_nr
> : 1));
> printf(_(s->mode->prompt_mode[prompt_mode_type]),
> s->buf.buf);
> - if (*s->s.cfg.reset_color_interactive)
> - fputs(s->s.cfg.reset_color_interactive, stdout);
> + if (*s->cfg.reset_color_interactive)
> + fputs(s->cfg.reset_color_interactive, stdout);
> fflush(stdout);
> if (read_single_character(s) == EOF)
> break;
> @@ -1848,7 +1848,7 @@ static int patch_update_file(struct add_p_state *s,
> err(s, _("Sorry, cannot split this hunk"));
> } else if (!split_hunk(s, file_diff,
> hunk - file_diff->hunk)) {
> - color_fprintf_ln(stdout, s->s.cfg.header_color,
> + color_fprintf_ln(stdout, s->cfg.header_color,
> _("Split into %d hunks."),
> (int)splittable_into);
> rendered_hunk_index = -1;
> @@ -1866,7 +1866,7 @@ static int patch_update_file(struct add_p_state *s,
> } else if (s->answer.buf[0] == '?') {
> const char *p = _(help_patch_remainder), *eol = p;
>
> - color_fprintf(stdout, s->s.cfg.help_color, "%s",
> + color_fprintf(stdout, s->cfg.help_color, "%s",
> _(s->mode->help_patch_text));
>
> /*
> @@ -1884,7 +1884,7 @@ static int patch_update_file(struct add_p_state *s,
> if (*p != '?' && !strchr(s->buf.buf, *p))
> continue;
>
> - color_fprintf_ln(stdout, s->s.cfg.help_color,
> + color_fprintf_ln(stdout, s->cfg.help_color,
> "%.*s", (int)(eol - p), p);
> }
> } else {
> @@ -1904,7 +1904,7 @@ static int patch_update_file(struct add_p_state *s,
> strbuf_reset(&s->buf);
> reassemble_patch(s, file_diff, 0, &s->buf);
>
> - discard_index(s->s.r->index);
> + discard_index(s->r->index);
> if (s->mode->apply_for_checkout)
> apply_for_checkout(s, &s->buf,
> s->mode->is_reverse);
> @@ -1915,8 +1915,8 @@ static int patch_update_file(struct add_p_state *s,
> NULL, 0, NULL, 0))
> error(_("'git apply' failed"));
> }
> - if (repo_read_index(s->s.r) >= 0)
> - repo_refresh_and_write_index(s->s.r, REFRESH_QUIET, 0,
> + if (repo_read_index(s->r) >= 0)
> + repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,
> 1, NULL, NULL, NULL);
> }
>
> @@ -1929,11 +1929,15 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
> const struct pathspec *ps)
> {
> struct add_p_state s = {
> - { r }, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
> + .r = r,
> + .answer = STRBUF_INIT,
> + .buf = STRBUF_INIT,
> + .plain = STRBUF_INIT,
> + .colored = STRBUF_INIT,
> };
> size_t i, binary_count = 0;
>
> - init_add_i_state(&s.s, r, opts);
> + interactive_config_init(&s.cfg, r, opts);
>
> if (mode == ADD_P_STASH)
> s.mode = &patch_mode_stash;
>
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH v6 09/11] add-patch: add support for in-memory index patching
2025-10-27 11:33 ` [PATCH v6 00/11] " Patrick Steinhardt
` (7 preceding siblings ...)
2025-10-27 11:33 ` [PATCH v6 08/11] add-patch: remove dependency on "add-interactive" subsystem Patrick Steinhardt
@ 2025-10-27 11:33 ` Patrick Steinhardt
2025-11-20 7:04 ` Elijah Newren
2025-10-27 11:33 ` [PATCH v6 10/11] cache-tree: allow writing in-memory index as tree Patrick Steinhardt
` (3 subsequent siblings)
12 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-27 11:33 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
With `run_add_p()` callers have the ability to apply changes from a
specific revision to a repository's index. This infra supports several
different modes, like for example applying changes to the index,
working tree or both.
One feature that is missing though is the ability to apply changes to an
in-memory index different from the repository's index. Add a new
function `run_add_p_index()` to plug this gap.
This new function will be used in a subsequent commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
add-patch.c | 112 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
add-patch.h | 8 +++++
2 files changed, 116 insertions(+), 4 deletions(-)
diff --git a/add-patch.c b/add-patch.c
index 790c848e799..9c1688bd5a0 100644
--- a/add-patch.c
+++ b/add-patch.c
@@ -4,11 +4,13 @@
#include "git-compat-util.h"
#include "add-patch.h"
#include "advice.h"
+#include "commit.h"
#include "config.h"
#include "diff.h"
#include "editor.h"
#include "environment.h"
#include "gettext.h"
+#include "hex.h"
#include "object-name.h"
#include "pager.h"
#include "read-cache-ll.h"
@@ -47,7 +49,7 @@ static struct patch_mode patch_mode_add = {
N_("Stage mode change [y,n,q,a,d%s,?]? "),
N_("Stage deletion [y,n,q,a,d%s,?]? "),
N_("Stage addition [y,n,q,a,d%s,?]? "),
- N_("Stage this hunk [y,n,q,a,d%s,?]? ")
+ N_("Stage this hunk [y,n,q,a,d%s,?]? "),
},
.edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk "
"will immediately be marked for staging."),
@@ -263,6 +265,8 @@ struct hunk {
struct add_p_state {
struct repository *r;
+ struct index_state *index;
+ const char *index_file;
struct interactive_config cfg;
struct strbuf answer, buf;
@@ -437,7 +441,7 @@ static void setup_child_process(struct add_p_state *s,
cp->git_cmd = 1;
strvec_pushf(&cp->env,
- INDEX_ENVIRONMENT "=%s", s->r->index_file);
+ INDEX_ENVIRONMENT "=%s", s->index_file);
}
static int parse_range(const char **p,
@@ -1904,7 +1908,7 @@ static int patch_update_file(struct add_p_state *s,
strbuf_reset(&s->buf);
reassemble_patch(s, file_diff, 0, &s->buf);
- discard_index(s->r->index);
+ discard_index(s->index);
if (s->mode->apply_for_checkout)
apply_for_checkout(s, &s->buf,
s->mode->is_reverse);
@@ -1915,9 +1919,11 @@ static int patch_update_file(struct add_p_state *s,
NULL, 0, NULL, 0))
error(_("'git apply' failed"));
}
- if (repo_read_index(s->r) >= 0)
+ if (read_index_from(s->index, s->index_file, s->r->gitdir) >= 0 &&
+ s->index == s->r->index) {
repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,
1, NULL, NULL, NULL);
+ }
}
putchar('\n');
@@ -1930,6 +1936,8 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
{
struct add_p_state s = {
.r = r,
+ .index = r->index,
+ .index_file = r->index_file,
.answer = STRBUF_INIT,
.buf = STRBUF_INIT,
.plain = STRBUF_INIT,
@@ -1988,3 +1996,99 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
add_p_state_clear(&s);
return 0;
}
+
+int run_add_p_index(struct repository *r,
+ struct index_state *index,
+ const char *index_file,
+ struct interactive_options *opts,
+ const char *revision,
+ const struct pathspec *ps)
+{
+ struct patch_mode mode = {
+ .apply_args = { "--cached", NULL },
+ .apply_check_args = { "--cached", NULL },
+ .prompt_mode = {
+ N_("Stage mode change [y,n,q,a,d%s,?]? "),
+ N_("Stage deletion [y,n,q,a,d%s,?]? "),
+ N_("Stage addition [y,n,q,a,d%s,?]? "),
+ N_("Stage this hunk [y,n,q,a,d%s,?]? ")
+ },
+ .edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk "
+ "will immediately be marked for staging."),
+ .help_patch_text =
+ N_("y - stage this hunk\n"
+ "n - do not stage this hunk\n"
+ "q - quit; do not stage this hunk or any of the remaining "
+ "ones\n"
+ "a - stage this hunk and all later hunks in the file\n"
+ "d - do not stage this hunk or any of the later hunks in "
+ "the file\n"),
+ .index_only = 1,
+ };
+ struct add_p_state s = {
+ .r = r,
+ .index = index,
+ .index_file = index_file,
+ .answer = STRBUF_INIT,
+ .buf = STRBUF_INIT,
+ .plain = STRBUF_INIT,
+ .colored = STRBUF_INIT,
+ .mode = &mode,
+ .revision = revision,
+ };
+ struct strbuf parent_revision = STRBUF_INIT;
+ char parent_tree_oid[GIT_MAX_HEXSZ + 1];
+ size_t binary_count = 0;
+ struct commit *commit;
+ int ret;
+
+ commit = lookup_commit_reference_by_name(revision);
+ if (!commit) {
+ err(&s, _("Revision does not refer to a commit"));
+ ret = -1;
+ goto out;
+ }
+
+ if (commit->parents)
+ oid_to_hex_r(parent_tree_oid, get_commit_tree_oid(commit->parents->item));
+ else
+ oid_to_hex_r(parent_tree_oid, r->hash_algo->empty_tree);
+
+ strbuf_addf(&parent_revision, "%s~", revision);
+ mode.diff_cmd[0] = "diff-tree";
+ mode.diff_cmd[1] = "-r";
+ mode.diff_cmd[2] = parent_tree_oid;
+
+ interactive_config_init(&s.cfg, r, opts);
+
+ if (parse_diff(&s, ps) < 0) {
+ ret = -1;
+ goto out;
+ }
+
+ for (size_t i = 0; i < s.file_diff_nr; i++) {
+ if (s.file_diff[i].binary && !s.file_diff[i].hunk_nr)
+ binary_count++;
+ else if (patch_update_file(&s, s.file_diff + i))
+ break;
+ }
+
+ if (s.file_diff_nr == 0) {
+ err(&s, _("No changes."));
+ ret = -1;
+ goto out;
+ }
+
+ if (binary_count == s.file_diff_nr) {
+ err(&s, _("Only binary files changed."));
+ ret = -1;
+ goto out;
+ }
+
+ ret = 0;
+
+out:
+ strbuf_release(&parent_revision);
+ add_p_state_clear(&s);
+ return ret;
+}
diff --git a/add-patch.h b/add-patch.h
index a4a05d9d145..901c42fd7b6 100644
--- a/add-patch.h
+++ b/add-patch.h
@@ -3,6 +3,7 @@
#include "color.h"
+struct index_state;
struct pathspec;
struct repository;
@@ -53,4 +54,11 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
struct interactive_options *opts, const char *revision,
const struct pathspec *ps);
+int run_add_p_index(struct repository *r,
+ struct index_state *index,
+ const char *index_file,
+ struct interactive_options *opts,
+ const char *revision,
+ const struct pathspec *ps);
+
#endif
--
2.51.1.930.gacf6e81ea2.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH v6 09/11] add-patch: add support for in-memory index patching
2025-10-27 11:33 ` [PATCH v6 09/11] add-patch: add support for in-memory index patching Patrick Steinhardt
@ 2025-11-20 7:04 ` Elijah Newren
2025-11-20 15:05 ` Phillip Wood
2025-12-02 18:49 ` Patrick Steinhardt
0 siblings, 2 replies; 278+ messages in thread
From: Elijah Newren @ 2025-11-20 7:04 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Karthik Nayak
On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:
> +int run_add_p_index(struct repository *r,
> + struct index_state *index,
> + const char *index_file,
> + struct interactive_options *opts,
> + const char *revision,
> + const struct pathspec *ps)
> +{
> + struct patch_mode mode = {
> + .apply_args = { "--cached", NULL },
> + .apply_check_args = { "--cached", NULL },
> + .prompt_mode = {
> + N_("Stage mode change [y,n,q,a,d%s,?]? "),
> + N_("Stage deletion [y,n,q,a,d%s,?]? "),
> + N_("Stage addition [y,n,q,a,d%s,?]? "),
> + N_("Stage this hunk [y,n,q,a,d%s,?]? ")
> + },
> + .edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk "
> + "will immediately be marked for staging."),
> + .help_patch_text =
> + N_("y - stage this hunk\n"
> + "n - do not stage this hunk\n"
> + "q - quit; do not stage this hunk or any of the remaining "
> + "ones\n"
> + "a - stage this hunk and all later hunks in the file\n"
> + "d - do not stage this hunk or any of the later hunks in "
> + "the file\n"),
> + .index_only = 1,
> + };
> + struct add_p_state s = {
> + .r = r,
> + .index = index,
> + .index_file = index_file,
> + .answer = STRBUF_INIT,
> + .buf = STRBUF_INIT,
> + .plain = STRBUF_INIT,
> + .colored = STRBUF_INIT,
> + .mode = &mode,
> + .revision = revision,
> + };
> + struct strbuf parent_revision = STRBUF_INIT;
> + char parent_tree_oid[GIT_MAX_HEXSZ + 1];
> + size_t binary_count = 0;
> + struct commit *commit;
> + int ret;
> +
> + commit = lookup_commit_reference_by_name(revision);
> + if (!commit) {
> + err(&s, _("Revision does not refer to a commit"));
> + ret = -1;
> + goto out;
> + }
> +
> + if (commit->parents)
> + oid_to_hex_r(parent_tree_oid, get_commit_tree_oid(commit->parents->item));
> + else
> + oid_to_hex_r(parent_tree_oid, r->hash_algo->empty_tree);
> +
> + strbuf_addf(&parent_revision, "%s~", revision);
> + mode.diff_cmd[0] = "diff-tree";
> + mode.diff_cmd[1] = "-r";
> + mode.diff_cmd[2] = parent_tree_oid;
> +
> + interactive_config_init(&s.cfg, r, opts);
> +
> + if (parse_diff(&s, ps) < 0) {
> + ret = -1;
> + goto out;
> + }
> +
> + for (size_t i = 0; i < s.file_diff_nr; i++) {
> + if (s.file_diff[i].binary && !s.file_diff[i].hunk_nr)
> + binary_count++;
> + else if (patch_update_file(&s, s.file_diff + i))
> + break;
> + }
> +
> + if (s.file_diff_nr == 0) {
> + err(&s, _("No changes."));
> + ret = -1;
> + goto out;
> + }
> +
> + if (binary_count == s.file_diff_nr) {
> + err(&s, _("Only binary files changed."));
> + ret = -1;
> + goto out;
> + }
> +
> + ret = 0;
> +
> +out:
> + strbuf_release(&parent_revision);
> + add_p_state_clear(&s);
> + return ret;
> +}
I'm totally unfamiliar with add-patch.[ch] beyond what I've been
reviewing in this series, so this may be a dumb/naive question, but
why add a sibling run_add_p_index() to run_add_p() via
copy+paste+modify? (Or is it not copy+paste+modify in some
interesting way?) I'm worried the two will drift, and I'm curious
whether run_add_p() should just be calling run_add_p_index() and just
passing r->index for the index field. Is there a reason that doesn't
work?
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH v6 09/11] add-patch: add support for in-memory index patching
2025-11-20 7:04 ` Elijah Newren
@ 2025-11-20 15:05 ` Phillip Wood
2025-12-02 18:49 ` Patrick Steinhardt
1 sibling, 0 replies; 278+ messages in thread
From: Phillip Wood @ 2025-11-20 15:05 UTC (permalink / raw)
To: Elijah Newren, Patrick Steinhardt
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Karthik Nayak
On 20/11/2025 07:04, Elijah Newren wrote:
> On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:
>
> I'm totally unfamiliar with add-patch.[ch] beyond what I've been
> reviewing in this series, so this may be a dumb/naive question, but
> why add a sibling run_add_p_index() to run_add_p() via
> copy+paste+modify? (Or is it not copy+paste+modify in some
> interesting way?) I'm worried the two will drift, and I'm curious
> whether run_add_p() should just be calling run_add_p_index() and just
> passing r->index for the index field. Is there a reason that doesn't
> work?
That's a very good question. I also wondered if there is a way to
prevent any future changes from accidentally using "s->r->index" instead
of the "s->index" but I don't think there is.
Thanks
Phillip
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v6 09/11] add-patch: add support for in-memory index patching
2025-11-20 7:04 ` Elijah Newren
2025-11-20 15:05 ` Phillip Wood
@ 2025-12-02 18:49 ` Patrick Steinhardt
1 sibling, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-12-02 18:49 UTC (permalink / raw)
To: Elijah Newren
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Karthik Nayak
On Wed, Nov 19, 2025 at 11:04:21PM -0800, Elijah Newren wrote:
> On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:
>
> > +int run_add_p_index(struct repository *r,
> > + struct index_state *index,
> > + const char *index_file,
> > + struct interactive_options *opts,
> > + const char *revision,
> > + const struct pathspec *ps)
> > +{
> > + struct patch_mode mode = {
> > + .apply_args = { "--cached", NULL },
> > + .apply_check_args = { "--cached", NULL },
> > + .prompt_mode = {
> > + N_("Stage mode change [y,n,q,a,d%s,?]? "),
> > + N_("Stage deletion [y,n,q,a,d%s,?]? "),
> > + N_("Stage addition [y,n,q,a,d%s,?]? "),
> > + N_("Stage this hunk [y,n,q,a,d%s,?]? ")
> > + },
> > + .edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk "
> > + "will immediately be marked for staging."),
> > + .help_patch_text =
> > + N_("y - stage this hunk\n"
> > + "n - do not stage this hunk\n"
> > + "q - quit; do not stage this hunk or any of the remaining "
> > + "ones\n"
> > + "a - stage this hunk and all later hunks in the file\n"
> > + "d - do not stage this hunk or any of the later hunks in "
> > + "the file\n"),
> > + .index_only = 1,
> > + };
> > + struct add_p_state s = {
> > + .r = r,
> > + .index = index,
> > + .index_file = index_file,
> > + .answer = STRBUF_INIT,
> > + .buf = STRBUF_INIT,
> > + .plain = STRBUF_INIT,
> > + .colored = STRBUF_INIT,
> > + .mode = &mode,
> > + .revision = revision,
> > + };
> > + struct strbuf parent_revision = STRBUF_INIT;
> > + char parent_tree_oid[GIT_MAX_HEXSZ + 1];
> > + size_t binary_count = 0;
> > + struct commit *commit;
> > + int ret;
> > +
> > + commit = lookup_commit_reference_by_name(revision);
> > + if (!commit) {
> > + err(&s, _("Revision does not refer to a commit"));
> > + ret = -1;
> > + goto out;
> > + }
> > +
> > + if (commit->parents)
> > + oid_to_hex_r(parent_tree_oid, get_commit_tree_oid(commit->parents->item));
> > + else
> > + oid_to_hex_r(parent_tree_oid, r->hash_algo->empty_tree);
> > +
> > + strbuf_addf(&parent_revision, "%s~", revision);
> > + mode.diff_cmd[0] = "diff-tree";
> > + mode.diff_cmd[1] = "-r";
> > + mode.diff_cmd[2] = parent_tree_oid;
> > +
> > + interactive_config_init(&s.cfg, r, opts);
> > +
> > + if (parse_diff(&s, ps) < 0) {
> > + ret = -1;
> > + goto out;
> > + }
> > +
> > + for (size_t i = 0; i < s.file_diff_nr; i++) {
> > + if (s.file_diff[i].binary && !s.file_diff[i].hunk_nr)
> > + binary_count++;
> > + else if (patch_update_file(&s, s.file_diff + i))
> > + break;
> > + }
> > +
> > + if (s.file_diff_nr == 0) {
> > + err(&s, _("No changes."));
> > + ret = -1;
> > + goto out;
> > + }
> > +
> > + if (binary_count == s.file_diff_nr) {
> > + err(&s, _("Only binary files changed."));
> > + ret = -1;
> > + goto out;
> > + }
> > +
> > + ret = 0;
> > +
> > +out:
> > + strbuf_release(&parent_revision);
> > + add_p_state_clear(&s);
> > + return ret;
> > +}
>
> I'm totally unfamiliar with add-patch.[ch] beyond what I've been
> reviewing in this series, so this may be a dumb/naive question, but
> why add a sibling run_add_p_index() to run_add_p() via
> copy+paste+modify? (Or is it not copy+paste+modify in some
> interesting way?) I'm worried the two will drift, and I'm curious
> whether run_add_p() should just be calling run_add_p_index() and just
> passing r->index for the index field. Is there a reason that doesn't
> work?
Most of the function isn't actually copy-paste-modify. Out of the ~90
lines of code of the new function only ~20 are the same. We could of
course introduce a function to share those lines, but it doesn't save us
_that_ much.
I think the logic is non-trivial enough though to warrant it being
duplicated regardless of that.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH v6 10/11] cache-tree: allow writing in-memory index as tree
2025-10-27 11:33 ` [PATCH v6 00/11] " Patrick Steinhardt
` (8 preceding siblings ...)
2025-10-27 11:33 ` [PATCH v6 09/11] add-patch: add support for in-memory index patching Patrick Steinhardt
@ 2025-10-27 11:33 ` Patrick Steinhardt
2025-11-20 7:04 ` Elijah Newren
2025-10-27 11:33 ` [PATCH v6 11/11] builtin/history: implement "split" subcommand Patrick Steinhardt
` (2 subsequent siblings)
12 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-27 11:33 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
The function `write_in_core_index_as_tree()` takes a repository and
writes its index into a tree object. What this function cannot do though
is to take an _arbitrary_ in-memory index.
Introduce a new `struct index_state` parameter so that the caller can
pass a different index than the one belonging to the repository. This
will be used in a subsequent commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
builtin/checkout.c | 3 ++-
cache-tree.c | 5 ++---
cache-tree.h | 3 ++-
3 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/builtin/checkout.c b/builtin/checkout.c
index d230b1f8995..0b90f398feb 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -902,7 +902,8 @@ static int merge_working_tree(const struct checkout_opts *opts,
0);
init_ui_merge_options(&o, the_repository);
o.verbosity = 0;
- work = write_in_core_index_as_tree(the_repository);
+ work = write_in_core_index_as_tree(the_repository,
+ the_repository->index);
ret = reset_tree(new_tree,
opts, 1,
diff --git a/cache-tree.c b/cache-tree.c
index 2aba47060e9..b67d0d703d2 100644
--- a/cache-tree.c
+++ b/cache-tree.c
@@ -699,11 +699,11 @@ static int write_index_as_tree_internal(struct object_id *oid,
return 0;
}
-struct tree* write_in_core_index_as_tree(struct repository *repo) {
+struct tree *write_in_core_index_as_tree(struct repository *repo,
+ struct index_state *index_state) {
struct object_id o;
int was_valid, ret;
- struct index_state *index_state = repo->index;
was_valid = index_state->cache_tree &&
cache_tree_fully_valid(index_state->cache_tree);
@@ -723,7 +723,6 @@ struct tree* write_in_core_index_as_tree(struct repository *repo) {
return lookup_tree(repo, &index_state->cache_tree->oid);
}
-
int write_index_as_tree(struct object_id *oid, struct index_state *index_state, const char *index_path, int flags, const char *prefix)
{
int entries, was_valid;
diff --git a/cache-tree.h b/cache-tree.h
index b82c4963e7c..f8bddae5235 100644
--- a/cache-tree.h
+++ b/cache-tree.h
@@ -47,7 +47,8 @@ int cache_tree_verify(struct repository *, struct index_state *);
#define WRITE_TREE_UNMERGED_INDEX (-2)
#define WRITE_TREE_PREFIX_ERROR (-3)
-struct tree* write_in_core_index_as_tree(struct repository *repo);
+struct tree *write_in_core_index_as_tree(struct repository *repo,
+ struct index_state *index_state);
int write_index_as_tree(struct object_id *oid, struct index_state *index_state, const char *index_path, int flags, const char *prefix);
void prime_cache_tree(struct repository *, struct index_state *, struct tree *);
--
2.51.1.930.gacf6e81ea2.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH v6 10/11] cache-tree: allow writing in-memory index as tree
2025-10-27 11:33 ` [PATCH v6 10/11] cache-tree: allow writing in-memory index as tree Patrick Steinhardt
@ 2025-11-20 7:04 ` Elijah Newren
0 siblings, 0 replies; 278+ messages in thread
From: Elijah Newren @ 2025-11-20 7:04 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Karthik Nayak
On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:
>
> The function `write_in_core_index_as_tree()` takes a repository and
> writes its index into a tree object. What this function cannot do though
> is to take an _arbitrary_ in-memory index.
>
> Introduce a new `struct index_state` parameter so that the caller can
> pass a different index than the one belonging to the repository. This
> will be used in a subsequent commit.
Make sense.
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> builtin/checkout.c | 3 ++-
> cache-tree.c | 5 ++---
> cache-tree.h | 3 ++-
> 3 files changed, 6 insertions(+), 5 deletions(-)
>
> diff --git a/builtin/checkout.c b/builtin/checkout.c
> index d230b1f8995..0b90f398feb 100644
> --- a/builtin/checkout.c
> +++ b/builtin/checkout.c
> @@ -902,7 +902,8 @@ static int merge_working_tree(const struct checkout_opts *opts,
> 0);
> init_ui_merge_options(&o, the_repository);
> o.verbosity = 0;
> - work = write_in_core_index_as_tree(the_repository);
> + work = write_in_core_index_as_tree(the_repository,
> + the_repository->index);
>
> ret = reset_tree(new_tree,
> opts, 1,
> diff --git a/cache-tree.c b/cache-tree.c
> index 2aba47060e9..b67d0d703d2 100644
> --- a/cache-tree.c
> +++ b/cache-tree.c
> @@ -699,11 +699,11 @@ static int write_index_as_tree_internal(struct object_id *oid,
> return 0;
> }
>
> -struct tree* write_in_core_index_as_tree(struct repository *repo) {
> +struct tree *write_in_core_index_as_tree(struct repository *repo,
> + struct index_state *index_state) {
> struct object_id o;
> int was_valid, ret;
>
> - struct index_state *index_state = repo->index;
> was_valid = index_state->cache_tree &&
> cache_tree_fully_valid(index_state->cache_tree);
>
> @@ -723,7 +723,6 @@ struct tree* write_in_core_index_as_tree(struct repository *repo) {
> return lookup_tree(repo, &index_state->cache_tree->oid);
> }
>
> -
Why the random whitespace change?
> int write_index_as_tree(struct object_id *oid, struct index_state *index_state, const char *index_path, int flags, const char *prefix)
> {
> int entries, was_valid;
> diff --git a/cache-tree.h b/cache-tree.h
> index b82c4963e7c..f8bddae5235 100644
> --- a/cache-tree.h
> +++ b/cache-tree.h
> @@ -47,7 +47,8 @@ int cache_tree_verify(struct repository *, struct index_state *);
> #define WRITE_TREE_UNMERGED_INDEX (-2)
> #define WRITE_TREE_PREFIX_ERROR (-3)
>
> -struct tree* write_in_core_index_as_tree(struct repository *repo);
> +struct tree *write_in_core_index_as_tree(struct repository *repo,
> + struct index_state *index_state);
> int write_index_as_tree(struct object_id *oid, struct index_state *index_state, const char *index_path, int flags, const char *prefix);
> void prime_cache_tree(struct repository *, struct index_state *, struct tree *);
>
>
> --
> 2.51.1.930.gacf6e81ea2.dirty
Looks good other than the random whitespace change.
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH v6 11/11] builtin/history: implement "split" subcommand
2025-10-27 11:33 ` [PATCH v6 00/11] " Patrick Steinhardt
` (9 preceding siblings ...)
2025-10-27 11:33 ` [PATCH v6 10/11] cache-tree: allow writing in-memory index as tree Patrick Steinhardt
@ 2025-10-27 11:33 ` Patrick Steinhardt
2025-11-20 7:05 ` Elijah Newren
2025-11-21 14:31 ` Phillip Wood
2025-11-12 19:13 ` [PATCH v6 00/11] Introduce git-history(1) command for easy history editing Sergey Organov
2025-11-20 7:07 ` Elijah Newren
12 siblings, 2 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-10-27 11:33 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
It is quite a common use case that one wants to split up one commit into
multiple commits by moving parts of the changes of the original commit
out into a separate commit. This is quite an involved operation though:
1. Identify the commit in question that is to be dropped.
2. Perform an interactive rebase on top of that commit's parent.
3. Modify the instruction sheet to "edit" the commit that is to be
split up.
4. Drop the commit via "git reset HEAD~".
5. Stage changes that should go into the first commit and commit it.
6. Stage changes that should go into the second commit and commit it.
7. Finalize the rebase.
This is quite complex, and overall I would claim that most people who
are not experts in Git would struggle with this flow.
Introduce a new "split" subcommand for git-history(1) to make this way
easier. All the user needs to do is to say `git history split $COMMIT`.
From hereon, Git asks the user which parts of the commit shall be moved
out into a separate commit and, once done, asks the user for the commit
message. Git then creates that split-out commit and applies the original
commit on top of it.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-history.adoc | 62 ++++++
builtin/history.c | 218 +++++++++++++++++++++
t/meson.build | 1 +
t/t3452-history-split.sh | 432 +++++++++++++++++++++++++++++++++++++++++
4 files changed, 713 insertions(+)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index bd903875120..3d6b2665f8d 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -9,6 +9,7 @@ SYNOPSIS
--------
[synopsis]
git history reword <commit>
+git history split <commit> [--] [<pathspec>...]
DESCRIPTION
-----------
@@ -37,6 +38,26 @@ Several commands are available to rewrite history in different ways:
details of this commit remain unchanged. This command will spawn an
editor with the current message of that commit.
+`split <commit> [--] [<pathspec>...]`::
+ Interactively split up <commit> into two commits by choosing
+ hunks introduced by it that will be moved into the new split-out
+ commit. These hunks will then be written into a new commit that
+ becomes the parent of the previous commit. The original commit
+ stays intact, except that its parent will be the newly split-out
+ commit.
++
+The commit message of the new commit will be asked for by launching the
+configured editor. Authorship of the commit will be the same as for the
+original commit.
++
+If passed, _<pathspec>_ can be used to limit which changes shall be split out
+of the original commit. Files not matching any of the pathspecs will remain
+part of the original commit. For more details, see the 'pathspec' entry in
+linkgit:gitglossary[7].
++
+It is invalid to select either all or no hunks, as that would lead to
+one of the commits becoming empty.
+
CONFIGURATION
-------------
@@ -44,6 +65,47 @@ include::includes/cmd-config-section-all.adoc[]
include::config/sequencer.adoc[]
+EXAMPLES
+--------
+
+Split a commit
+~~~~~~~~~~~~~~
+
+----------
+$ git log --stat --oneline
+3f81232 (HEAD -> main) original
+ bar | 1 +
+ foo | 1 +
+ 2 files changed, 2 insertions(+)
+
+$ git history split HEAD
+diff --git a/bar b/bar
+new file mode 100644
+index 0000000..5716ca5
+--- /dev/null
++++ b/bar
+@@ -0,0 +1 @@
++bar
+(1/1) Stage addition [y,n,q,a,d,e,p,?]? y
+
+diff --git a/foo b/foo
+new file mode 100644
+index 0000000..257cc56
+--- /dev/null
++++ b/foo
+@@ -0,0 +1 @@
++foo
+(1/1) Stage addition [y,n,q,a,d,e,p,?]? n
+
+$ git log --stat --oneline
+7cebe64 (HEAD -> main) original
+ foo | 1 +
+ 1 file changed, 1 insertion(+)
+d1582f3 split-out commit
+ bar | 1 +
+ 1 file changed, 1 insertion(+)
+----------
+
GIT
---
Part of the linkgit:git[1] suite
diff --git a/builtin/history.c b/builtin/history.c
index cb251ae2e01..cae841707d0 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -1,6 +1,7 @@
#define USE_THE_REPOSITORY_VARIABLE
#include "builtin.h"
+#include "cache-tree.h"
#include "commit-reach.h"
#include "commit.h"
#include "config.h"
@@ -8,17 +9,22 @@
#include "environment.h"
#include "gettext.h"
#include "hex.h"
+#include "oidmap.h"
#include "parse-options.h"
+#include "path.h"
+#include "read-cache.h"
#include "refs.h"
#include "replay.h"
#include "reset.h"
#include "revision.h"
+#include "run-command.h"
#include "sequencer.h"
#include "strvec.h"
#include "tree.h"
#include "wt-status.h"
#define GIT_HISTORY_REWORD_USAGE N_("git history reword <commit>")
+#define GIT_HISTORY_SPLIT_USAGE N_("git history split <commit> [--] [<pathspec>...]")
static int collect_commits(struct repository *repo,
struct commit *old_commit,
@@ -323,6 +329,216 @@ static int cmd_history_reword(int argc,
return ret;
}
+static int split_commit(struct repository *repo,
+ struct commit *original_commit,
+ struct pathspec *pathspec,
+ struct object_id *out)
+{
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
+ struct strbuf index_file = STRBUF_INIT, split_message = STRBUF_INIT;
+ struct child_process read_tree_cmd = CHILD_PROCESS_INIT;
+ struct index_state index = INDEX_STATE_INIT(repo);
+ struct object_id original_commit_tree_oid, parent_tree_oid;
+ const char *original_message, *original_body, *ptr;
+ char original_commit_oid[GIT_MAX_HEXSZ + 1];
+ char *original_author = NULL;
+ struct commit_list *parents = NULL;
+ struct commit *first_commit;
+ struct tree *split_tree;
+ size_t len;
+ int ret;
+
+ if (original_commit->parents)
+ parent_tree_oid = *get_commit_tree_oid(original_commit->parents->item);
+ else
+ oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
+ original_commit_tree_oid = *get_commit_tree_oid(original_commit);
+
+ /*
+ * Construct the first commit. This is done by taking the original
+ * commit parent's tree and selectively patching changes from the diff
+ * between that parent and its child.
+ */
+ repo_git_path_replace(repo, &index_file, "%s", "history-split.index");
+
+ read_tree_cmd.git_cmd = 1;
+ strvec_pushf(&read_tree_cmd.env, "GIT_INDEX_FILE=%s", index_file.buf);
+ strvec_push(&read_tree_cmd.args, "read-tree");
+ strvec_push(&read_tree_cmd.args, oid_to_hex(&parent_tree_oid));
+ ret = run_command(&read_tree_cmd);
+ if (ret < 0)
+ goto out;
+
+ ret = read_index_from(&index, index_file.buf, repo->gitdir);
+ if (ret < 0) {
+ ret = error(_("failed reading temporary index"));
+ goto out;
+ }
+
+ oid_to_hex_r(original_commit_oid, &original_commit->object.oid);
+ ret = run_add_p_index(repo, &index, index_file.buf, &interactive_opts,
+ original_commit_oid, pathspec);
+ if (ret < 0)
+ goto out;
+
+ split_tree = write_in_core_index_as_tree(repo, &index);
+ if (!split_tree) {
+ ret = error(_("failed split tree"));
+ goto out;
+ }
+
+ unlink(index_file.buf);
+
+ /*
+ * We disallow the cases where either the split-out commit or the
+ * original commit would become empty. Consequently, if we see that the
+ * new tree ID matches either of those trees we abort.
+ */
+ if (oideq(&split_tree->object.oid, &parent_tree_oid)) {
+ ret = error(_("split commit is empty"));
+ goto out;
+ } else if (oideq(&split_tree->object.oid, &original_commit_tree_oid)) {
+ ret = error(_("split commit tree matches original commit"));
+ goto out;
+ }
+
+ /* We retain authorship of the original commit. */
+ original_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);
+ ptr = find_commit_header(original_message, "author", &len);
+ if (ptr)
+ original_author = xmemdupz(ptr, len);
+
+ ret = fill_commit_message(repo, &parent_tree_oid, &split_tree->object.oid,
+ "", "split-out", &split_message);
+ if (ret < 0)
+ goto out;
+
+ ret = commit_tree(split_message.buf, split_message.len, &split_tree->object.oid,
+ original_commit->parents, &out[0], original_author, NULL);
+ if (ret < 0) {
+ ret = error(_("failed writing split-out commit"));
+ goto out;
+ }
+
+ /*
+ * The second commit is much simpler to construct, as we can simply use
+ * the original commit details, except that we adjust its parent to be
+ * the newly split-out commit.
+ */
+ find_commit_subject(original_message, &original_body);
+ first_commit = lookup_commit_reference(repo, &out[0]);
+ commit_list_append(first_commit, &parents);
+
+ ret = commit_tree(original_body, strlen(original_body), &original_commit_tree_oid,
+ parents, &out[1], original_author, NULL);
+ if (ret < 0) {
+ ret = error(_("failed writing second commit"));
+ goto out;
+ }
+
+ ret = 0;
+
+out:
+ if (index_file.len)
+ unlink(index_file.buf);
+ strbuf_release(&split_message);
+ strbuf_release(&index_file);
+ free_commit_list(parents);
+ free(original_author);
+ release_index(&index);
+ return ret;
+}
+
+static int cmd_history_split(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo)
+{
+ const char * const usage[] = {
+ GIT_HISTORY_SPLIT_USAGE,
+ NULL,
+ };
+ struct option options[] = {
+ OPT_END(),
+ };
+ struct oidmap rewritten_commits = OIDMAP_INIT;
+ struct commit *original_commit, *parent, *head;
+ struct strvec commits = STRVEC_INIT;
+ struct commit_list *from_list = NULL;
+ struct object_id split_commits[2];
+ struct pathspec pathspec = { 0 };
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc < 1) {
+ ret = error(_("command expects a revision"));
+ goto out;
+ }
+ repo_config(repo, git_default_config, NULL);
+
+ original_commit = lookup_commit_reference_by_name(argv[0]);
+ if (!original_commit) {
+ ret = error(_("commit to be split cannot be found: %s"), argv[0]);
+ goto out;
+ }
+
+ parent = original_commit->parents ? original_commit->parents->item : NULL;
+ if (parent && repo_parse_commit(repo, parent)) {
+ ret = error(_("unable to parse commit %s"),
+ oid_to_hex(&parent->object.oid));
+ goto out;
+ }
+
+ head = lookup_commit_reference_by_name("HEAD");
+ if (!head) {
+ ret = error(_("could not resolve HEAD to a commit"));
+ goto out;
+ }
+
+ commit_list_append(original_commit, &from_list);
+ if (!repo_is_descendant_of(repo, head, from_list)) {
+ ret = error(_("split commit must be reachable from current HEAD commit"));
+ goto out;
+ }
+
+ parse_pathspec(&pathspec, 0,
+ PATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,
+ prefix, argv + 1);
+
+ /*
+ * Collect the list of commits that we'll have to reapply now already.
+ * This ensures that we'll abort early on in case the range of commits
+ * contains merges, which we do not yet handle.
+ */
+ ret = collect_commits(repo, parent, head, &commits);
+ if (ret < 0)
+ goto out;
+
+ /*
+ * Then we split up the commit and replace the original commit with the
+ * new ones.
+ */
+ ret = split_commit(repo, original_commit, &pathspec, split_commits);
+ if (ret < 0)
+ goto out;
+
+ replace_commits(&commits, &original_commit->object.oid,
+ split_commits, ARRAY_SIZE(split_commits));
+
+ ret = apply_commits(repo, &commits, parent, head, "split");
+ if (ret < 0)
+ goto out;
+
+ ret = 0;
+
+out:
+ oidmap_clear(&rewritten_commits, 0);
+ free_commit_list(from_list);
+ clear_pathspec(&pathspec);
+ strvec_clear(&commits);
+ return ret;
+}
+
int cmd_history(int argc,
const char **argv,
const char *prefix,
@@ -330,11 +546,13 @@ int cmd_history(int argc,
{
const char * const usage[] = {
GIT_HISTORY_REWORD_USAGE,
+ GIT_HISTORY_SPLIT_USAGE,
NULL,
};
parse_opt_subcommand_fn *fn = NULL;
struct option options[] = {
OPT_SUBCOMMAND("reword", &fn, cmd_history_reword),
+ OPT_SUBCOMMAND("split", &fn, cmd_history_split),
OPT_END(),
};
diff --git a/t/meson.build b/t/meson.build
index a3ec9199947..5d3014a768f 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -386,6 +386,7 @@ integration_tests = [
't3438-rebase-broken-files.sh',
't3450-history.sh',
't3451-history-reword.sh',
+ 't3452-history-split.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
diff --git a/t/t3452-history-split.sh b/t/t3452-history-split.sh
new file mode 100755
index 00000000000..2aac28afdf0
--- /dev/null
+++ b/t/t3452-history-split.sh
@@ -0,0 +1,432 @@
+#!/bin/sh
+
+test_description='tests for git-history split subcommand'
+
+. ./test-lib.sh
+
+set_fake_editor () {
+ write_script fake-editor.sh <<-EOF &&
+ echo "$@" >"\$1"
+ EOF
+ test_set_editor "$(pwd)"/fake-editor.sh
+}
+
+expect_log () {
+ git log --format="%s" >actual &&
+ cat >expect &&
+ test_cmp expect actual
+}
+
+expect_tree_entries () {
+ git ls-tree --name-only "$1" >actual &&
+ cat >expect &&
+ test_cmp expect actual
+}
+
+test_expect_success 'refuses to work with merge commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ git branch branch &&
+ test_commit ours &&
+ git switch branch &&
+ test_commit theirs &&
+ git switch - &&
+ git merge theirs &&
+ test_must_fail git history split HEAD 2>err &&
+ test_grep "cannot rearrange commit history with merges" err &&
+ test_must_fail git history split HEAD~ 2>err &&
+ test_grep "cannot rearrange commit history with merges" err
+ )
+'
+
+test_expect_success 'refuses to work with unrelated commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ git branch branch &&
+ test_commit ours &&
+ git switch branch &&
+ test_commit theirs &&
+ test_must_fail git history split ours 2>err &&
+ test_grep "split commit must be reachable from current HEAD commit" err
+ )
+'
+
+test_expect_success 'can split up tip commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit initial &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ git symbolic-ref HEAD >expect &&
+ set_fake_editor "split-out commit" &&
+ git history split HEAD <<-EOF &&
+ y
+ n
+ EOF
+ git symbolic-ref HEAD >actual &&
+ test_cmp expect actual &&
+
+ expect_log <<-EOF &&
+ split-me
+ split-out commit
+ initial
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ bar
+ initial.t
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ initial.t
+ EOF
+ )
+'
+
+test_expect_success 'can split up root commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m root &&
+ test_commit tip &&
+
+ set_fake_editor "split-out commit" &&
+ git history split HEAD~ <<-EOF &&
+ y
+ n
+ EOF
+
+ expect_log <<-EOF &&
+ tip
+ root
+ split-out commit
+ EOF
+
+ expect_tree_entries HEAD~2 <<-EOF &&
+ bar
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ bar
+ foo
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ tip.t
+ EOF
+ )
+'
+
+test_expect_success 'can split up in-between commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit initial &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+ test_commit tip &&
+
+ set_fake_editor "split-out commit" &&
+ git history split HEAD~ <<-EOF &&
+ y
+ n
+ EOF
+
+ expect_log <<-EOF &&
+ tip
+ split-me
+ split-out commit
+ initial
+ EOF
+
+ expect_tree_entries HEAD~2 <<-EOF &&
+ bar
+ initial.t
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ bar
+ foo
+ initial.t
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ initial.t
+ tip.t
+ EOF
+ )
+'
+
+test_expect_success 'can pick multiple hunks' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar baz foo qux &&
+ git add . &&
+ git commit -m split-me &&
+
+ set_fake_editor "split-out-commit" &&
+ git history split HEAD <<-EOF &&
+ y
+ n
+ y
+ n
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ bar
+ foo
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ baz
+ foo
+ qux
+ EOF
+ )
+'
+
+
+test_expect_success 'can use only last hunk' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ set_fake_editor "split-out commit" &&
+ git history split HEAD <<-EOF &&
+ n
+ y
+ EOF
+
+ expect_log <<-EOF &&
+ split-me
+ split-out commit
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ foo
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ EOF
+ )
+'
+
+test_expect_success 'aborts with empty commit message' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ set_fake_editor "" &&
+ test_must_fail git history split HEAD <<-EOF 2>err &&
+ y
+ n
+ EOF
+ test_grep "Aborting commit due to empty commit message." err
+ )
+'
+
+test_expect_success 'commit message editor sees split-out changes' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ write_script fake-editor.sh <<-\EOF &&
+ cp "$1" . &&
+ echo "some commit message" >>"$1"
+ EOF
+ test_set_editor "$(pwd)"/fake-editor.sh &&
+
+ git history split HEAD <<-EOF &&
+ y
+ n
+ EOF
+
+ cat >expect <<-EOF &&
+
+ # Please enter the commit message for the split-out changes. Lines starting
+ # with ${SQ}#${SQ} will be ignored.
+ # Changes to be committed:
+ # new file: bar
+ #
+ EOF
+ test_cmp expect COMMIT_EDITMSG &&
+
+ expect_log <<-EOF
+ split-me
+ some commit message
+ EOF
+ )
+'
+
+test_expect_success 'can use pathspec to limit what gets split' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ set_fake_editor "split-out commit" &&
+ git history split HEAD -- foo <<-EOF &&
+ y
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ foo
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ EOF
+ )
+'
+
+test_expect_success 'refuses to create empty split-out commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ test_must_fail git history split HEAD 2>err <<-EOF &&
+ n
+ n
+ EOF
+ test_grep "split commit is empty" err
+ )
+'
+
+test_expect_success 'hooks are executed for rewritten commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+ old_head=$(git rev-parse HEAD) &&
+
+ write_script .git/hooks/prepare-commit-msg <<-EOF &&
+ touch "$(pwd)/hooks.log"
+ EOF
+ write_script .git/hooks/post-commit <<-EOF &&
+ touch "$(pwd)/hooks.log"
+ EOF
+ write_script .git/hooks/post-rewrite <<-EOF &&
+ touch "$(pwd)/hooks.log"
+ EOF
+
+ set_fake_editor "split-out commit" &&
+ git history split HEAD <<-EOF &&
+ y
+ n
+ EOF
+
+ expect_log <<-EOF &&
+ split-me
+ split-out commit
+ EOF
+
+ test_path_is_missing hooks.log
+ )
+'
+
+test_expect_success 'refuses to create empty original commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ test_must_fail git history split HEAD 2>err <<-EOF &&
+ y
+ y
+ EOF
+ test_grep "split commit tree matches original commit" err
+ )
+'
+
+test_expect_success 'retains changes in the worktree and index' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ echo a >a &&
+ echo b >b &&
+ git add . &&
+ git commit -m "initial commit" &&
+ echo a-modified >a &&
+ echo b-modified >b &&
+ git add b &&
+ set_fake_editor "a-only" &&
+ git history split HEAD <<-EOF &&
+ y
+ n
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ a
+ EOF
+ expect_tree_entries HEAD <<-EOF &&
+ a
+ b
+ EOF
+
+ cat >expect <<-\EOF &&
+ M a
+ M b
+ ?? actual
+ ?? expect
+ ?? fake-editor.sh
+ EOF
+ git status --porcelain >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_done
--
2.51.1.930.gacf6e81ea2.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH v6 11/11] builtin/history: implement "split" subcommand
2025-10-27 11:33 ` [PATCH v6 11/11] builtin/history: implement "split" subcommand Patrick Steinhardt
@ 2025-11-20 7:05 ` Elijah Newren
2025-12-02 18:49 ` Patrick Steinhardt
2025-11-21 14:31 ` Phillip Wood
1 sibling, 1 reply; 278+ messages in thread
From: Elijah Newren @ 2025-11-20 7:05 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Karthik Nayak
On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:
>
> It is quite a common use case that one wants to split up one commit into
> multiple commits by moving parts of the changes of the original commit
> out into a separate commit. This is quite an involved operation though:
>
> 1. Identify the commit in question that is to be dropped.
>
> 2. Perform an interactive rebase on top of that commit's parent.
>
> 3. Modify the instruction sheet to "edit" the commit that is to be
> split up.
>
> 4. Drop the commit via "git reset HEAD~".
>
> 5. Stage changes that should go into the first commit and commit it.
>
> 6. Stage changes that should go into the second commit and commit it.
>
> 7. Finalize the rebase.
>
> This is quite complex, and overall I would claim that most people who
> are not experts in Git would struggle with this flow.
>
> Introduce a new "split" subcommand for git-history(1) to make this way
> easier. All the user needs to do is to say `git history split $COMMIT`.
> From hereon, Git asks the user which parts of the commit shall be moved
> out into a separate commit and, once done, asks the user for the commit
> message. Git then creates that split-out commit and applies the original
> commit on top of it.
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> Documentation/git-history.adoc | 62 ++++++
> builtin/history.c | 218 +++++++++++++++++++++
> t/meson.build | 1 +
> t/t3452-history-split.sh | 432 +++++++++++++++++++++++++++++++++++++++++
> 4 files changed, 713 insertions(+)
>
> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
> index bd903875120..3d6b2665f8d 100644
> --- a/Documentation/git-history.adoc
> +++ b/Documentation/git-history.adoc
> @@ -9,6 +9,7 @@ SYNOPSIS
> --------
> [synopsis]
> git history reword <commit>
> +git history split <commit> [--] [<pathspec>...]
>
> DESCRIPTION
> -----------
> @@ -37,6 +38,26 @@ Several commands are available to rewrite history in different ways:
> details of this commit remain unchanged. This command will spawn an
> editor with the current message of that commit.
>
> +`split <commit> [--] [<pathspec>...]`::
> + Interactively split up <commit> into two commits by choosing
> + hunks introduced by it that will be moved into the new split-out
> + commit. These hunks will then be written into a new commit that
> + becomes the parent of the previous commit. The original commit
> + stays intact, except that its parent will be the newly split-out
> + commit.
Always two? Should we allow someone to split into three or four?
> ++
> +The commit message of the new commit will be asked for by launching the
> +configured editor. Authorship of the commit will be the same as for the
> +original commit.
Which one is the new one? Aren't they both?
> ++
> +If passed, _<pathspec>_ can be used to limit which changes shall be split out
> +of the original commit. Files not matching any of the pathspecs will remain
> +part of the original commit. For more details, see the 'pathspec' entry in
> +linkgit:gitglossary[7].
> ++
> +It is invalid to select either all or no hunks, as that would lead to
> +one of the commits becoming empty.
If the user edits a hunk, what happens then? Is this function
prepared to deal with that?
> +
> CONFIGURATION
> -------------
>
> @@ -44,6 +65,47 @@ include::includes/cmd-config-section-all.adoc[]
>
> include::config/sequencer.adoc[]
>
> +EXAMPLES
> +--------
> +
> +Split a commit
> +~~~~~~~~~~~~~~
> +
> +----------
> +$ git log --stat --oneline
> +3f81232 (HEAD -> main) original
> + bar | 1 +
> + foo | 1 +
> + 2 files changed, 2 insertions(+)
> +
> +$ git history split HEAD
> +diff --git a/bar b/bar
> +new file mode 100644
> +index 0000000..5716ca5
> +--- /dev/null
> ++++ b/bar
> +@@ -0,0 +1 @@
> ++bar
> +(1/1) Stage addition [y,n,q,a,d,e,p,?]? y
> +
> +diff --git a/foo b/foo
> +new file mode 100644
> +index 0000000..257cc56
> +--- /dev/null
> ++++ b/foo
> +@@ -0,0 +1 @@
> ++foo
> +(1/1) Stage addition [y,n,q,a,d,e,p,?]? n
> +
> +$ git log --stat --oneline
> +7cebe64 (HEAD -> main) original
> + foo | 1 +
> + 1 file changed, 1 insertion(+)
> +d1582f3 split-out commit
> + bar | 1 +
> + 1 file changed, 1 insertion(+)
> +----------
> +
> GIT
> ---
> Part of the linkgit:git[1] suite
> diff --git a/builtin/history.c b/builtin/history.c
> index cb251ae2e01..cae841707d0 100644
> --- a/builtin/history.c
> +++ b/builtin/history.c
> @@ -1,6 +1,7 @@
> #define USE_THE_REPOSITORY_VARIABLE
>
> #include "builtin.h"
> +#include "cache-tree.h"
> #include "commit-reach.h"
> #include "commit.h"
> #include "config.h"
> @@ -8,17 +9,22 @@
> #include "environment.h"
> #include "gettext.h"
> #include "hex.h"
> +#include "oidmap.h"
> #include "parse-options.h"
> +#include "path.h"
> +#include "read-cache.h"
> #include "refs.h"
> #include "replay.h"
> #include "reset.h"
> #include "revision.h"
> +#include "run-command.h"
> #include "sequencer.h"
> #include "strvec.h"
> #include "tree.h"
> #include "wt-status.h"
>
> #define GIT_HISTORY_REWORD_USAGE N_("git history reword <commit>")
> +#define GIT_HISTORY_SPLIT_USAGE N_("git history split <commit> [--] [<pathspec>...]")
>
> static int collect_commits(struct repository *repo,
> struct commit *old_commit,
> @@ -323,6 +329,216 @@ static int cmd_history_reword(int argc,
> return ret;
> }
>
> +static int split_commit(struct repository *repo,
> + struct commit *original_commit,
> + struct pathspec *pathspec,
> + struct object_id *out)
> +{
> + struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
> + struct strbuf index_file = STRBUF_INIT, split_message = STRBUF_INIT;
> + struct child_process read_tree_cmd = CHILD_PROCESS_INIT;
> + struct index_state index = INDEX_STATE_INIT(repo);
> + struct object_id original_commit_tree_oid, parent_tree_oid;
> + const char *original_message, *original_body, *ptr;
> + char original_commit_oid[GIT_MAX_HEXSZ + 1];
> + char *original_author = NULL;
> + struct commit_list *parents = NULL;
> + struct commit *first_commit;
> + struct tree *split_tree;
> + size_t len;
> + int ret;
> +
> + if (original_commit->parents)
> + parent_tree_oid = *get_commit_tree_oid(original_commit->parents->item);
> + else
> + oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
> + original_commit_tree_oid = *get_commit_tree_oid(original_commit);
> +
> + /*
> + * Construct the first commit. This is done by taking the original
> + * commit parent's tree and selectively patching changes from the diff
> + * between that parent and its child.
> + */
> + repo_git_path_replace(repo, &index_file, "%s", "history-split.index");
> +
> + read_tree_cmd.git_cmd = 1;
> + strvec_pushf(&read_tree_cmd.env, "GIT_INDEX_FILE=%s", index_file.buf);
> + strvec_push(&read_tree_cmd.args, "read-tree");
> + strvec_push(&read_tree_cmd.args, oid_to_hex(&parent_tree_oid));
> + ret = run_command(&read_tree_cmd);
> + if (ret < 0)
> + goto out;
> +
> + ret = read_index_from(&index, index_file.buf, repo->gitdir);
> + if (ret < 0) {
> + ret = error(_("failed reading temporary index"));
> + goto out;
> + }
> +
> + oid_to_hex_r(original_commit_oid, &original_commit->object.oid);
> + ret = run_add_p_index(repo, &index, index_file.buf, &interactive_opts,
> + original_commit_oid, pathspec);
> + if (ret < 0)
> + goto out;
> +
> + split_tree = write_in_core_index_as_tree(repo, &index);
> + if (!split_tree) {
> + ret = error(_("failed split tree"));
> + goto out;
> + }
> +
> + unlink(index_file.buf);
> +
> + /*
> + * We disallow the cases where either the split-out commit or the
> + * original commit would become empty. Consequently, if we see that the
> + * new tree ID matches either of those trees we abort.
> + */
> + if (oideq(&split_tree->object.oid, &parent_tree_oid)) {
> + ret = error(_("split commit is empty"));
> + goto out;
> + } else if (oideq(&split_tree->object.oid, &original_commit_tree_oid)) {
> + ret = error(_("split commit tree matches original commit"));
> + goto out;
> + }
> +
> + /* We retain authorship of the original commit. */
> + original_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);
> + ptr = find_commit_header(original_message, "author", &len);
> + if (ptr)
> + original_author = xmemdupz(ptr, len);
> +
> + ret = fill_commit_message(repo, &parent_tree_oid, &split_tree->object.oid,
> + "", "split-out", &split_message);
> + if (ret < 0)
> + goto out;
> +
> + ret = commit_tree(split_message.buf, split_message.len, &split_tree->object.oid,
> + original_commit->parents, &out[0], original_author, NULL);
As with reword, you are discarding all extended headers?
> + if (ret < 0) {
> + ret = error(_("failed writing split-out commit"));
> + goto out;
> + }
> +
> + /*
> + * The second commit is much simpler to construct, as we can simply use
> + * the original commit details, except that we adjust its parent to be
> + * the newly split-out commit.
> + */
> + find_commit_subject(original_message, &original_body);
> + first_commit = lookup_commit_reference(repo, &out[0]);
> + commit_list_append(first_commit, &parents);
> +
> + ret = commit_tree(original_body, strlen(original_body), &original_commit_tree_oid,
> + parents, &out[1], original_author, NULL);
I don't understand why the second commit is the one that is to retain
the commit message. I can see that users would sometimes want that,
but I don't see why it'd be hardcoded.
> + if (ret < 0) {
> + ret = error(_("failed writing second commit"));
> + goto out;
> + }
> +
> + ret = 0;
> +
> +out:
> + if (index_file.len)
> + unlink(index_file.buf);
> + strbuf_release(&split_message);
> + strbuf_release(&index_file);
> + free_commit_list(parents);
> + free(original_author);
> + release_index(&index);
> + return ret;
> +}
> +
> +static int cmd_history_split(int argc,
> + const char **argv,
> + const char *prefix,
> + struct repository *repo)
> +{
> + const char * const usage[] = {
> + GIT_HISTORY_SPLIT_USAGE,
> + NULL,
> + };
> + struct option options[] = {
> + OPT_END(),
> + };
> + struct oidmap rewritten_commits = OIDMAP_INIT;
> + struct commit *original_commit, *parent, *head;
> + struct strvec commits = STRVEC_INIT;
> + struct commit_list *from_list = NULL;
> + struct object_id split_commits[2];
> + struct pathspec pathspec = { 0 };
> + int ret;
> +
> + argc = parse_options(argc, argv, prefix, options, usage, 0);
> + if (argc < 1) {
> + ret = error(_("command expects a revision"));
> + goto out;
> + }
> + repo_config(repo, git_default_config, NULL);
> +
> + original_commit = lookup_commit_reference_by_name(argv[0]);
> + if (!original_commit) {
> + ret = error(_("commit to be split cannot be found: %s"), argv[0]);
> + goto out;
> + }
> +
> + parent = original_commit->parents ? original_commit->parents->item : NULL;
> + if (parent && repo_parse_commit(repo, parent)) {
> + ret = error(_("unable to parse commit %s"),
> + oid_to_hex(&parent->object.oid));
> + goto out;
> + }
> +
> + head = lookup_commit_reference_by_name("HEAD");
> + if (!head) {
> + ret = error(_("could not resolve HEAD to a commit"));
> + goto out;
> + }
> +
> + commit_list_append(original_commit, &from_list);
> + if (!repo_is_descendant_of(repo, head, from_list)) {
> + ret = error(_("split commit must be reachable from current HEAD commit"));
> + goto out;
> + }
Again, I don't see why the commit to be split needs to be an ancestor
of HEAD; seems like an arbitrary requirement.
> +
> + parse_pathspec(&pathspec, 0,
> + PATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,
> + prefix, argv + 1);
> +
> + /*
> + * Collect the list of commits that we'll have to reapply now already.
> + * This ensures that we'll abort early on in case the range of commits
> + * contains merges, which we do not yet handle.
> + */
> + ret = collect_commits(repo, parent, head, &commits);
> + if (ret < 0)
> + goto out;
> +
> + /*
> + * Then we split up the commit and replace the original commit with the
> + * new ones.
> + */
> + ret = split_commit(repo, original_commit, &pathspec, split_commits);
> + if (ret < 0)
> + goto out;
> +
> + replace_commits(&commits, &original_commit->object.oid,
> + split_commits, ARRAY_SIZE(split_commits));
> +
> + ret = apply_commits(repo, &commits, parent, head, "split");
> + if (ret < 0)
> + goto out;
Much like with "reword", I think we could drop your auxiliary
functions (collect_commits(), replace_commits(), apply_commits()),
since replay already handles revision walking.
However, unlike with "reword" you've got a slight mess here. If the
user edits the hunk to be applied, then (1) the rest of the replayed
commits may have conflicts (which replay doesn't handle yet), and (2)
after replaying you'll need to reset your working tree and index to
match the rebased result, which will be tricky if you had either
staged or unstaged modifications.
> +
> + ret = 0;
> +
> +out:
> + oidmap_clear(&rewritten_commits, 0);
> + free_commit_list(from_list);
> + clear_pathspec(&pathspec);
> + strvec_clear(&commits);
> + return ret;
> +}
> +
> int cmd_history(int argc,
> const char **argv,
> const char *prefix,
> @@ -330,11 +546,13 @@ int cmd_history(int argc,
> {
> const char * const usage[] = {
> GIT_HISTORY_REWORD_USAGE,
> + GIT_HISTORY_SPLIT_USAGE,
> NULL,
> };
> parse_opt_subcommand_fn *fn = NULL;
> struct option options[] = {
> OPT_SUBCOMMAND("reword", &fn, cmd_history_reword),
> + OPT_SUBCOMMAND("split", &fn, cmd_history_split),
> OPT_END(),
> };
>
> diff --git a/t/meson.build b/t/meson.build
> index a3ec9199947..5d3014a768f 100644
> --- a/t/meson.build
> +++ b/t/meson.build
> @@ -386,6 +386,7 @@ integration_tests = [
> 't3438-rebase-broken-files.sh',
> 't3450-history.sh',
> 't3451-history-reword.sh',
> + 't3452-history-split.sh',
> 't3500-cherry.sh',
> 't3501-revert-cherry-pick.sh',
> 't3502-cherry-pick-merge.sh',
> diff --git a/t/t3452-history-split.sh b/t/t3452-history-split.sh
> new file mode 100755
> index 00000000000..2aac28afdf0
> --- /dev/null
> +++ b/t/t3452-history-split.sh
> @@ -0,0 +1,432 @@
> +#!/bin/sh
> +
> +test_description='tests for git-history split subcommand'
> +
> +. ./test-lib.sh
> +
> +set_fake_editor () {
> + write_script fake-editor.sh <<-EOF &&
> + echo "$@" >"\$1"
> + EOF
> + test_set_editor "$(pwd)"/fake-editor.sh
> +}
> +
> +expect_log () {
> + git log --format="%s" >actual &&
> + cat >expect &&
> + test_cmp expect actual
> +}
> +
> +expect_tree_entries () {
> + git ls-tree --name-only "$1" >actual &&
> + cat >expect &&
> + test_cmp expect actual
> +}
> +
> +test_expect_success 'refuses to work with merge commits' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + test_commit base &&
> + git branch branch &&
> + test_commit ours &&
> + git switch branch &&
> + test_commit theirs &&
> + git switch - &&
> + git merge theirs &&
> + test_must_fail git history split HEAD 2>err &&
> + test_grep "cannot rearrange commit history with merges" err &&
> + test_must_fail git history split HEAD~ 2>err &&
> + test_grep "cannot rearrange commit history with merges" err
> + )
> +'
> +
> +test_expect_success 'refuses to work with unrelated commits' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + test_commit base &&
> + git branch branch &&
> + test_commit ours &&
> + git switch branch &&
> + test_commit theirs &&
> + test_must_fail git history split ours 2>err &&
> + test_grep "split commit must be reachable from current HEAD commit" err
> + )
> +'
I don't understand why this test is desirable. I guess that's not
surprising given that I called into question why you'd want this to be
an error in the relevant code.
> +test_expect_success 'can split up tip commit' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + test_commit initial &&
> + touch bar foo &&
> + git add . &&
> + git commit -m split-me &&
> +
> + git symbolic-ref HEAD >expect &&
> + set_fake_editor "split-out commit" &&
> + git history split HEAD <<-EOF &&
> + y
> + n
> + EOF
> + git symbolic-ref HEAD >actual &&
> + test_cmp expect actual &&
> +
> + expect_log <<-EOF &&
> + split-me
> + split-out commit
> + initial
> + EOF
> +
> + expect_tree_entries HEAD~ <<-EOF &&
> + bar
> + initial.t
> + EOF
> +
> + expect_tree_entries HEAD <<-EOF
> + bar
> + foo
> + initial.t
> + EOF
> + )
> +'
> +
> +test_expect_success 'can split up root commit' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + touch bar foo &&
> + git add . &&
> + git commit -m root &&
> + test_commit tip &&
> +
> + set_fake_editor "split-out commit" &&
> + git history split HEAD~ <<-EOF &&
> + y
> + n
> + EOF
> +
> + expect_log <<-EOF &&
> + tip
> + root
> + split-out commit
> + EOF
> +
> + expect_tree_entries HEAD~2 <<-EOF &&
> + bar
> + EOF
> +
> + expect_tree_entries HEAD~ <<-EOF &&
> + bar
> + foo
> + EOF
> +
> + expect_tree_entries HEAD <<-EOF
> + bar
> + foo
> + tip.t
> + EOF
> + )
> +'
> +
> +test_expect_success 'can split up in-between commit' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + test_commit initial &&
> + touch bar foo &&
> + git add . &&
> + git commit -m split-me &&
> + test_commit tip &&
> +
> + set_fake_editor "split-out commit" &&
> + git history split HEAD~ <<-EOF &&
> + y
> + n
> + EOF
> +
> + expect_log <<-EOF &&
> + tip
> + split-me
> + split-out commit
> + initial
> + EOF
> +
> + expect_tree_entries HEAD~2 <<-EOF &&
> + bar
> + initial.t
> + EOF
> +
> + expect_tree_entries HEAD~ <<-EOF &&
> + bar
> + foo
> + initial.t
> + EOF
> +
> + expect_tree_entries HEAD <<-EOF
> + bar
> + foo
> + initial.t
> + tip.t
> + EOF
> + )
> +'
> +
> +test_expect_success 'can pick multiple hunks' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + touch bar baz foo qux &&
> + git add . &&
> + git commit -m split-me &&
> +
> + set_fake_editor "split-out-commit" &&
> + git history split HEAD <<-EOF &&
> + y
> + n
> + y
> + n
> + EOF
> +
> + expect_tree_entries HEAD~ <<-EOF &&
> + bar
> + foo
> + EOF
> +
> + expect_tree_entries HEAD <<-EOF
> + bar
> + baz
> + foo
> + qux
> + EOF
> + )
> +'
> +
> +
> +test_expect_success 'can use only last hunk' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + touch bar foo &&
> + git add . &&
> + git commit -m split-me &&
> +
> + set_fake_editor "split-out commit" &&
> + git history split HEAD <<-EOF &&
> + n
> + y
> + EOF
> +
> + expect_log <<-EOF &&
> + split-me
> + split-out commit
> + EOF
> +
> + expect_tree_entries HEAD~ <<-EOF &&
> + foo
> + EOF
> +
> + expect_tree_entries HEAD <<-EOF
> + bar
> + foo
> + EOF
> + )
> +'
> +
> +test_expect_success 'aborts with empty commit message' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + touch bar foo &&
> + git add . &&
> + git commit -m split-me &&
> +
> + set_fake_editor "" &&
> + test_must_fail git history split HEAD <<-EOF 2>err &&
> + y
> + n
> + EOF
> + test_grep "Aborting commit due to empty commit message." err
> + )
> +'
> +
> +test_expect_success 'commit message editor sees split-out changes' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + touch bar foo &&
> + git add . &&
> + git commit -m split-me &&
> +
> + write_script fake-editor.sh <<-\EOF &&
> + cp "$1" . &&
> + echo "some commit message" >>"$1"
> + EOF
> + test_set_editor "$(pwd)"/fake-editor.sh &&
> +
> + git history split HEAD <<-EOF &&
> + y
> + n
> + EOF
> +
> + cat >expect <<-EOF &&
> +
> + # Please enter the commit message for the split-out changes. Lines starting
> + # with ${SQ}#${SQ} will be ignored.
> + # Changes to be committed:
> + # new file: bar
> + #
> + EOF
> + test_cmp expect COMMIT_EDITMSG &&
> +
> + expect_log <<-EOF
> + split-me
> + some commit message
> + EOF
> + )
> +'
> +
> +test_expect_success 'can use pathspec to limit what gets split' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + touch bar foo &&
> + git add . &&
> + git commit -m split-me &&
> +
> + set_fake_editor "split-out commit" &&
> + git history split HEAD -- foo <<-EOF &&
> + y
> + EOF
> +
> + expect_tree_entries HEAD~ <<-EOF &&
> + foo
> + EOF
> +
> + expect_tree_entries HEAD <<-EOF
> + bar
> + foo
> + EOF
> + )
> +'
> +
> +test_expect_success 'refuses to create empty split-out commit' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + test_commit base &&
> + touch bar foo &&
> + git add . &&
> + git commit -m split-me &&
> +
> + test_must_fail git history split HEAD 2>err <<-EOF &&
> + n
> + n
> + EOF
> + test_grep "split commit is empty" err
> + )
> +'
> +
> +test_expect_success 'hooks are executed for rewritten commits' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + touch bar foo &&
> + git add . &&
> + git commit -m split-me &&
> + old_head=$(git rev-parse HEAD) &&
> +
> + write_script .git/hooks/prepare-commit-msg <<-EOF &&
> + touch "$(pwd)/hooks.log"
> + EOF
> + write_script .git/hooks/post-commit <<-EOF &&
> + touch "$(pwd)/hooks.log"
> + EOF
> + write_script .git/hooks/post-rewrite <<-EOF &&
> + touch "$(pwd)/hooks.log"
> + EOF
> +
> + set_fake_editor "split-out commit" &&
> + git history split HEAD <<-EOF &&
> + y
> + n
> + EOF
> +
> + expect_log <<-EOF &&
> + split-me
> + split-out commit
> + EOF
> +
> + test_path_is_missing hooks.log
> + )
> +'
`test_path_is_missing hooks.log` suggests the hooks are NOT executed
for rewritten commits; your cover letter and documentation said hooks
wouldn't run either, so I'm guessing the test description is the bug
here left over from an earlier round?
> +test_expect_success 'refuses to create empty original commit' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + touch bar foo &&
> + git add . &&
> + git commit -m split-me &&
> +
> + test_must_fail git history split HEAD 2>err <<-EOF &&
> + y
> + y
> + EOF
> + test_grep "split commit tree matches original commit" err
> + )
> +'
> +
> +test_expect_success 'retains changes in the worktree and index' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + echo a >a &&
> + echo b >b &&
> + git add . &&
> + git commit -m "initial commit" &&
> + echo a-modified >a &&
> + echo b-modified >b &&
> + git add b &&
> + set_fake_editor "a-only" &&
> + git history split HEAD <<-EOF &&
> + y
> + n
> + EOF
> +
> + expect_tree_entries HEAD~ <<-EOF &&
> + a
> + EOF
> + expect_tree_entries HEAD <<-EOF &&
> + a
> + b
> + EOF
> +
> + cat >expect <<-\EOF &&
> + M a
> + M b
> + ?? actual
> + ?? expect
> + ?? fake-editor.sh
> + EOF
> + git status --porcelain >actual &&
> + test_cmp expect actual
> + )
> +'
...but does this test mean we need to prevent users from editing hunks
when splitting commits? If we don't, how can we retain changes in the
worktree and index?
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH v6 11/11] builtin/history: implement "split" subcommand
2025-11-20 7:05 ` Elijah Newren
@ 2025-12-02 18:49 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-12-02 18:49 UTC (permalink / raw)
To: Elijah Newren
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Karthik Nayak
On Wed, Nov 19, 2025 at 11:05:37PM -0800, Elijah Newren wrote:
> On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:
> > diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
> > index bd903875120..3d6b2665f8d 100644
> > --- a/Documentation/git-history.adoc
> > +++ b/Documentation/git-history.adoc
> > @@ -37,6 +38,26 @@ Several commands are available to rewrite history in different ways:
> > details of this commit remain unchanged. This command will spawn an
> > editor with the current message of that commit.
> >
> > +`split <commit> [--] [<pathspec>...]`::
> > + Interactively split up <commit> into two commits by choosing
> > + hunks introduced by it that will be moved into the new split-out
> > + commit. These hunks will then be written into a new commit that
> > + becomes the parent of the previous commit. The original commit
> > + stays intact, except that its parent will be the newly split-out
> > + commit.
>
> Always two? Should we allow someone to split into three or four?
For now it's always two, yes. This is mostly modeled after `jj split`,
which also does the same. For the sake of simplicity I'd suggest to keep
it like this by default, but I could certainly see that we introduce a
new option in the future that allows the user to split into multiple
commits. In that case, we would simply loop around the interactive
prompt until all remaining hunks have been selected.
> > ++
> > +The commit message of the new commit will be asked for by launching the
> > +configured editor. Authorship of the commit will be the same as for the
> > +original commit.
>
> Which one is the new one? Aren't they both?
I'll change the behaviour to ask for a message for both commits, so I'll
adapt this.
> > ++
> > +If passed, _<pathspec>_ can be used to limit which changes shall be split out
> > +of the original commit. Files not matching any of the pathspecs will remain
> > +part of the original commit. For more details, see the 'pathspec' entry in
> > +linkgit:gitglossary[7].
> > ++
> > +It is invalid to select either all or no hunks, as that would lead to
> > +one of the commits becoming empty.
>
> If the user edits a hunk, what happens then? Is this function
> prepared to deal with that?
The second commit will retain the original tree, so there wouldn't be a
conflict even in that case. It's a good question though -- maybe we
should disable editing hunks for now?
> > +diff --git a/foo b/foo
> > +new file mode 100644
> > +index 0000000..257cc56
> > +--- /dev/null
> > ++++ b/foo
[snip]
> > + /* We retain authorship of the original commit. */
> > + original_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);
> > + ptr = find_commit_header(original_message, "author", &len);
> > + if (ptr)
> > + original_author = xmemdupz(ptr, len);
> > +
> > + ret = fill_commit_message(repo, &parent_tree_oid, &split_tree->object.oid,
> > + "", "split-out", &split_message);
> > + if (ret < 0)
> > + goto out;
> > +
> > + ret = commit_tree(split_message.buf, split_message.len, &split_tree->object.oid,
> > + original_commit->parents, &out[0], original_author, NULL);
>
> As with reword, you are discarding all extended headers?
Good catch, will fix!
> > + if (ret < 0) {
> > + ret = error(_("failed writing split-out commit"));
> > + goto out;
> > + }
> > +
> > + /*
> > + * The second commit is much simpler to construct, as we can simply use
> > + * the original commit details, except that we adjust its parent to be
> > + * the newly split-out commit.
> > + */
> > + find_commit_subject(original_message, &original_body);
> > + first_commit = lookup_commit_reference(repo, &out[0]);
> > + commit_list_append(first_commit, &parents);
> > +
> > + ret = commit_tree(original_body, strlen(original_body), &original_commit_tree_oid,
> > + parents, &out[1], original_author, NULL);
>
> I don't understand why the second commit is the one that is to retain
> the commit message. I can see that users would sometimes want that,
> but I don't see why it'd be hardcoded.
My thinking here is that "I am splitting changes out of a specific
commit", so that commit still continues to exist.
[snip]
> > + commit_list_append(original_commit, &from_list);
> > + if (!repo_is_descendant_of(repo, head, from_list)) {
> > + ret = error(_("split commit must be reachable from current HEAD commit"));
> > + goto out;
> > + }
>
> Again, I don't see why the commit to be split needs to be an ancestor
> of HEAD; seems like an arbitrary requirement.
This is done for the sake of simplicity: I'd rather want to be as
restrictive as possible initially and then extend git-history(1) to
handle more cases as we go forward.
[snip]
> > + /*
> > + * Then we split up the commit and replace the original commit with the
> > + * new ones.
> > + */
> > + ret = split_commit(repo, original_commit, &pathspec, split_commits);
> > + if (ret < 0)
> > + goto out;
> > +
> > + replace_commits(&commits, &original_commit->object.oid,
> > + split_commits, ARRAY_SIZE(split_commits));
> > +
> > + ret = apply_commits(repo, &commits, parent, head, "split");
> > + if (ret < 0)
> > + goto out;
>
> Much like with "reword", I think we could drop your auxiliary
> functions (collect_commits(), replace_commits(), apply_commits()),
> since replay already handles revision walking.
I mostly introduced these functions because I want to extend
git-history(1) going forward to also handle cases that git-replay(1)
doesn't currently handle. This includes for example also reordering of
commits, which I think isn't easily possible with the replay subsystem?
> However, unlike with "reword" you've got a slight mess here. If the
> user edits the hunk to be applied, then (1) the rest of the replayed
> commits may have conflicts (which replay doesn't handle yet), and (2)
> after replaying you'll need to reset your working tree and index to
> match the rebased result, which will be tricky if you had either
> staged or unstaged modifications.
Yeah, the "editing" part is an actual oversight on my part. I'll
restrict this for now.
> > diff --git a/t/t3452-history-split.sh b/t/t3452-history-split.sh
> > new file mode 100755
> > index 00000000000..2aac28afdf0
> > --- /dev/null
> > +++ b/t/t3452-history-split.sh
> > @@ -0,0 +1,432 @@
[snip]
> > +test_expect_success 'refuses to work with unrelated commits' '
> > + test_when_finished "rm -rf repo" &&
> > + git init repo &&
> > + (
> > + cd repo &&
> > + test_commit base &&
> > + git branch branch &&
> > + test_commit ours &&
> > + git switch branch &&
> > + test_commit theirs &&
> > + test_must_fail git history split ours 2>err &&
> > + test_grep "split commit must be reachable from current HEAD commit" err
> > + )
> > +'
>
> I don't understand why this test is desirable. I guess that's not
> surprising given that I called into question why you'd want this to be
> an error in the relevant code.
The question is what happens in the case where you edit a commit that is
unrelated to the current history. Which branches would be updated? Do we
update any at all? If not, what would the user-visible result be? We're
getting into territory where semantics are not immediately obvious, and
I want git-history(1) to be a command that makes history editing easy
for the most common use cases.
If we find good semantics in the future for how to perform such an
operation I'm very much in favor of adding that behaviour. But I think
that this is outside the scope of this series right now, as I rather
care about making the easy and obvious parts work first.
And meanwhile, while we still have restrictions like these in place, I
want to ensure that we don't accidentally do the wrong thing in cases
that we don't yet support. Hence the test.
> > +test_expect_success 'hooks are executed for rewritten commits' '
> > + test_when_finished "rm -rf repo" &&
> > + git init repo &&
> > + (
> > + cd repo &&
> > + touch bar foo &&
> > + git add . &&
> > + git commit -m split-me &&
> > + old_head=$(git rev-parse HEAD) &&
> > +
> > + write_script .git/hooks/prepare-commit-msg <<-EOF &&
> > + touch "$(pwd)/hooks.log"
> > + EOF
> > + write_script .git/hooks/post-commit <<-EOF &&
> > + touch "$(pwd)/hooks.log"
> > + EOF
> > + write_script .git/hooks/post-rewrite <<-EOF &&
> > + touch "$(pwd)/hooks.log"
> > + EOF
> > +
> > + set_fake_editor "split-out commit" &&
> > + git history split HEAD <<-EOF &&
> > + y
> > + n
> > + EOF
> > +
> > + expect_log <<-EOF &&
> > + split-me
> > + split-out commit
> > + EOF
> > +
> > + test_path_is_missing hooks.log
> > + )
> > +'
>
> `test_path_is_missing hooks.log` suggests the hooks are NOT executed
> for rewritten commits; your cover letter and documentation said hooks
> wouldn't run either, so I'm guessing the test description is the bug
> here left over from an earlier round?
It is indeed, thanks!
> > +test_expect_success 'retains changes in the worktree and index' '
> > + test_when_finished "rm -rf repo" &&
> > + git init repo &&
> > + (
> > + cd repo &&
> > + echo a >a &&
> > + echo b >b &&
> > + git add . &&
> > + git commit -m "initial commit" &&
> > + echo a-modified >a &&
> > + echo b-modified >b &&
> > + git add b &&
> > + set_fake_editor "a-only" &&
> > + git history split HEAD <<-EOF &&
> > + y
> > + n
> > + EOF
> > +
> > + expect_tree_entries HEAD~ <<-EOF &&
> > + a
> > + EOF
> > + expect_tree_entries HEAD <<-EOF &&
> > + a
> > + b
> > + EOF
> > +
> > + cat >expect <<-\EOF &&
> > + M a
> > + M b
> > + ?? actual
> > + ?? expect
> > + ?? fake-editor.sh
> > + EOF
> > + git status --porcelain >actual &&
> > + test_cmp expect actual
> > + )
> > +'
>
> ...but does this test mean we need to prevent users from editing hunks
> when splitting commits? If we don't, how can we retain changes in the
> worktree and index?
I'll disallow editing hunks for now. We may reintroduce this ability in
the future, but let's focus on the easy parts for now :)
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v6 11/11] builtin/history: implement "split" subcommand
2025-10-27 11:33 ` [PATCH v6 11/11] builtin/history: implement "split" subcommand Patrick Steinhardt
2025-11-20 7:05 ` Elijah Newren
@ 2025-11-21 14:31 ` Phillip Wood
2025-12-02 18:51 ` Patrick Steinhardt
1 sibling, 1 reply; 278+ messages in thread
From: Phillip Wood @ 2025-11-21 14:31 UTC (permalink / raw)
To: Patrick Steinhardt, git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
Hi Patrick
On 27/10/2025 11:33, Patrick Steinhardt wrote:
> It is quite a common use case that one wants to split up one commit into
> multiple commits by moving parts of the changes of the original commit
> out into a separate commit. This is quite an involved operation though:
>
> 1. Identify the commit in question that is to be dropped.
>
> 2. Perform an interactive rebase on top of that commit's parent.
>
> 3. Modify the instruction sheet to "edit" the commit that is to be
> split up.
>
> 4. Drop the commit via "git reset HEAD~".
>
> 5. Stage changes that should go into the first commit and commit it.
>
> 6. Stage changes that should go into the second commit and commit it.
>
> 7. Finalize the rebase.
>
> This is quite complex, and overall I would claim that most people who
> are not experts in Git would struggle with this flow.
If they want to test the split commit it's even more complicated because
they need to stash the unstaged changes. We should think about how we
can add support for testing split commits to this command in the future.
> Introduce a new "split" subcommand for git-history(1) to make this way
> easier. All the user needs to do is to say `git history split $COMMIT`.
> From hereon, Git asks the user which parts of the commit shall be moved
> out into a separate commit and, once done, asks the user for the commit
> message. Git then creates that split-out commit and applies the original
> commit on top of it.
As others have said (and I thought we'd agreed c.f.
<aMfdR3JE4zq-2j9b@pks.im>) I think it would be better to prompt the user
to edit the existing commit message when creating both commits.
Elsewhere Elijah mention being able to split a commit into more than two
commits. I wonder if we could loop running run_add_p_index() and
committing the result until there are no more changes left. It does mean
that the user has to actively select changes for the final commit though
which might be annoying. We can always play with that later.
Looking below I do wonder if we can share more code between subcommands
when it comes to checking the commit we're given on the command line and
re-creating a commit and having the user edit the message.
> +static int split_commit(struct repository *repo,
> + struct commit *original_commit,
> + struct pathspec *pathspec,
> + struct object_id *out)
> {
> [...]> + /*
> + * Construct the first commit. This is done by taking the original
> + * commit parent's tree and selectively patching changes from the diff
> + * between that parent and its child.
> + */
> + repo_git_path_replace(repo, &index_file, "%s", "history-split.index");
> +
> + read_tree_cmd.git_cmd = 1;
> + strvec_pushf(&read_tree_cmd.env, "GIT_INDEX_FILE=%s", index_file.buf);
> + strvec_push(&read_tree_cmd.args, "read-tree");
> + strvec_push(&read_tree_cmd.args, oid_to_hex(&parent_tree_oid));
> + ret = run_command(&read_tree_cmd);
Why do we need to fork "read-tree" here rather than call unpack_trees()
ourselves?
> [...]
> + /* We retain authorship of the original commit. */
> + original_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);
> + ptr = find_commit_header(original_message, "author", &len);
> + if (ptr)
> + original_author = xmemdupz(ptr, len);
> +
> + ret = fill_commit_message(repo, &parent_tree_oid, &split_tree->object.oid,
> + "", "split-out", &split_message);
> + if (ret < 0)
> + goto out;
> +
> + ret = commit_tree(split_message.buf, split_message.len, &split_tree->object.oid,
> + original_commit->parents, &out[0], original_author, NULL);
> + if (ret < 0) {
> + ret = error(_("failed writing split-out commit"));
> + goto out;
> + }
Don't we have the same code for rewording a commit, maybe we should
package this up into a shared helper function.
> +static int cmd_history_split(int argc,
> + const char **argv,
> + const char *prefix,
> + struct repository *repo)
> +{
> + const char * const usage[] = {
> + GIT_HISTORY_SPLIT_USAGE,
> + NULL,
> + };
> + struct option options[] = {
> + OPT_END(),
> + };
> + struct oidmap rewritten_commits = OIDMAP_INIT;
> + struct commit *original_commit, *parent, *head;
> + struct strvec commits = STRVEC_INIT;
> + struct commit_list *from_list = NULL;
> + struct object_id split_commits[2];
> + struct pathspec pathspec = { 0 };
> + int ret;
> +
> + argc = parse_options(argc, argv, prefix, options, usage, 0);
> + if (argc < 1) {
> + ret = error(_("command expects a revision"));
> + goto out;
> + }
> + repo_config(repo, git_default_config, NULL);
> +
> + original_commit = lookup_commit_reference_by_name(argv[0]);
> + if (!original_commit) {
> + ret = error(_("commit to be split cannot be found: %s"), argv[0]);
> + goto out;
> + }
> +
> + parent = original_commit->parents ? original_commit->parents->item : NULL;
> + if (parent && repo_parse_commit(repo, parent)) {
> + ret = error(_("unable to parse commit %s"),
> + oid_to_hex(&parent->object.oid));
> + goto out;
> + }
> +
> + head = lookup_commit_reference_by_name("HEAD");
> + if (!head) {
> + ret = error(_("could not resolve HEAD to a commit"));
> + goto out;
> + }
> +
> + commit_list_append(original_commit, &from_list);
> + if (!repo_is_descendant_of(repo, head, from_list)) {
> + ret = error(_("split commit must be reachable from current HEAD commit"));
> + goto out;
> + }
This is very similar to cmd_history_reword() up to this point. When we
add the "drop" and "amend" subcommands they're going to want to do the
same checks.
> + parse_pathspec(&pathspec, 0,
> + PATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,
> + prefix, argv + 1);
This and calling split_commit() below are the only real differences with
cmd_history_reword(), is it worth trying to share some more code between
the two?
> diff --git a/t/t3452-history-split.sh b/t/t3452-history-split.sh
> [...]
> +test_expect_success 'refuses to work with merge commits' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + test_commit base &&
> + git branch branch &&
> + test_commit ours &&
> + git switch branch &&
> + test_commit theirs &&
> + git switch - &&
> + git merge theirs &&
> + test_must_fail git history split HEAD 2>err &&
> + test_grep "cannot rearrange commit history with merges" err &&
> + test_must_fail git history split HEAD~ 2>err &&
> + test_grep "cannot rearrange commit history with merges" err
> + )
> +'
My comments from the reword tests apply here as well.
Thanks
Phillip
> +
> +test_expect_success 'refuses to work with unrelated commits' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + test_commit base &&
> + git branch branch &&
> + test_commit ours &&
> + git switch branch &&
> + test_commit theirs &&
> + test_must_fail git history split ours 2>err &&
> + test_grep "split commit must be reachable from current HEAD commit" err
> + )
> +'
> +
> +test_expect_success 'can split up tip commit' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + test_commit initial &&
> + touch bar foo &&
> + git add . &&
> + git commit -m split-me &&
> +
> + git symbolic-ref HEAD >expect &&
> + set_fake_editor "split-out commit" &&
> + git history split HEAD <<-EOF &&
> + y
> + n
> + EOF
> + git symbolic-ref HEAD >actual &&
> + test_cmp expect actual &&
> +
> + expect_log <<-EOF &&
> + split-me
> + split-out commit
> + initial
> + EOF
> +
> + expect_tree_entries HEAD~ <<-EOF &&
> + bar
> + initial.t
> + EOF
> +
> + expect_tree_entries HEAD <<-EOF
> + bar
> + foo
> + initial.t
> + EOF
> + )
> +'
> +
> +test_expect_success 'can split up root commit' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + touch bar foo &&
> + git add . &&
> + git commit -m root &&
> + test_commit tip &&
> +
> + set_fake_editor "split-out commit" &&
> + git history split HEAD~ <<-EOF &&
> + y
> + n
> + EOF
> +
> + expect_log <<-EOF &&
> + tip
> + root
> + split-out commit
> + EOF
> +
> + expect_tree_entries HEAD~2 <<-EOF &&
> + bar
> + EOF
> +
> + expect_tree_entries HEAD~ <<-EOF &&
> + bar
> + foo
> + EOF
> +
> + expect_tree_entries HEAD <<-EOF
> + bar
> + foo
> + tip.t
> + EOF
> + )
> +'
> +
> +test_expect_success 'can split up in-between commit' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + test_commit initial &&
> + touch bar foo &&
> + git add . &&
> + git commit -m split-me &&
> + test_commit tip &&
> +
> + set_fake_editor "split-out commit" &&
> + git history split HEAD~ <<-EOF &&
> + y
> + n
> + EOF
> +
> + expect_log <<-EOF &&
> + tip
> + split-me
> + split-out commit
> + initial
> + EOF
> +
> + expect_tree_entries HEAD~2 <<-EOF &&
> + bar
> + initial.t
> + EOF
> +
> + expect_tree_entries HEAD~ <<-EOF &&
> + bar
> + foo
> + initial.t
> + EOF
> +
> + expect_tree_entries HEAD <<-EOF
> + bar
> + foo
> + initial.t
> + tip.t
> + EOF
> + )
> +'
> +
> +test_expect_success 'can pick multiple hunks' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + touch bar baz foo qux &&
> + git add . &&
> + git commit -m split-me &&
> +
> + set_fake_editor "split-out-commit" &&
> + git history split HEAD <<-EOF &&
> + y
> + n
> + y
> + n
> + EOF
> +
> + expect_tree_entries HEAD~ <<-EOF &&
> + bar
> + foo
> + EOF
> +
> + expect_tree_entries HEAD <<-EOF
> + bar
> + baz
> + foo
> + qux
> + EOF
> + )
> +'
> +
> +
> +test_expect_success 'can use only last hunk' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + touch bar foo &&
> + git add . &&
> + git commit -m split-me &&
> +
> + set_fake_editor "split-out commit" &&
> + git history split HEAD <<-EOF &&
> + n
> + y
> + EOF
> +
> + expect_log <<-EOF &&
> + split-me
> + split-out commit
> + EOF
> +
> + expect_tree_entries HEAD~ <<-EOF &&
> + foo
> + EOF
> +
> + expect_tree_entries HEAD <<-EOF
> + bar
> + foo
> + EOF
> + )
> +'
> +
> +test_expect_success 'aborts with empty commit message' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + touch bar foo &&
> + git add . &&
> + git commit -m split-me &&
> +
> + set_fake_editor "" &&
> + test_must_fail git history split HEAD <<-EOF 2>err &&
> + y
> + n
> + EOF
> + test_grep "Aborting commit due to empty commit message." err
> + )
> +'
> +
> +test_expect_success 'commit message editor sees split-out changes' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + touch bar foo &&
> + git add . &&
> + git commit -m split-me &&
> +
> + write_script fake-editor.sh <<-\EOF &&
> + cp "$1" . &&
> + echo "some commit message" >>"$1"
> + EOF
> + test_set_editor "$(pwd)"/fake-editor.sh &&
> +
> + git history split HEAD <<-EOF &&
> + y
> + n
> + EOF
> +
> + cat >expect <<-EOF &&
> +
> + # Please enter the commit message for the split-out changes. Lines starting
> + # with ${SQ}#${SQ} will be ignored.
> + # Changes to be committed:
> + # new file: bar
> + #
> + EOF
> + test_cmp expect COMMIT_EDITMSG &&
> +
> + expect_log <<-EOF
> + split-me
> + some commit message
> + EOF
> + )
> +'
> +
> +test_expect_success 'can use pathspec to limit what gets split' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + touch bar foo &&
> + git add . &&
> + git commit -m split-me &&
> +
> + set_fake_editor "split-out commit" &&
> + git history split HEAD -- foo <<-EOF &&
> + y
> + EOF
> +
> + expect_tree_entries HEAD~ <<-EOF &&
> + foo
> + EOF
> +
> + expect_tree_entries HEAD <<-EOF
> + bar
> + foo
> + EOF
> + )
> +'
> +
> +test_expect_success 'refuses to create empty split-out commit' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + test_commit base &&
> + touch bar foo &&
> + git add . &&
> + git commit -m split-me &&
> +
> + test_must_fail git history split HEAD 2>err <<-EOF &&
> + n
> + n
> + EOF
> + test_grep "split commit is empty" err
> + )
> +'
> +
> +test_expect_success 'hooks are executed for rewritten commits' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + touch bar foo &&
> + git add . &&
> + git commit -m split-me &&
> + old_head=$(git rev-parse HEAD) &&
> +
> + write_script .git/hooks/prepare-commit-msg <<-EOF &&
> + touch "$(pwd)/hooks.log"
> + EOF
> + write_script .git/hooks/post-commit <<-EOF &&
> + touch "$(pwd)/hooks.log"
> + EOF
> + write_script .git/hooks/post-rewrite <<-EOF &&
> + touch "$(pwd)/hooks.log"
> + EOF
> +
> + set_fake_editor "split-out commit" &&
> + git history split HEAD <<-EOF &&
> + y
> + n
> + EOF
> +
> + expect_log <<-EOF &&
> + split-me
> + split-out commit
> + EOF
> +
> + test_path_is_missing hooks.log
> + )
> +'
> +
> +test_expect_success 'refuses to create empty original commit' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + touch bar foo &&
> + git add . &&
> + git commit -m split-me &&
> +
> + test_must_fail git history split HEAD 2>err <<-EOF &&
> + y
> + y
> + EOF
> + test_grep "split commit tree matches original commit" err
> + )
> +'
> +
> +test_expect_success 'retains changes in the worktree and index' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + echo a >a &&
> + echo b >b &&
> + git add . &&
> + git commit -m "initial commit" &&
> + echo a-modified >a &&
> + echo b-modified >b &&
> + git add b &&
> + set_fake_editor "a-only" &&
> + git history split HEAD <<-EOF &&
> + y
> + n
> + EOF
> +
> + expect_tree_entries HEAD~ <<-EOF &&
> + a
> + EOF
> + expect_tree_entries HEAD <<-EOF &&
> + a
> + b
> + EOF
> +
> + cat >expect <<-\EOF &&
> + M a
> + M b
> + ?? actual
> + ?? expect
> + ?? fake-editor.sh
> + EOF
> + git status --porcelain >actual &&
> + test_cmp expect actual
> + )
> +'
> +
> +test_done
>
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH v6 11/11] builtin/history: implement "split" subcommand
2025-11-21 14:31 ` Phillip Wood
@ 2025-12-02 18:51 ` Patrick Steinhardt
2025-12-10 9:51 ` Phillip Wood
0 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-12-02 18:51 UTC (permalink / raw)
To: phillip.wood
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
On Fri, Nov 21, 2025 at 02:31:14PM +0000, Phillip Wood wrote:
> Hi Patrick
>
> On 27/10/2025 11:33, Patrick Steinhardt wrote:
> > It is quite a common use case that one wants to split up one commit into
> > multiple commits by moving parts of the changes of the original commit
> > out into a separate commit. This is quite an involved operation though:
> >
> > 1. Identify the commit in question that is to be dropped.
> >
> > 2. Perform an interactive rebase on top of that commit's parent.
> >
> > 3. Modify the instruction sheet to "edit" the commit that is to be
> > split up.
> >
> > 4. Drop the commit via "git reset HEAD~".
> >
> > 5. Stage changes that should go into the first commit and commit it.
> >
> > 6. Stage changes that should go into the second commit and commit it.
> >
> > 7. Finalize the rebase.
> >
> > This is quite complex, and overall I would claim that most people who
> > are not experts in Git would struggle with this flow.
>
> If they want to test the split commit it's even more complicated because
> they need to stash the unstaged changes. We should think about how we can
> add support for testing split commits to this command in the future.
>
> > Introduce a new "split" subcommand for git-history(1) to make this way
> > easier. All the user needs to do is to say `git history split $COMMIT`.
> > From hereon, Git asks the user which parts of the commit shall be moved
> > out into a separate commit and, once done, asks the user for the commit
> > message. Git then creates that split-out commit and applies the original
> > commit on top of it.
>
> As others have said (and I thought we'd agreed c.f.
> <aMfdR3JE4zq-2j9b@pks.im>) I think it would be better to prompt the user to
> edit the existing commit message when creating both commits. Elsewhere
> Elijah mention being able to split a commit into more than two commits. I
> wonder if we could loop running run_add_p_index() and committing the result
> until there are no more changes left. It does mean that the user has to
> actively select changes for the final commit though which might be annoying.
> We can always play with that later.
Ah, right. Changing this now to prompt for both commit messages.
> Looking below I do wonder if we can share more code between subcommands when
> it comes to checking the commit we're given on the command line and
> re-creating a commit and having the user edit the message.
>
> > +static int split_commit(struct repository *repo,
> > + struct commit *original_commit,
> > + struct pathspec *pathspec,
> > + struct object_id *out)
> > {
> > [...]> + /*
> > + * Construct the first commit. This is done by taking the original
> > + * commit parent's tree and selectively patching changes from the diff
> > + * between that parent and its child.
> > + */
> > + repo_git_path_replace(repo, &index_file, "%s", "history-split.index");
> > +
> > + read_tree_cmd.git_cmd = 1;
> > + strvec_pushf(&read_tree_cmd.env, "GIT_INDEX_FILE=%s", index_file.buf);
> > + strvec_push(&read_tree_cmd.args, "read-tree");
> > + strvec_push(&read_tree_cmd.args, oid_to_hex(&parent_tree_oid));
> > + ret = run_command(&read_tree_cmd);
>
> Why do we need to fork "read-tree" here rather than call unpack_trees()
> ourselves?
This is an artifact of how the `run_add_p()` interfaces work. They
unfortunately do not work on top of an in-memory index, but they work on
an on-disk index.
> > [...]
> > + /* We retain authorship of the original commit. */
> > + original_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);
> > + ptr = find_commit_header(original_message, "author", &len);
> > + if (ptr)
> > + original_author = xmemdupz(ptr, len);
> > +
> > + ret = fill_commit_message(repo, &parent_tree_oid, &split_tree->object.oid,
> > + "", "split-out", &split_message);
> > + if (ret < 0)
> > + goto out;
> > +
> > + ret = commit_tree(split_message.buf, split_message.len, &split_tree->object.oid,
> > + original_commit->parents, &out[0], original_author, NULL);
> > + if (ret < 0) {
> > + ret = error(_("failed writing split-out commit"));
> > + goto out;
> > + }
>
> Don't we have the same code for rewording a commit, maybe we should package
> this up into a shared helper function.
Hm, indeed, there's a bit of non-trivial logic here. I'll refactor this.
> > +static int cmd_history_split(int argc,
> > + const char **argv,
> > + const char *prefix,
> > + struct repository *repo)
> > +{
> > + const char * const usage[] = {
> > + GIT_HISTORY_SPLIT_USAGE,
> > + NULL,
> > + };
> > + struct option options[] = {
> > + OPT_END(),
> > + };
> > + struct oidmap rewritten_commits = OIDMAP_INIT;
> > + struct commit *original_commit, *parent, *head;
> > + struct strvec commits = STRVEC_INIT;
> > + struct commit_list *from_list = NULL;
> > + struct object_id split_commits[2];
> > + struct pathspec pathspec = { 0 };
> > + int ret;
> > +
> > + argc = parse_options(argc, argv, prefix, options, usage, 0);
> > + if (argc < 1) {
> > + ret = error(_("command expects a revision"));
> > + goto out;
> > + }
> > + repo_config(repo, git_default_config, NULL);
> > +
> > + original_commit = lookup_commit_reference_by_name(argv[0]);
> > + if (!original_commit) {
> > + ret = error(_("commit to be split cannot be found: %s"), argv[0]);
> > + goto out;
> > + }
> > +
> > + parent = original_commit->parents ? original_commit->parents->item : NULL;
> > + if (parent && repo_parse_commit(repo, parent)) {
> > + ret = error(_("unable to parse commit %s"),
> > + oid_to_hex(&parent->object.oid));
> > + goto out;
> > + }
> > +
> > + head = lookup_commit_reference_by_name("HEAD");
> > + if (!head) {
> > + ret = error(_("could not resolve HEAD to a commit"));
> > + goto out;
> > + }
> > +
> > + commit_list_append(original_commit, &from_list);
> > + if (!repo_is_descendant_of(repo, head, from_list)) {
> > + ret = error(_("split commit must be reachable from current HEAD commit"));
> > + goto out;
> > + }
>
> This is very similar to cmd_history_reword() up to this point. When we add
> the "drop" and "amend" subcommands they're going to want to do the same
> checks.
>
> > + parse_pathspec(&pathspec, 0,
> > + PATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,
> > + prefix, argv + 1);
>
> This and calling split_commit() below are the only real differences with
> cmd_history_reword(), is it worth trying to share some more code between the
> two?
Yup, done.
Thanks!
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH v6 11/11] builtin/history: implement "split" subcommand
2025-12-02 18:51 ` Patrick Steinhardt
@ 2025-12-10 9:51 ` Phillip Wood
2025-12-19 13:00 ` Patrick Steinhardt
0 siblings, 1 reply; 278+ messages in thread
From: Phillip Wood @ 2025-12-10 9:51 UTC (permalink / raw)
To: Patrick Steinhardt, phillip.wood
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
On 02/12/2025 18:51, Patrick Steinhardt wrote:
> On Fri, Nov 21, 2025 at 02:31:14PM +0000, Phillip Wood wrote:
>> On 27/10/2025 11:33, Patrick Steinhardt wrote:
>>> + * Construct the first commit. This is done by taking the original
>>> + * commit parent's tree and selectively patching changes from the diff
>>> + * between that parent and its child.
>>> + */
>>> + repo_git_path_replace(repo, &index_file, "%s", "history-split.index");
>>> +
>>> + read_tree_cmd.git_cmd = 1;
>>> + strvec_pushf(&read_tree_cmd.env, "GIT_INDEX_FILE=%s", index_file.buf);
>>> + strvec_push(&read_tree_cmd.args, "read-tree");
>>> + strvec_push(&read_tree_cmd.args, oid_to_hex(&parent_tree_oid));
>>> + ret = run_command(&read_tree_cmd);
>>
>> Why do we need to fork "read-tree" here rather than call unpack_trees()
>> ourselves?
>
> This is an artifact of how the `run_add_p()` interfaces work. They
> unfortunately do not work on top of an in-memory index, but they work on
> an on-disk index.
Oh I see, but why does that mean we need to fork a subprocess rather
than writing the index to disc in this process?
Thanks
Phillip
>>> [...]
>>> + /* We retain authorship of the original commit. */
>>> + original_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);
>>> + ptr = find_commit_header(original_message, "author", &len);
>>> + if (ptr)
>>> + original_author = xmemdupz(ptr, len);
>>> +
>>> + ret = fill_commit_message(repo, &parent_tree_oid, &split_tree->object.oid,
>>> + "", "split-out", &split_message);
>>> + if (ret < 0)
>>> + goto out;
>>> +
>>> + ret = commit_tree(split_message.buf, split_message.len, &split_tree->object.oid,
>>> + original_commit->parents, &out[0], original_author, NULL);
>>> + if (ret < 0) {
>>> + ret = error(_("failed writing split-out commit"));
>>> + goto out;
>>> + }
>>
>> Don't we have the same code for rewording a commit, maybe we should package
>> this up into a shared helper function.
>
> Hm, indeed, there's a bit of non-trivial logic here. I'll refactor this.
>
>>> +static int cmd_history_split(int argc,
>>> + const char **argv,
>>> + const char *prefix,
>>> + struct repository *repo)
>>> +{
>>> + const char * const usage[] = {
>>> + GIT_HISTORY_SPLIT_USAGE,
>>> + NULL,
>>> + };
>>> + struct option options[] = {
>>> + OPT_END(),
>>> + };
>>> + struct oidmap rewritten_commits = OIDMAP_INIT;
>>> + struct commit *original_commit, *parent, *head;
>>> + struct strvec commits = STRVEC_INIT;
>>> + struct commit_list *from_list = NULL;
>>> + struct object_id split_commits[2];
>>> + struct pathspec pathspec = { 0 };
>>> + int ret;
>>> +
>>> + argc = parse_options(argc, argv, prefix, options, usage, 0);
>>> + if (argc < 1) {
>>> + ret = error(_("command expects a revision"));
>>> + goto out;
>>> + }
>>> + repo_config(repo, git_default_config, NULL);
>>> +
>>> + original_commit = lookup_commit_reference_by_name(argv[0]);
>>> + if (!original_commit) {
>>> + ret = error(_("commit to be split cannot be found: %s"), argv[0]);
>>> + goto out;
>>> + }
>>> +
>>> + parent = original_commit->parents ? original_commit->parents->item : NULL;
>>> + if (parent && repo_parse_commit(repo, parent)) {
>>> + ret = error(_("unable to parse commit %s"),
>>> + oid_to_hex(&parent->object.oid));
>>> + goto out;
>>> + }
>>> +
>>> + head = lookup_commit_reference_by_name("HEAD");
>>> + if (!head) {
>>> + ret = error(_("could not resolve HEAD to a commit"));
>>> + goto out;
>>> + }
>>> +
>>> + commit_list_append(original_commit, &from_list);
>>> + if (!repo_is_descendant_of(repo, head, from_list)) {
>>> + ret = error(_("split commit must be reachable from current HEAD commit"));
>>> + goto out;
>>> + }
>>
>> This is very similar to cmd_history_reword() up to this point. When we add
>> the "drop" and "amend" subcommands they're going to want to do the same
>> checks.
>>
>>> + parse_pathspec(&pathspec, 0,
>>> + PATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,
>>> + prefix, argv + 1);
>>
>> This and calling split_commit() below are the only real differences with
>> cmd_history_reword(), is it worth trying to share some more code between the
>> two?
>
> Yup, done.
>
> Thanks!
>
> Patrick
>
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH v6 11/11] builtin/history: implement "split" subcommand
2025-12-10 9:51 ` Phillip Wood
@ 2025-12-19 13:00 ` Patrick Steinhardt
0 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-12-19 13:00 UTC (permalink / raw)
To: phillip.wood
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
On Wed, Dec 10, 2025 at 09:51:33AM +0000, Phillip Wood wrote:
> On 02/12/2025 18:51, Patrick Steinhardt wrote:
> > On Fri, Nov 21, 2025 at 02:31:14PM +0000, Phillip Wood wrote:
> > > On 27/10/2025 11:33, Patrick Steinhardt wrote:
> > > > + * Construct the first commit. This is done by taking the original
> > > > + * commit parent's tree and selectively patching changes from the diff
> > > > + * between that parent and its child.
> > > > + */
> > > > + repo_git_path_replace(repo, &index_file, "%s", "history-split.index");
> > > > +
> > > > + read_tree_cmd.git_cmd = 1;
> > > > + strvec_pushf(&read_tree_cmd.env, "GIT_INDEX_FILE=%s", index_file.buf);
> > > > + strvec_push(&read_tree_cmd.args, "read-tree");
> > > > + strvec_push(&read_tree_cmd.args, oid_to_hex(&parent_tree_oid));
> > > > + ret = run_command(&read_tree_cmd);
> > >
> > > Why do we need to fork "read-tree" here rather than call unpack_trees()
> > > ourselves?
> >
> > This is an artifact of how the `run_add_p()` interfaces work. They
> > unfortunately do not work on top of an in-memory index, but they work on
> > an on-disk index.
>
> Oh I see, but why does that mean we need to fork a subprocess rather than
> writing the index to disc in this process?
Fair indeed. I guess it was laziness because other parts of Git did it
the same way. Anyway, let me change this now.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing
2025-10-27 11:33 ` [PATCH v6 00/11] " Patrick Steinhardt
` (10 preceding siblings ...)
2025-10-27 11:33 ` [PATCH v6 11/11] builtin/history: implement "split" subcommand Patrick Steinhardt
@ 2025-11-12 19:13 ` Sergey Organov
2025-11-20 7:07 ` Elijah Newren
12 siblings, 0 replies; 278+ messages in thread
From: Sergey Organov @ 2025-11-12 19:13 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, D. Ben Knoble, Junio C Hamano, Jean-Noël AVILA,
Martin von Zweigbergk, Kristoffer Haugsbakk, Elijah Newren,
Karthik Nayak
Hi Patrick,
Patrick Steinhardt <ps@pks.im> writes:
> Hi,
>
> over recent months I've been playing around with Jujutsu quite
> frequently. While I still prefer using Git, there's been a couple
> features in it that I really like and that I'd like to have in Git, as
> well.
[disclaimer: I didn't follow all the discussions closely enough, so
excuse me if these things below have been already addressed]
Are you aware of 'git revise', I wonder?
https://github.com/mystor/git-revise
In particular, its
git revise --interactive --edit
feature helps a lot in massive editing of commit messages. It creates
single file with all the commit messages (similar to todo list), and
then applies all the changes you make to the file as it revises
particular commits. Very handy.
It'd be nice to have something like this in the mainstream Git.
BTW, it has commit split feature as well, though I didn't use it myself.
Thanks,
-- Sergey Organov
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing
2025-10-27 11:33 ` [PATCH v6 00/11] " Patrick Steinhardt
` (11 preceding siblings ...)
2025-11-12 19:13 ` [PATCH v6 00/11] Introduce git-history(1) command for easy history editing Sergey Organov
@ 2025-11-20 7:07 ` Elijah Newren
2025-11-20 20:28 ` Junio C Hamano
12 siblings, 1 reply; 278+ messages in thread
From: Elijah Newren @ 2025-11-20 7:07 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: git, D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Karthik Nayak
On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:
>
> Hi,
>
> over recent months I've been playing around with Jujutsu quite
> frequently. While I still prefer using Git, there's been a couple
> features in it that I really like and that I'd like to have in Git, as
> well.
>
> A copule of these features relate to history editing. Most importantly,
> I really dig the following commands:
>
> - jj-abandon(1) to drop a specific commit from your history.
>
> - jj-absorb(1) to take some changes and automatically apply them to
> commits in your history that last modified the respective hunks.
>
> - jj-split(1) to split a commit into two.
>
> - jj-new(1) to insert a new commit after or before a specific other
> commit.
>
> Not all of these commands can be ported directly into Git. jj-new(1) for
> example doesn't really make a ton of sense for us, I'd claim. But some
> of these commands _do_ make sense.
>
> This patch series is a starting point for such a command. I've
> significantly slimmed it down from the first couple revisions now
> following the discussions at the Contributor's Summit yesterday. This
> was my intent anyway, as I already mentioned on the last iteration.
Sorry for taking so long to review the series now that it's based on
replay. Thanks for working on this!
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing
2025-11-20 7:07 ` Elijah Newren
@ 2025-11-20 20:28 ` Junio C Hamano
2025-11-20 20:40 ` Elijah Newren
0 siblings, 1 reply; 278+ messages in thread
From: Junio C Hamano @ 2025-11-20 20:28 UTC (permalink / raw)
To: Elijah Newren
Cc: Patrick Steinhardt, git, D. Ben Knoble, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Karthik Nayak
Elijah Newren <newren@gmail.com> writes:
>> This patch series is a starting point for such a command. I've
>> significantly slimmed it down from the first couple revisions now
>> following the discussions at the Contributor's Summit yesterday. This
>> was my intent anyway, as I already mentioned on the last iteration.
>
> Sorry for taking so long to review the series now that it's based on
> replay. Thanks for working on this!
With your comments and Phillip's, it seems that we are very close to
a good stopping point. Let me mark the topic as expecting a
hopefully small and final reroll before getting ready for 'next'.
Thanks, all.
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing
2025-11-20 20:28 ` Junio C Hamano
@ 2025-11-20 20:40 ` Elijah Newren
2025-11-20 20:49 ` Junio C Hamano
0 siblings, 1 reply; 278+ messages in thread
From: Elijah Newren @ 2025-11-20 20:40 UTC (permalink / raw)
To: Junio C Hamano
Cc: Patrick Steinhardt, git, D. Ben Knoble, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Karthik Nayak
On Thu, Nov 20, 2025 at 12:28 PM Junio C Hamano <gitster@pobox.com> wrote:
>
> Elijah Newren <newren@gmail.com> writes:
>
> >> This patch series is a starting point for such a command. I've
> >> significantly slimmed it down from the first couple revisions now
> >> following the discussions at the Contributor's Summit yesterday. This
> >> was my intent anyway, as I already mentioned on the last iteration.
> >
> > Sorry for taking so long to review the series now that it's based on
> > replay. Thanks for working on this!
>
> With your comments and Phillip's, it seems that we are very close to
> a good stopping point. Let me mark the topic as expecting a
> hopefully small and final reroll before getting ready for 'next'.
>
> Thanks, all.
I'm a little unsure if it'll be small or just one reroll. Some of the
changes for patches 5 & 9 might be big (but straightforward), there's
also a couple design related questions (single branch, HEAD-centric)
that might bring up bigger usability issues to address (if a commit
being edited is part of multiple branches, do we just rewrite all of
them by default, or error out unless the user specifies how they want
it handled)?, and a potential gotcha on patch 11 (how can you preserve
the index and working tree if the user edits the patch while splitting
a commit?) that may require rethinking or restricting that feature.
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing
2025-11-20 20:40 ` Elijah Newren
@ 2025-11-20 20:49 ` Junio C Hamano
2025-11-20 22:02 ` Elijah Newren
0 siblings, 1 reply; 278+ messages in thread
From: Junio C Hamano @ 2025-11-20 20:49 UTC (permalink / raw)
To: Elijah Newren
Cc: Patrick Steinhardt, git, D. Ben Knoble, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Karthik Nayak
Elijah Newren <newren@gmail.com> writes:
> On Thu, Nov 20, 2025 at 12:28 PM Junio C Hamano <gitster@pobox.com> wrote:
>>
>> Elijah Newren <newren@gmail.com> writes:
>>
>> >> This patch series is a starting point for such a command. I've
>> >> significantly slimmed it down from the first couple revisions now
>> >> following the discussions at the Contributor's Summit yesterday. This
>> >> was my intent anyway, as I already mentioned on the last iteration.
>> >
>> > Sorry for taking so long to review the series now that it's based on
>> > replay. Thanks for working on this!
>>
>> With your comments and Phillip's, it seems that we are very close to
>> a good stopping point. Let me mark the topic as expecting a
>> hopefully small and final reroll before getting ready for 'next'.
>>
>> Thanks, all.
>
> I'm a little unsure if it'll be small or just one reroll. Some of the
> changes for patches 5 & 9 might be big (but straightforward), there's
> also a couple design related questions (single branch, HEAD-centric)
> that might bring up bigger usability issues to address (if a commit
> being edited is part of multiple branches, do we just rewrite all of
> them by default, or error out unless the user specifies how they want
> it handled)?, and a potential gotcha on patch 11 (how can you preserve
> the index and working tree if the user edits the patch while splitting
> a commit?) that may require rethinking or restricting that feature.
Perhaps. But I thought the existing patches limited its initial
scope small and manageable that by operating only on a single strand
of pearls, with an intention to extend to cover more cases later. I
was hoping that we can start small and simple, initially limiting it
to single branch, etc., in other areas that require design
decisions.
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing
2025-11-20 20:49 ` Junio C Hamano
@ 2025-11-20 22:02 ` Elijah Newren
2025-11-21 14:31 ` Phillip Wood
0 siblings, 1 reply; 278+ messages in thread
From: Elijah Newren @ 2025-11-20 22:02 UTC (permalink / raw)
To: Junio C Hamano
Cc: Patrick Steinhardt, git, D. Ben Knoble, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Karthik Nayak
On Thu, Nov 20, 2025 at 12:49 PM Junio C Hamano <gitster@pobox.com> wrote:
>
> Elijah Newren <newren@gmail.com> writes:
>
> > On Thu, Nov 20, 2025 at 12:28 PM Junio C Hamano <gitster@pobox.com> wrote:
> >>
> >> Elijah Newren <newren@gmail.com> writes:
> >>
> >> >> This patch series is a starting point for such a command. I've
> >> >> significantly slimmed it down from the first couple revisions now
> >> >> following the discussions at the Contributor's Summit yesterday. This
> >> >> was my intent anyway, as I already mentioned on the last iteration.
> >> >
> >> > Sorry for taking so long to review the series now that it's based on
> >> > replay. Thanks for working on this!
> >>
> >> With your comments and Phillip's, it seems that we are very close to
> >> a good stopping point. Let me mark the topic as expecting a
> >> hopefully small and final reroll before getting ready for 'next'.
> >>
> >> Thanks, all.
> >
> > I'm a little unsure if it'll be small or just one reroll. Some of the
> > changes for patches 5 & 9 might be big (but straightforward), there's
> > also a couple design related questions (single branch, HEAD-centric)
> > that might bring up bigger usability issues to address (if a commit
> > being edited is part of multiple branches, do we just rewrite all of
> > them by default, or error out unless the user specifies how they want
> > it handled)?, and a potential gotcha on patch 11 (how can you preserve
> > the index and working tree if the user edits the patch while splitting
> > a commit?) that may require rethinking or restricting that feature.
>
> Perhaps. But I thought the existing patches limited its initial
> scope small and manageable that by operating only on a single strand
> of pearls, with an intention to extend to cover more cases later. I
> was hoping that we can start small and simple, initially limiting it
> to single branch, etc., in other areas that require design
> decisions.
So, you are referring to the single branch, HEAD-centric piece of the
feedback. The funny thing there is that operating on a more limited
case, without checking and verifying that you are indeed in the more
limited case (and erroring out if not), risks painting us into a
corner or providing some really buggy behavior when we aren't actually
in that case. To me, it opens a can of worms and makes the problem
scope bigger instead of smaller. Funnily enough, the single branch
thing is also the one piece of this that I think could be solved by a
fairly small change in the reroll (and I pointed out how in the
comments), so the limited view really didn't buy anything here IMO.
The other problems are independent of whether you try to limit the
scope initially in such a manner:
Are the testcases and the code requiring something for the feature
(ensuring the index and worktree are preserved) doing something that
is incompatible with the capabilities given to the user (allowing them
to edit the patch while splitting, so that they stage stuff that
wasn't part of the original commit)? Or...is it assumed that the
split commits always "sum" to the changes in the original commit,
meaning the "other" patch immediately undoes those extra changes?
(Perhaps it's the latter, which I didn't think of until now, so maybe
we are closer to a solution than I realized. In fact, re-reading the
code that looks like it does do that and I just missed it. But,
perhaps having users edit the patch when splitting commits is a
special case that should be called out in the docs, since that might
surprise users who try it?)
I'm also worried about extended header handling for the edited
(reworded or split) commits. That seems to have been overlooked in
this series, despite the fact that in early versions extended headers
were explicitly called out for the remainder of the commits being
replayed/rebased, so it seems interesting that they weren't considered
for the commits explicitly being edited.
And I'm a bit surprised that the original commit message for a split
commit is automatically associated with the second commit; if I had
been forced to choose, I would have assumed it should be associated
with the first.
Granted, I think good progress is being made and perhaps the changes
needed for the rest aren't that huge (and maybe there's more pieces
I'm not quite understanding yet similar to the
two-split-patches-always-summing-to-the-original), I was just a little
surprised that my comments are summarized by "expecting a small and
final reroll". :-)
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing
2025-11-20 22:02 ` Elijah Newren
@ 2025-11-21 14:31 ` Phillip Wood
2025-11-21 16:01 ` Junio C Hamano
2025-11-23 2:30 ` [PATCH v6 00/11] Introduce git-history(1) command for easy history editing Elijah Newren
0 siblings, 2 replies; 278+ messages in thread
From: Phillip Wood @ 2025-11-21 14:31 UTC (permalink / raw)
To: Elijah Newren, Junio C Hamano
Cc: Patrick Steinhardt, git, D. Ben Knoble, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Karthik Nayak
On 20/11/2025 22:02, Elijah Newren wrote:
> On Thu, Nov 20, 2025 at 12:49 PM Junio C Hamano <gitster@pobox.com> wrote:
>> Elijah Newren <newren@gmail.com> writes:
>>>
>>> I'm a little unsure if it'll be small or just one reroll. Some of the
>>> changes for patches 5 & 9 might be big (but straightforward), there's
>>> also a couple design related questions (single branch, HEAD-centric)
>>> that might bring up bigger usability issues to address (if a commit
>>> being edited is part of multiple branches, do we just rewrite all of
>>> them by default, or error out unless the user specifies how they want
>>> it handled)?, and a potential gotcha on patch 11 (how can you preserve
>>> the index and working tree if the user edits the patch while splitting
>>> a commit?) that may require rethinking or restricting that feature.
>>
>> Perhaps. But I thought the existing patches limited its initial
>> scope small and manageable that by operating only on a single strand
>> of pearls, with an intention to extend to cover more cases later. I
>> was hoping that we can start small and simple, initially limiting it
>> to single branch, etc., in other areas that require design
>> decisions.
>
> So, you are referring to the single branch, HEAD-centric piece of the
> feedback. The funny thing there is that operating on a more limited
> case, without checking and verifying that you are indeed in the more
> limited case (and erroring out if not), risks painting us into a
> corner or providing some really buggy behavior when we aren't actually
> in that case. To me, it opens a can of worms and makes the problem
> scope bigger instead of smaller. Funnily enough, the single branch
> thing is also the one piece of this that I think could be solved by a
> fairly small change in the reroll (and I pointed out how in the
> comments), so the limited view really didn't buy anything here IMO.
I can't find that comment. Are you referring to reusing more of the
replay machinery? If so we have the problem that the user gives a single
commit to "git history" so we don't have a handy revision range to pass
to the replay machinery unless we assume we're rewriting an ancestor of
HEAD or we go and find all the branches descended from the commit the
user gave us. Long term we should certainly do the latter but depending
on how much work it is to implement that we may want to go with the
single branch case at first
> The other problems are independent of whether you try to limit the
> scope initially in such a manner:
>
> Are the testcases and the code requiring something for the feature
> (ensuring the index and worktree are preserved) doing something that
> is incompatible with the capabilities given to the user (allowing them
> to edit the patch while splitting, so that they stage stuff that
> wasn't part of the original commit)? Or...is it assumed that the
> split commits always "sum" to the changes in the original commit,
> meaning the "other" patch immediately undoes those extra changes?
Yes that's what's implemented. I think that makes sense for the "split"
command. Often when splitting a commit one needs to make small changes
to the diff in order for the result to compile but you still want the
same end state from the sum of the split commits.
> I'm also worried about extended header handling for the edited
> (reworded or split) commits. That seems to have been overlooked in
> this series, despite the fact that in early versions extended headers
> were explicitly called out for the remainder of the commits being
> replayed/rebased, so it seems interesting that they weren't considered
> for the commits explicitly being edited.
What headers does it make sense to copy when splitting a commit? When
rewording it is more likely that copying the extended headers is what
the user wants but the example of the "encoding" header you gave does
not make sense to me as we re-encode the commit message and author data
when the user edit's the message so we're not preserving the original
encoding.
> And I'm a bit surprised that the original commit message for a split
> commit is automatically associated with the second commit; if I had
> been forced to choose, I would have assumed it should be associated
> with the first.
I don't think it is safe to assume either - we should prompt the user to
edit the message when creating both commits and seed the editor with the
original message.
> Granted, I think good progress is being made and perhaps the changes
> needed for the rest aren't that huge (and maybe there's more pieces
> I'm not quite understanding yet similar to the
> two-split-patches-always-summing-to-the-original), I was just a little
> surprised that my comments are summarized by "expecting a small and
> final reroll". :-)
Yes I'm not expecting any new functionality but I am expecting a bit
more than tiny cleanup.
Thanks
Phillip
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing
2025-11-21 14:31 ` Phillip Wood
@ 2025-11-21 16:01 ` Junio C Hamano
2025-11-23 2:54 ` Elijah Newren
2025-11-23 2:30 ` [PATCH v6 00/11] Introduce git-history(1) command for easy history editing Elijah Newren
1 sibling, 1 reply; 278+ messages in thread
From: Junio C Hamano @ 2025-11-21 16:01 UTC (permalink / raw)
To: Phillip Wood
Cc: Elijah Newren, Patrick Steinhardt, git, D. Ben Knoble,
Sergey Organov, Jean-Noël AVILA, Martin von Zweigbergk,
Kristoffer Haugsbakk, Karthik Nayak
Phillip Wood <phillip.wood123@gmail.com> writes:
>> Granted, I think good progress is being made and perhaps the changes
>> needed for the rest aren't that huge (and maybe there's more pieces
>> I'm not quite understanding yet similar to the
>> two-split-patches-always-summing-to-the-original), I was just a little
>> surprised that my comments are summarized by "expecting a small and
>> final reroll". :-)
It was because I didn't think (and I still do not think) your
comments are something for the immediate future, for a tool that
wants to present its minimum-serviceable experimental version to
users so that the users can experiment, extend it more and fix its
behaviour incrementally. I may have been probably wrong, but I was
getting an impression from the reviews that it is getting to there,
not the feature-perfect version that needs only maintenance from now
on, but a minimum-serviceable one.
We could instead of collect all the I want moon and I want cheeze
comments and iterate until the tool has all of them before it hits
'next', but I do not think it is often what we do to a new feature.
> Yes I'm not expecting any new functionality but I am expecting a bit
> more than tiny cleanup.
OK. Then we'd need a non-trivial amount of work before we get
there.
Thanks.
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing
2025-11-21 16:01 ` Junio C Hamano
@ 2025-11-23 2:54 ` Elijah Newren
2025-12-02 18:49 ` Patrick Steinhardt
0 siblings, 1 reply; 278+ messages in thread
From: Elijah Newren @ 2025-11-23 2:54 UTC (permalink / raw)
To: Junio C Hamano
Cc: Phillip Wood, Patrick Steinhardt, git, D. Ben Knoble,
Sergey Organov, Jean-Noël AVILA, Martin von Zweigbergk,
Kristoffer Haugsbakk, Karthik Nayak
On Fri, Nov 21, 2025 at 8:01 AM Junio C Hamano <gitster@pobox.com> wrote:
>
> Phillip Wood <phillip.wood123@gmail.com> writes:
>
> >> Granted, I think good progress is being made and perhaps the changes
> >> needed for the rest aren't that huge (and maybe there's more pieces
> >> I'm not quite understanding yet similar to the
> >> two-split-patches-always-summing-to-the-original), I was just a little
> >> surprised that my comments are summarized by "expecting a small and
> >> final reroll". :-)
>
> It was because I didn't think (and I still do not think) your
> comments are something for the immediate future, for a tool that
> wants to present its minimum-serviceable experimental version to
> users so that the users can experiment, extend it more and fix its
> behaviour incrementally. I may have been probably wrong, but I was
> getting an impression from the reviews that it is getting to there,
> not the feature-perfect version that needs only maintenance from now
> on, but a minimum-serviceable one.
>
> We could instead of collect all the I want moon and I want cheeze
> comments and iterate until the tool has all of them before it hits
> 'next', but I do not think it is often what we do to a new feature.
You're reading my feedback as feature requests rather than as bugs
and/or possible paint-ourselves-in-a-corner situations in the
presented implementation? I must have described things rather poorly;
if they were just feature requests, I'd agree we could just implement
them later.
But maybe I see where the confusion arises, since you were focusing
solely on the single branch thing; that's the one issue where it's
perhaps not as clear whether I was discussing a bug or a new feature.
Let me try to explain that case another way:
The early cover letters said they focused on a case where just a
single branch was involved, yet they don't check whether there really
is only one branch involved for safe operation.
If a user tries to reword or split a commit that is in the history of
multiple branches, the current implementation does not check and makes
the branches diverge. Some users may want that, though I suspect most
would be negatively surprised. The commit messages and code do not
even discuss this case. If we merge the code as-is and then later
notice and fix this problem soon enough, maybe we'd be fine, but I
always worry a bit about a git-switch/git-restore kind of case where
it sits long enough and people depend on side-effects in a way that
prevents us from fixing it. Besides, since the bug has been
identified and there are multiple not-too-hard ways to fix, I think we
should do something. Some possibilities:
* Document this case and warn users to check on their own (not that
friendly, but might be good enough for the first cut).
* Check if the user-specified commit is part of multiple branches
and error out, unless the user provides a flag verifying that they
want histories to diverge.
* Just rewrite all relevant branches
The third of those could sound like a feature request in isolation,
but I raised it primarily because it's a potential fix to this
overlooked bug. I mentioned all three possible fixes, but assumed
others didn't realize how simple that third option was, so I pointed
out how easy it was with some code (~12 lines, which also replace many
more existing lines of code). I personally think the third option is
*less work* than the second option, and that the focus on trying to
limit to a single branch is creating more work rather than simplifying
the problem. But if folks really do want to limit to a single branch
despite the code existing to handle the more general case, then we can
implement one of the other solutions. (If we do so, I still think
choice three is more friendly to users, to cpus, and to future
extension of these features while also simultaneously simplifying the
existing code; so I'll volunteer to investigate and post patches on
top of this series if others decide to go with one of the other
choices for the initial version of the feature.) My main point here
is just that there is a clear, un-discussed bug, and it should be
addressed somehow in the initial version of the feature.
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing
2025-11-23 2:54 ` Elijah Newren
@ 2025-12-02 18:49 ` Patrick Steinhardt
2025-12-05 8:49 ` Elijah Newren
2025-12-09 18:29 ` Kristoffer Haugsbakk
0 siblings, 2 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-12-02 18:49 UTC (permalink / raw)
To: Elijah Newren
Cc: Junio C Hamano, Phillip Wood, git, D. Ben Knoble, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Karthik Nayak
On Sat, Nov 22, 2025 at 06:54:00PM -0800, Elijah Newren wrote:
> On Fri, Nov 21, 2025 at 8:01 AM Junio C Hamano <gitster@pobox.com> wrote:
> >
> > Phillip Wood <phillip.wood123@gmail.com> writes:
> >
> > >> Granted, I think good progress is being made and perhaps the changes
> > >> needed for the rest aren't that huge (and maybe there's more pieces
> > >> I'm not quite understanding yet similar to the
> > >> two-split-patches-always-summing-to-the-original), I was just a little
> > >> surprised that my comments are summarized by "expecting a small and
> > >> final reroll". :-)
> >
> > It was because I didn't think (and I still do not think) your
> > comments are something for the immediate future, for a tool that
> > wants to present its minimum-serviceable experimental version to
> > users so that the users can experiment, extend it more and fix its
> > behaviour incrementally. I may have been probably wrong, but I was
> > getting an impression from the reviews that it is getting to there,
> > not the feature-perfect version that needs only maintenance from now
> > on, but a minimum-serviceable one.
> >
> > We could instead of collect all the I want moon and I want cheeze
> > comments and iterate until the tool has all of them before it hits
> > 'next', but I do not think it is often what we do to a new feature.
>
> You're reading my feedback as feature requests rather than as bugs
> and/or possible paint-ourselves-in-a-corner situations in the
> presented implementation? I must have described things rather poorly;
> if they were just feature requests, I'd agree we could just implement
> them later.
The command is explicitly marked as experimental so that we can iterate
on its behaviour as needed. So I don't think we're painting us into a
corner yet.
> But maybe I see where the confusion arises, since you were focusing
> solely on the single branch thing; that's the one issue where it's
> perhaps not as clear whether I was discussing a bug or a new feature.
> Let me try to explain that case another way:
>
>
> The early cover letters said they focused on a case where just a
> single branch was involved, yet they don't check whether there really
> is only one branch involved for safe operation.
I think this depends on the definition. We _do_ verify that the commit
that is to be edited is part of the current branch. What we _don't_
check is that the commit is _only_ part of that current branch.
I'm not sure whether that is something we want. I myself have the same
commit on multiple branches quite regularly, as I tend to queue up
multiple dependent patch series. But I wouldn't ever want a history edit
to affect all of these branches myself, I really only want it to modify
the branch I'm currently on.
> If a user tries to reword or split a commit that is in the history of
> multiple branches, the current implementation does not check and makes
> the branches diverge. Some users may want that, though I suspect most
> would be negatively surprised. The commit messages and code do not
> even discuss this case. If we merge the code as-is and then later
> notice and fix this problem soon enough, maybe we'd be fine, but I
> always worry a bit about a git-switch/git-restore kind of case where
> it sits long enough and people depend on side-effects in a way that
> prevents us from fixing it. Besides, since the bug has been
> identified and there are multiple not-too-hard ways to fix, I think we
> should do something. Some possibilities:
>
> * Document this case and warn users to check on their own (not that
> friendly, but might be good enough for the first cut).
> * Check if the user-specified commit is part of multiple branches
> and error out, unless the user provides a flag verifying that they
> want histories to diverge.
> * Just rewrite all relevant branches
>
> The third of those could sound like a feature request in isolation,
> but I raised it primarily because it's a potential fix to this
> overlooked bug. I mentioned all three possible fixes, but assumed
> others didn't realize how simple that third option was, so I pointed
> out how easy it was with some code (~12 lines, which also replace many
> more existing lines of code). I personally think the third option is
> *less work* than the second option, and that the focus on trying to
> limit to a single branch is creating more work rather than simplifying
> the problem. But if folks really do want to limit to a single branch
> despite the code existing to handle the more general case, then we can
> implement one of the other solutions. (If we do so, I still think
> choice three is more friendly to users, to cpus, and to future
> extension of these features while also simultaneously simplifying the
> existing code; so I'll volunteer to investigate and post patches on
> top of this series if others decide to go with one of the other
> choices for the initial version of the feature.) My main point here
> is just that there is a clear, un-discussed bug, and it should be
> addressed somehow in the initial version of the feature.
So with the above clarification I wouldn't call any of this a bug, but
rather working as designed. We could of course still print a warning in
that case to protect the user, but one problem I see is that generating
this warning could be quite expensive as we'd now have to walk all
references. That might be cheap in case the user only has short-lived
feature branchs. But it may very well not be cheap in case they for
example have old release branches checked out, as we'd now have to
potentially walk a significant portiion of history.
Consequently I'm leaning more into the direction of doing nothing. It's
not really clear to me that this is a bug, and we still can introduce a
flag in the future that opts into the behaviour of rewriting relevant
branches. That behaviour certainly can be useful, but I'd claim that
it would be rather surprising to the user if that was the default.
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing
2025-12-02 18:49 ` Patrick Steinhardt
@ 2025-12-05 8:49 ` Elijah Newren
2025-12-09 7:53 ` Patrick Steinhardt
2025-12-09 18:29 ` Kristoffer Haugsbakk
1 sibling, 1 reply; 278+ messages in thread
From: Elijah Newren @ 2025-12-05 8:49 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: Junio C Hamano, Phillip Wood, git, D. Ben Knoble, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Karthik Nayak
On Tue, Dec 2, 2025 at 10:50 AM Patrick Steinhardt <ps@pks.im> wrote:
>
> On Sat, Nov 22, 2025 at 06:54:00PM -0800, Elijah Newren wrote:
> > On Fri, Nov 21, 2025 at 8:01 AM Junio C Hamano <gitster@pobox.com> wrote:
> > >
> > > Phillip Wood <phillip.wood123@gmail.com> writes:
> > >
> > > >> Granted, I think good progress is being made and perhaps the changes
> > > >> needed for the rest aren't that huge (and maybe there's more pieces
> > > >> I'm not quite understanding yet similar to the
> > > >> two-split-patches-always-summing-to-the-original), I was just a little
> > > >> surprised that my comments are summarized by "expecting a small and
> > > >> final reroll". :-)
> > >
> > > It was because I didn't think (and I still do not think) your
> > > comments are something for the immediate future, for a tool that
> > > wants to present its minimum-serviceable experimental version to
> > > users so that the users can experiment, extend it more and fix its
> > > behaviour incrementally. I may have been probably wrong, but I was
> > > getting an impression from the reviews that it is getting to there,
> > > not the feature-perfect version that needs only maintenance from now
> > > on, but a minimum-serviceable one.
> > >
> > > We could instead of collect all the I want moon and I want cheeze
> > > comments and iterate until the tool has all of them before it hits
> > > 'next', but I do not think it is often what we do to a new feature.
> >
> > You're reading my feedback as feature requests rather than as bugs
> > and/or possible paint-ourselves-in-a-corner situations in the
> > presented implementation? I must have described things rather poorly;
> > if they were just feature requests, I'd agree we could just implement
> > them later.
>
> The command is explicitly marked as experimental so that we can iterate
> on its behaviour as needed. So I don't think we're painting us into a
> corner yet.
I'm aware it's marked as experimental and even commented on that at
least once in this series. I'm not sure that's sufficient _in this
kind of case_ for the following reasons:
* Users tend to not have multiple branches sharing commits and then
try to operate on those, it will only come up rarely. (I still think
it's a very important usecase, just that it's not common, in part
because git has trained people away from this kind of usecase.)
* Since the command is marked as experimental, it might extend the
timeline until we see such uses.
* The implemented behavior is not always a bug, some users will want
it (it's the kind of thing that makes sense as an option).
Combining the above, it might be a long time before folks really hit
this behavior and start pointing out its problems, rather than hitting
them early on. By then, we don't know how many users we have, just
that it's been a long time, and we are further faced with the fact
that some subset of users have begun to depend upon the existing
behavior. The combination may make it hard to change at that point.
To me, it feels very much like risking painting ourselves into a
corner. If it were an issue I felt people would likely hit right away
OR it was an issue which users would always view as a bug, then I'd
agree with you that there's no (or very little) risk with just
proceeding as-is.
> > But maybe I see where the confusion arises, since you were focusing
> > solely on the single branch thing; that's the one issue where it's
> > perhaps not as clear whether I was discussing a bug or a new feature.
> > Let me try to explain that case another way:
> >
> >
> > The early cover letters said they focused on a case where just a
> > single branch was involved, yet they don't check whether there really
> > is only one branch involved for safe operation.
>
> I think this depends on the definition. We _do_ verify that the commit
> that is to be edited is part of the current branch. What we _don't_
> check is that the commit is _only_ part of that current branch.
>
> I'm not sure whether that is something we want. I myself have the same
> commit on multiple branches quite regularly, as I tend to queue up
> multiple dependent patch series. But I wouldn't ever want a history edit
> to affect all of these branches myself, I really only want it to modify
> the branch I'm currently on.
To me at least, that feels crazy. Rebase was broken for multiple
interconnected or dependent branches precisely because you could only
update one branch, then needed to find the subset of the next branch
not contained in the first as well as finding where to graft that next
set onto in order to manually rebase it, then repeat for the third
branch, and so on. That's precisely the design mistake that made me
give up on rebase and write something else...and you want to
explicitly copy it?
(--update-refs did come along later to help in the common case that
each new branch fully contained the previous ones, but it didn't help
when you had multiple branches that built on some common base, or had
other interesting topologies, and thus didn't really fix the
underlying problem.)
> > If a user tries to reword or split a commit that is in the history of
> > multiple branches, the current implementation does not check and makes
> > the branches diverge. Some users may want that, though I suspect most
> > would be negatively surprised. The commit messages and code do not
> > even discuss this case. If we merge the code as-is and then later
> > notice and fix this problem soon enough, maybe we'd be fine, but I
> > always worry a bit about a git-switch/git-restore kind of case where
> > it sits long enough and people depend on side-effects in a way that
> > prevents us from fixing it. Besides, since the bug has been
> > identified and there are multiple not-too-hard ways to fix, I think we
> > should do something. Some possibilities:
> >
> > * Document this case and warn users to check on their own (not that
> > friendly, but might be good enough for the first cut).
> > * Check if the user-specified commit is part of multiple branches
> > and error out, unless the user provides a flag verifying that they
> > want histories to diverge.
> > * Just rewrite all relevant branches
> >
[...]
> So with the above clarification I wouldn't call any of this a bug, but
> rather working as designed. We could of course still print a warning in
> that case to protect the user, but one problem I see is that generating
> this warning could be quite expensive as we'd now have to walk all
> references. That might be cheap in case the user only has short-lived
> feature branchs. But it may very well not be cheap in case they for
> example have old release branches checked out, as we'd now have to
> potentially walk a significant portiion of history.
Munging user's histories in unexpected ways and depending on them to
figure out on their own that such has happened may well be much more
expensive. Granted, that's human time rather than cpu time, so it's
not directly comparable, but this feels like a big foot-gun, and I
think big foot-guns deserve good checks and warnings. I agree there's
a cost here, but I think the cost is warranted, especially since such
an error message would be the thing that frees us to iterate and
define the multi-branch behavior later.
> Consequently I'm leaning more into the direction of doing nothing. It's
> not really clear to me that this is a bug, and we still can introduce a
> flag in the future that opts into the behaviour of rewriting relevant
> branches. That behaviour certainly can be useful, but I'd claim that
> it would be rather surprising to the user if that was the default.
Well, as I stated above, this is basically copying what I view as the
fundamental design mistake of git-rebase. The many other points of
feedback I had on this series (e.g. extended headers, reusing replay's
walking, etc.) are things I could easily negotiate on; this one
bothers me much, much more. To me, it ruins the command and makes me
feel it is unsuitable for inclusion in git; this is, after all, the
kind of thing that made me decide to write yet another command to
workaround such a flaw. If the series is merged with this behavior,
I'm going to be in the awkward position of feeling I need to actively
recommend against its usage unless _and until_ we either
(a) check that a commit is only part of one branch before proceeding,
(b) always require the user to specify with a flag how to handle
commits that happen to be part of multiple branches (even when a
commit only happens to be part of one branch, in order to allow us to
not bother checking whether it's part of more),
or
(c) rewrite all branches that contain the given commit by default
(with an option to only rewrite the current one).
That said, obviously the choice of whether the series is merged isn't
up to me. And maybe I'm in the minority, and others don't care about
this issue at all. But it's how I feel about it.
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing
2025-12-05 8:49 ` Elijah Newren
@ 2025-12-09 7:53 ` Patrick Steinhardt
2025-12-09 17:43 ` Martin von Zweigbergk
2025-12-10 6:55 ` Elijah Newren
0 siblings, 2 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-12-09 7:53 UTC (permalink / raw)
To: Elijah Newren
Cc: Junio C Hamano, Phillip Wood, git, D. Ben Knoble, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Karthik Nayak
On Fri, Dec 05, 2025 at 12:49:04AM -0800, Elijah Newren wrote:
> On Tue, Dec 2, 2025 at 10:50 AM Patrick Steinhardt <ps@pks.im> wrote:
> > Consequently I'm leaning more into the direction of doing nothing. It's
> > not really clear to me that this is a bug, and we still can introduce a
> > flag in the future that opts into the behaviour of rewriting relevant
> > branches. That behaviour certainly can be useful, but I'd claim that
> > it would be rather surprising to the user if that was the default.
>
> Well, as I stated above, this is basically copying what I view as the
> fundamental design mistake of git-rebase. The many other points of
> feedback I had on this series (e.g. extended headers, reusing replay's
> walking, etc.) are things I could easily negotiate on; this one
> bothers me much, much more. To me, it ruins the command and makes me
> feel it is unsuitable for inclusion in git; this is, after all, the
> kind of thing that made me decide to write yet another command to
> workaround such a flaw. If the series is merged with this behavior,
> I'm going to be in the awkward position of feeling I need to actively
> recommend against its usage unless _and until_ we either
>
> (a) check that a commit is only part of one branch before proceeding,
> (b) always require the user to specify with a flag how to handle
> commits that happen to be part of multiple branches (even when a
> commit only happens to be part of one branch, in order to allow us to
> not bother checking whether it's part of more),
> or
> (c) rewrite all branches that contain the given commit by default
> (with an option to only rewrite the current one).
>
> That said, obviously the choice of whether the series is merged isn't
> up to me. And maybe I'm in the minority, and others don't care about
> this issue at all. But it's how I feel about it.
I guess it's a matter of workflows and tastes, and there's never going
to be the one correct way of doing things. I don't think (b) is a good
option as it makes things more complex even for the simplest cases. But
I wouldn't be opposed to a combination of (a) and (b) if we can
implement (a) efficiently.
Do we already have logic like this in git-replay(1)?
Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing
2025-12-09 7:53 ` Patrick Steinhardt
@ 2025-12-09 17:43 ` Martin von Zweigbergk
2025-12-10 11:32 ` Phillip Wood
2025-12-10 6:55 ` Elijah Newren
1 sibling, 1 reply; 278+ messages in thread
From: Martin von Zweigbergk @ 2025-12-09 17:43 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: Elijah Newren, Junio C Hamano, Phillip Wood, git, D. Ben Knoble,
Sergey Organov, Jean-Noël AVILA, Kristoffer Haugsbakk,
Karthik Nayak
On Mon, Dec 8, 2025 at 11:53 PM Patrick Steinhardt <ps@pks.im> wrote:
>
> On Fri, Dec 05, 2025 at 12:49:04AM -0800, Elijah Newren wrote:
> > On Tue, Dec 2, 2025 at 10:50 AM Patrick Steinhardt <ps@pks.im> wrote:
> > > Consequently I'm leaning more into the direction of doing nothing. It's
> > > not really clear to me that this is a bug, and we still can introduce a
> > > flag in the future that opts into the behaviour of rewriting relevant
> > > branches. That behaviour certainly can be useful, but I'd claim that
> > > it would be rather surprising to the user if that was the default.
> >
> > Well, as I stated above, this is basically copying what I view as the
> > fundamental design mistake of git-rebase. The many other points of
> > feedback I had on this series (e.g. extended headers, reusing replay's
> > walking, etc.) are things I could easily negotiate on; this one
> > bothers me much, much more. To me, it ruins the command and makes me
> > feel it is unsuitable for inclusion in git; this is, after all, the
> > kind of thing that made me decide to write yet another command to
> > workaround such a flaw. If the series is merged with this behavior,
> > I'm going to be in the awkward position of feeling I need to actively
> > recommend against its usage unless _and until_ we either
> >
> > (a) check that a commit is only part of one branch before proceeding,
> > (b) always require the user to specify with a flag how to handle
> > commits that happen to be part of multiple branches (even when a
> > commit only happens to be part of one branch, in order to allow us to
> > not bother checking whether it's part of more),
> > or
> > (c) rewrite all branches that contain the given commit by default
> > (with an option to only rewrite the current one).
> >
> > That said, obviously the choice of whether the series is merged isn't
> > up to me. And maybe I'm in the minority, and others don't care about
> > this issue at all. But it's how I feel about it.
>
> I guess it's a matter of workflows and tastes, and there's never going
> to be the one correct way of doing things. I don't think (b) is a good
> option as it makes things more complex even for the simplest cases. But
> I wouldn't be opposed to a combination of (a) and (b) if we can
> implement (a) efficiently.
FWIW, I agree with Elijah that (c) is the right end state. That's
perhaps not surprising given that that's what jj has been doing for
many years (as many of you know already, I'm one of the jj
maintainers). I think it's very rare that we hear from users that they
want to rewrite a commit and its descendants while leaving some of the
descendants in place. We have a `jj duplicate` command they can use,
but that won't move any bookmarks (branches) over. So if you have
bookmark `foo` with descendant bookmarks `bar` and `baz` and you want
to amend `foo` while moving `bar` over and keeping `baz` in place, the
simplest way is probably to duplicate all the necessary commit
(something like `jj duplicate -r main..bar`) and then manually move
over `foo` and `bar`. That seems like such a rare use case that we
haven't had a reason to make it simpler so far.
Regarding performance of (a), I would think that walking all
branches/tags until you reach the to-be-modified commit (or reach an
older generation) can usually be done quickly enough, but obviously
there are many others on this list who know that better than me :) But
I have also heard that some Git repos have tens (hundreds?) of
thousands of branches. Hopefully those refs are just on some server
where users won't run `git history`.
>
> Do we already have logic like this in git-replay(1)?
>
> Patrick
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing
2025-12-09 17:43 ` Martin von Zweigbergk
@ 2025-12-10 11:32 ` Phillip Wood
0 siblings, 0 replies; 278+ messages in thread
From: Phillip Wood @ 2025-12-10 11:32 UTC (permalink / raw)
To: Martin von Zweigbergk, Patrick Steinhardt
Cc: Elijah Newren, Junio C Hamano, git, D. Ben Knoble, Sergey Organov,
Jean-Noël AVILA, Kristoffer Haugsbakk, Karthik Nayak
On 09/12/2025 17:43, Martin von Zweigbergk wrote:
> On Mon, Dec 8, 2025 at 11:53 PM Patrick Steinhardt <ps@pks.im> wrote:
>> On Fri, Dec 05, 2025 at 12:49:04AM -0800, Elijah Newren wrote:
>>>
>>> (a) check that a commit is only part of one branch before proceeding,
>>> (b) always require the user to specify with a flag how to handle
>>> commits that happen to be part of multiple branches (even when a
>>> commit only happens to be part of one branch, in order to allow us to
>>> not bother checking whether it's part of more),
>>> or
>>> (c) rewrite all branches that contain the given commit by default
>>> (with an option to only rewrite the current one).
>>>
>>> That said, obviously the choice of whether the series is merged isn't
>>> up to me. And maybe I'm in the minority, and others don't care about
>>> this issue at all. But it's how I feel about it.
>>
>> I guess it's a matter of workflows and tastes, and there's never going
>> to be the one correct way of doing things. I don't think (b) is a good
>> option as it makes things more complex even for the simplest cases. But
>> I wouldn't be opposed to a combination of (a) and (b) if we can
>> implement (a) efficiently.
>
> FWIW, I agree with Elijah that (c) is the right end state.
FWIW I think so too, but there does seem to be a significant number of
people who find that behavior surprising so maybe we need a flag to
control what gets rewritten.
Thanks
Phillip
> That's
> perhaps not surprising given that that's what jj has been doing for
> many years (as many of you know already, I'm one of the jj
> maintainers). I think it's very rare that we hear from users that they
> want to rewrite a commit and its descendants while leaving some of the
> descendants in place. We have a `jj duplicate` command they can use,
> but that won't move any bookmarks (branches) over. So if you have
> bookmark `foo` with descendant bookmarks `bar` and `baz` and you want
> to amend `foo` while moving `bar` over and keeping `baz` in place, the
> simplest way is probably to duplicate all the necessary commit
> (something like `jj duplicate -r main..bar`) and then manually move
> over `foo` and `bar`. That seems like such a rare use case that we
> haven't had a reason to make it simpler so far.
>
> Regarding performance of (a), I would think that walking all
> branches/tags until you reach the to-be-modified commit (or reach an
> older generation) can usually be done quickly enough, but obviously
> there are many others on this list who know that better than me :) But
> I have also heard that some Git repos have tens (hundreds?) of
> thousands of branches. Hopefully those refs are just on some server
> where users won't run `git history`.
>
>>
>> Do we already have logic like this in git-replay(1)?
>>
>> Patrick
>
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing
2025-12-09 7:53 ` Patrick Steinhardt
2025-12-09 17:43 ` Martin von Zweigbergk
@ 2025-12-10 6:55 ` Elijah Newren
1 sibling, 0 replies; 278+ messages in thread
From: Elijah Newren @ 2025-12-10 6:55 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: Junio C Hamano, Phillip Wood, git, D. Ben Knoble, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Karthik Nayak
On Mon, Dec 8, 2025 at 11:53 PM Patrick Steinhardt <ps@pks.im> wrote:
>
> On Fri, Dec 05, 2025 at 12:49:04AM -0800, Elijah Newren wrote:
> > On Tue, Dec 2, 2025 at 10:50 AM Patrick Steinhardt <ps@pks.im> wrote:
> > > Consequently I'm leaning more into the direction of doing nothing. It's
> > > not really clear to me that this is a bug, and we still can introduce a
> > > flag in the future that opts into the behaviour of rewriting relevant
> > > branches. That behaviour certainly can be useful, but I'd claim that
> > > it would be rather surprising to the user if that was the default.
> >
> > Well, as I stated above, this is basically copying what I view as the
> > fundamental design mistake of git-rebase. The many other points of
> > feedback I had on this series (e.g. extended headers, reusing replay's
> > walking, etc.) are things I could easily negotiate on; this one
> > bothers me much, much more. To me, it ruins the command and makes me
> > feel it is unsuitable for inclusion in git; this is, after all, the
> > kind of thing that made me decide to write yet another command to
> > workaround such a flaw. If the series is merged with this behavior,
> > I'm going to be in the awkward position of feeling I need to actively
> > recommend against its usage unless _and until_ we either
> >
> > (a) check that a commit is only part of one branch before proceeding,
> > (b) always require the user to specify with a flag how to handle
> > commits that happen to be part of multiple branches (even when a
> > commit only happens to be part of one branch, in order to allow us to
> > not bother checking whether it's part of more),
> > or
> > (c) rewrite all branches that contain the given commit by default
> > (with an option to only rewrite the current one).
> >
> > That said, obviously the choice of whether the series is merged isn't
> > up to me. And maybe I'm in the minority, and others don't care about
> > this issue at all. But it's how I feel about it.
>
> I guess it's a matter of workflows and tastes, and there's never going
> to be the one correct way of doing things. I don't think (b) is a good
> option as it makes things more complex even for the simplest cases. But
> I wouldn't be opposed to a combination of (a) and (b) if we can
> implement (a) efficiently.
>
> Do we already have logic like this in git-replay(1)?
No, git-replay was written from the beginning with the idea in mind of
handling multiple branches (e.g. letting Junio edit a single commit in
someone's topic and updating all the subsequent commits and merges
without having to individually fuss with them all, or similarly for
the Git For Windows or Microsoft Git forks, or at a smaller level if I
have multiple topics that share a few commits and I want to update one
of those), so the idea was always (c) by default, with options for
alternate behavior, which is kind of the opposite angle you are
approaching from. Anyway, because of that view, nothing like (a) was
ever implemented.
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing
2025-12-02 18:49 ` Patrick Steinhardt
2025-12-05 8:49 ` Elijah Newren
@ 2025-12-09 18:29 ` Kristoffer Haugsbakk
2025-12-12 22:00 ` Working on top of mega merges D. Ben Knoble
1 sibling, 1 reply; 278+ messages in thread
From: Kristoffer Haugsbakk @ 2025-12-09 18:29 UTC (permalink / raw)
To: Patrick Steinhardt, Elijah Newren
Cc: Junio C Hamano, Phillip Wood, git, D. Ben Knoble, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Karthik Nayak
On Tue, Dec 2, 2025, at 19:49, Patrick Steinhardt wrote:
>>[snip]
>
> I'm not sure whether that is something we want. I myself have the same
> commit on multiple branches quite regularly, as I tend to queue up
> multiple dependent patch series. But I wouldn't ever want a history edit
> to affect all of these branches myself, I really only want it to modify
> the branch I'm currently on.
Let’s say with the current subcommands I want to
• Split a commit because it contains unrelated formatting fixups
• I want to fix typos in a commit message and also add a paragraph
Then let’s say that there are three branches that contain these commits.
I can’t think of a case where I intend to only make these changes for
one of the branches.
A default of update-all-branches-in-range seems like the best default.
I also agree with everything Elijah wrote here:
https://lore.kernel.org/git/CABPp-BFtx7-vLFbVqbHar=UZb1CGX5=ufMA4hrJRkSYuB14_Tw@mail.gmail.com/
I haven’t used Jujutsu yet. But apparently it is much less
branch-focused. But for us who use branches and sometimes need
“dependent branches” or “stacked branches” the default equivalent of
`--update-refs` seems like a win to me.
Okay, maybe let’s say that I am really making intentionally-divergent
histories and one commit needs to be reworded for that divergent
context. That could happen. But I can’t imageine where I would do that.
>[snip]
> Consequently I'm leaning more into the direction of doing nothing. It's
> not really clear to me that this is a bug, and we still can introduce a
> flag in the future that opts into the behaviour of rewriting relevant
> branches. That behaviour certainly can be useful, but I'd claim that
> it would be rather surprising to the user if that was the default.
Maybe it’s surprising with the current defaults of other commands. But
for a new-and-better (for some circumstances or all) commands defaulting
to updating all branches sounds great.
I have read or heard about the “mega merge” strategy in Jujutsu.[1]
Being able to (this is how I imagine it could work) make a temporary
integration branch where N branches can be edited by making edits to
them and having all the branches be updated sounds amazing.[2][3] I have
found myself doing temporary integration branches where I make fixes on
top and manually cherry-picking them to the correct target afterwards.
🔗 1: https://news.ycombinator.com/item?id=44650248
† 2: Maybe the commits are “edited” and then the integration merges are
re-done which sounds simple in the case of splitting commits and
rewording commit messages. Well, “simple”, that’s easy for me to
say from the peanut gallery. :) (I am aware that git-history(1)
does not support merges in this current iteration)
† 3: I am thinking of the “common case” of `-update-refs` but what
Elijah pointed out about “other interesting topologies” sounds even
better. :) In the link from above:
https://lore.kernel.org/git/CABPp-BFtx7-vLFbVqbHar=UZb1CGX5=ufMA4hrJRkSYuB14_Tw@mail.gmail.com/
^ permalink raw reply [flat|nested] 278+ messages in thread* Working on top of mega merges
2025-12-09 18:29 ` Kristoffer Haugsbakk
@ 2025-12-12 22:00 ` D. Ben Knoble
0 siblings, 0 replies; 278+ messages in thread
From: D. Ben Knoble @ 2025-12-12 22:00 UTC (permalink / raw)
To: Kristoffer Haugsbakk
Cc: Patrick Steinhardt, Elijah Newren, Junio C Hamano, Phillip Wood,
Git, Sergey Organov, Jean-Noël AVILA, Martin von Zweigbergk,
Karthik Nayak
On Tue, Dec 9, 2025 at 1:29 PM Kristoffer Haugsbakk
<kristofferhaugsbakk@fastmail.com> wrote:
>
> On Tue, Dec 2, 2025, at 19:49, Patrick Steinhardt wrote:
> >>[snip]
>
> I haven’t used Jujutsu yet.
Same, however:
> I have read or heard about the “mega merge” strategy in Jujutsu.[1]
> Being able to (this is how I imagine it could work) make a temporary
> integration branch where N branches can be edited by making edits to
> them and having all the branches be updated sounds amazing.[2][3] I have
> found myself doing temporary integration branches where I make fixes on
> top and manually cherry-picking them to the correct target afterwards.
I have done something like this once. Created a few independent
branches, wrangled an octopus merge together, and then worked on top
of that. When I wanted to commit, I did "commit --fixup" with the
appropriate branch/commit, then eventually "rebase --autosquash
--rebase-merges" or something. In that particular case, I started with
empty commits on each branch, I think.
You could probably shuffle commits (like to add a new one to a sub
branch) using "rebase --interactive…" there, too, but I don't think I
tried that.
Anyway, the workflow did its job, but I didn't end up with something I
think I'd try in practice very often, at least not yet. It did make me
wonder what from jj we'd need to make things smoother, though.
--
D. Ben Knoble
^ permalink raw reply [flat|nested] 278+ messages in thread
* Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing
2025-11-21 14:31 ` Phillip Wood
2025-11-21 16:01 ` Junio C Hamano
@ 2025-11-23 2:30 ` Elijah Newren
2025-11-24 16:31 ` Phillip Wood
1 sibling, 1 reply; 278+ messages in thread
From: Elijah Newren @ 2025-11-23 2:30 UTC (permalink / raw)
To: phillip.wood
Cc: Junio C Hamano, Patrick Steinhardt, git, D. Ben Knoble,
Sergey Organov, Jean-Noël AVILA, Martin von Zweigbergk,
Kristoffer Haugsbakk, Karthik Nayak
On Fri, Nov 21, 2025 at 6:31 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
>
[...]
> > So, you are referring to the single branch, HEAD-centric piece of the
> > feedback. The funny thing there is that operating on a more limited
> > case, without checking and verifying that you are indeed in the more
> > limited case (and erroring out if not), risks painting us into a
> > corner or providing some really buggy behavior when we aren't actually
> > in that case. To me, it opens a can of worms and makes the problem
> > scope bigger instead of smaller. Funnily enough, the single branch
> > thing is also the one piece of this that I think could be solved by a
> > fairly small change in the reroll (and I pointed out how in the
> > comments), so the limited view really didn't buy anything here IMO.
>
> I can't find that comment. Are you referring to reusing more of the
> replay machinery?
Yeah, what's needed is the equivalent of running "git replay --onto
${NEW_COMMIT_ID} --ancestry-path ^${OLD_COMMIT_ID} --branches", as
noted in more detail over at
https://lore.kernel.org/git/CABPp-BEm1QBP+CuSOn5FaE3XJVFg+Qbfzdp560u00ZERbNm6qQ@mail.gmail.com/
.
> If so we have the problem that the user gives a single
> commit to "git history" so we don't have a handy revision range to pass
> to the replay machinery unless we assume we're rewriting an ancestor of
> HEAD or we go and find all the branches descended from the commit the
> user gave us.
The range is included in the command above: "--ancestry-path
^${OLD_COMMIT_ID} --branches"
And because of this, we don't even really need to "find" all the
branches as a separate step, it's just part of the same revision walk
for rewriting commits.
Whereas if we do want to only handle a single branch as the current
implementation does, then we *need* to do an extra revision walk to
ensure that the commit is not also part of any other branch and error
out if it is, because disconnecting the histories would be very
counterintuitive in most cases. If users really do want to disconnect
histories of two branches sharing a commit, we should require the user
to provide some flag to explicitly specify such to signal that it is
okay for us to bypass such a check and just rewrite one branch. Such
a check is missing from the current code.
> Long term we should certainly do the latter but depending
> on how much work it is to implement that we may want to go with the
> single branch case at first
I showed the implementation of the latter, and it's actually (much)
less code than what's already in this series; see the
replay_descendants() function I posted at the same link above.
My replay-edit work used a just slightly modified form of that
function, because editing a commit and replaying all commits from all
branches that reached the OLD_COMMIT_ID, to now be replayed on top of
NEW_COMMIT_ID, is exactly what was needed there too. (If you're
curious about the modifications: I had an extra --brief-stats option
because I found it nice to provide some user feedback about what was
updated, and I pulled the "--branches" portion of the command from a
${GIT_DIR}/REPLAY_EDIT file, because that allowed me to give users the
opportunity to disconnect histories via some mechanism that would put
a single branch name in that file instead of "--branches".)
> > The other problems are independent of whether you try to limit the
> > scope initially in such a manner:
> >
> > Are the testcases and the code requiring something for the feature
> > (ensuring the index and worktree are preserved) doing something that
> > is incompatible with the capabilities given to the user (allowing them
> > to edit the patch while splitting, so that they stage stuff that
> > wasn't part of the original commit)? Or...is it assumed that the
> > split commits always "sum" to the changes in the original commit,
> > meaning the "other" patch immediately undoes those extra changes?
>
> Yes that's what's implemented. I think that makes sense for the "split"
> command. Often when splitting a commit one needs to make small changes
> to the diff in order for the result to compile but you still want the
> same end state from the sum of the split commits.
Makes sense; thanks for confirming. I just didn't realize this was
the case while reviewing the patches until my response above.
> > I'm also worried about extended header handling for the edited
> > (reworded or split) commits. That seems to have been overlooked in
> > this series, despite the fact that in early versions extended headers
> > were explicitly called out for the remainder of the commits being
> > replayed/rebased, so it seems interesting that they weren't considered
> > for the commits explicitly being edited.
>
> What headers does it make sense to copy when splitting a commit? When
> rewording it is more likely that copying the extended headers is what
> the user wants but the example of the "encoding" header you gave does
> not make sense to me as we re-encode the commit message and author data
> when the user edit's the message so we're not preserving the original
> encoding.
I agree that when rewording we probably want to copy most extended
headers, but you make a good point about encoding. For splitting, I
agree it's less clear, and I'm not sure I know the answer. But I
expected the topic to at least be discussed and mentioned in the
relevant commit messages. It appears to have been silently
overlooked, and I'm worried it's the kind of topic that doesn't come
up often, meaning that if we don't discuss now and just pick whatever
behavior we get from implementation side-effects, then people will
come back in a year or two and point out we got it buggy but it's too
late to change it.
> > And I'm a bit surprised that the original commit message for a split
> > commit is automatically associated with the second commit; if I had
> > been forced to choose, I would have assumed it should be associated
> > with the first.
>
> I don't think it is safe to assume either - we should prompt the user to
> edit the message when creating both commits and seed the editor with the
> original message.
That sounds like a better solution to me for that particular issue,
and probably wouldn't be hard to implement.
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing
2025-11-23 2:30 ` [PATCH v6 00/11] Introduce git-history(1) command for easy history editing Elijah Newren
@ 2025-11-24 16:31 ` Phillip Wood
2025-11-25 3:39 ` Elijah Newren
0 siblings, 1 reply; 278+ messages in thread
From: Phillip Wood @ 2025-11-24 16:31 UTC (permalink / raw)
To: Elijah Newren, phillip.wood
Cc: Junio C Hamano, Patrick Steinhardt, git, D. Ben Knoble,
Sergey Organov, Jean-Noël AVILA, Martin von Zweigbergk,
Kristoffer Haugsbakk, Karthik Nayak
On 23/11/2025 02:30, Elijah Newren wrote:
> On Fri, Nov 21, 2025 at 6:31 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
>>
> [...]
>>> So, you are referring to the single branch, HEAD-centric piece of the
>>> feedback. The funny thing there is that operating on a more limited
>>> case, without checking and verifying that you are indeed in the more
>>> limited case (and erroring out if not), risks painting us into a
>>> corner or providing some really buggy behavior when we aren't actually
>>> in that case. To me, it opens a can of worms and makes the problem
>>> scope bigger instead of smaller. Funnily enough, the single branch
>>> thing is also the one piece of this that I think could be solved by a
>>> fairly small change in the reroll (and I pointed out how in the
>>> comments), so the limited view really didn't buy anything here IMO.
>>
>> I can't find that comment. Are you referring to reusing more of the
>> replay machinery?
>
> Yeah, what's needed is the equivalent of running "git replay --onto
> ${NEW_COMMIT_ID} --ancestry-path ^${OLD_COMMIT_ID} --branches", as
> noted in more detail over at
> https://lore.kernel.org/git/CABPp-BEm1QBP+CuSOn5FaE3XJVFg+Qbfzdp560u00ZERbNm6qQ@mail.gmail.com/
Thanks, I'd somehow missed that when I read that message the first time
>> If so we have the problem that the user gives a single
>> commit to "git history" so we don't have a handy revision range to pass
>> to the replay machinery unless we assume we're rewriting an ancestor of
>> HEAD or we go and find all the branches descended from the commit the
>> user gave us.
>
> The range is included in the command above: "--ancestry-path
> ^${OLD_COMMIT_ID} --branches"
>
> And because of this, we don't even really need to "find" all the
> branches as a separate step, it's just part of the same revision walk
> for rewriting commits.
Oh, so --branches means we consider all the branches and --ancestry-path
excludes the those that are not descended from the commit we're
rewriting - nice. We'd need to be careful about modifying the commit at
the tip of a branch though as in that case we'd exclude the branch from
the set of commits with ^{OLD_COMMIT_ID} and so "git replay" would not
update that branch.
In general rewriting multiple branches can be confusing if those
branches are checked out elsewhere and the HEAD of that worktree
suddenly changes but for rewording and splitting commits as implemented
here the final tree is the same after the rewrite so it should be fine.
The other potential problem with rewriting multiple branches is that we
need to ensure two separate "git history" processes running at the same
time in two different worktrees don't try to update the same branch.
"git rebase --update-refs" has some logic to prevent that.
> Whereas if we do want to only handle a single branch as the current
> implementation does, then we *need* to do an extra revision walk to
> ensure that the commit is not also part of any other branch and error
> out if it is, because disconnecting the histories would be very
> counterintuitive in most cases.
Oh - I had not understood what the "extra work" you were talking about
before was - that makes it clear.
> If users really do want to disconnect
> histories of two branches sharing a commit, we should require the user
> to provide some flag to explicitly specify such to signal that it is
> okay for us to bypass such a check and just rewrite one branch. Such
> a check is missing from the current code.
>
>> Long term we should certainly do the latter but depending
>> on how much work it is to implement that we may want to go with the
>> single branch case at first
>
> I showed the implementation of the latter, and it's actually (much)
> less code than what's already in this series; see the
> replay_descendants() function I posted at the same link above.
>
> My replay-edit work used a just slightly modified form of that
> function, because editing a commit and replaying all commits from all
> branches that reached the OLD_COMMIT_ID, to now be replayed on top of
> NEW_COMMIT_ID, is exactly what was needed there too. (If you're
> curious about the modifications: I had an extra --brief-stats option
> because I found it nice to provide some user feedback about what was
> updated, and I pulled the "--branches" portion of the command from a
> ${GIT_DIR}/REPLAY_EDIT file, because that allowed me to give users the
> opportunity to disconnect histories via some mechanism that would put
> a single branch name in that file instead of "--branches".)
Interesting - I watched you're git merge talk about it recently and it
looked quite impressive.
>>> I'm also worried about extended header handling for the edited
>>> (reworded or split) commits. That seems to have been overlooked in
>>> this series, despite the fact that in early versions extended headers
>>> were explicitly called out for the remainder of the commits being
>>> replayed/rebased, so it seems interesting that they weren't considered
>>> for the commits explicitly being edited.
>>
>> What headers does it make sense to copy when splitting a commit? When
>> rewording it is more likely that copying the extended headers is what
>> the user wants but the example of the "encoding" header you gave does
>> not make sense to me as we re-encode the commit message and author data
>> when the user edit's the message so we're not preserving the original
>> encoding.
>
> I agree that when rewording we probably want to copy most extended
> headers, but you make a good point about encoding. For splitting, I
> agree it's less clear, and I'm not sure I know the answer. But I
> expected the topic to at least be discussed and mentioned in the
> relevant commit messages. It appears to have been silently
> overlooked, and I'm worried it's the kind of topic that doesn't come
> up often, meaning that if we don't discuss now and just pick whatever
> behavior we get from implementation side-effects, then people will
> come back in a year or two and point out we got it buggy but it's too
> late to change it.
It would certainly be worth adding a comment about commit headers in the
commit message.
Thanks
Phillip
>>> And I'm a bit surprised that the original commit message for a split
>>> commit is automatically associated with the second commit; if I had
>>> been forced to choose, I would have assumed it should be associated
>>> with the first.
>>
>> I don't think it is safe to assume either - we should prompt the user to
>> edit the message when creating both commits and seed the editor with the
>> original message.
>
> That sounds like a better solution to me for that particular issue,
> and probably wouldn't be hard to implement.
^ permalink raw reply [flat|nested] 278+ messages in thread* Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing
2025-11-24 16:31 ` Phillip Wood
@ 2025-11-25 3:39 ` Elijah Newren
0 siblings, 0 replies; 278+ messages in thread
From: Elijah Newren @ 2025-11-25 3:39 UTC (permalink / raw)
To: phillip.wood
Cc: Junio C Hamano, Patrick Steinhardt, git, D. Ben Knoble,
Sergey Organov, Jean-Noël AVILA, Martin von Zweigbergk,
Kristoffer Haugsbakk, Karthik Nayak
On Mon, Nov 24, 2025 at 8:31 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
>
> On 23/11/2025 02:30, Elijah Newren wrote:
> > On Fri, Nov 21, 2025 at 6:31 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
> >>
[...]
> > The range is included in the command above: "--ancestry-path
> > ^${OLD_COMMIT_ID} --branches"
> >
> > And because of this, we don't even really need to "find" all the
> > branches as a separate step, it's just part of the same revision walk
> > for rewriting commits.
>
> Oh, so --branches means we consider all the branches and --ancestry-path
> excludes the those that are not descended from the commit we're
> rewriting - nice. We'd need to be careful about modifying the commit at
> the tip of a branch though as in that case we'd exclude the branch from
> the set of commits with ^{OLD_COMMIT_ID} and so "git replay" would not
> update that branch.
Ah, indeed, that's a good callout.
[...]
> > My replay-edit work used a just slightly modified form of that
> > function,
[...]
>
> Interesting - I watched you're git merge talk about it recently and it
> looked quite impressive.
Thanks. :-)
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH v7 00/12] Introduce git-history(1) command for easy history editing
2025-08-19 10:55 [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing Patrick Steinhardt
` (19 preceding siblings ...)
2025-10-27 11:33 ` [PATCH v6 00/11] " Patrick Steinhardt
@ 2025-12-03 10:48 ` Patrick Steinhardt
2025-12-03 10:48 ` [PATCH v7 01/12] wt-status: provide function to expose status for trees Patrick Steinhardt
` (11 more replies)
20 siblings, 12 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-12-03 10:48 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
Hi,
over recent months I've been playing around with Jujutsu quite
frequently. While I still prefer using Git, there's been a couple
features in it that I really like and that I'd like to have in Git, as
well.
A copule of these features relate to history editing. Most importantly,
I really dig the following commands:
- jj-abandon(1) to drop a specific commit from your history.
- jj-absorb(1) to take some changes and automatically apply them to
commits in your history that last modified the respective hunks.
- jj-split(1) to split a commit into two.
- jj-new(1) to insert a new commit after or before a specific other
commit.
Not all of these commands can be ported directly into Git. jj-new(1) for
example doesn't really make a ton of sense for us, I'd claim. But some
of these commands _do_ make sense.
This patch series is a starting point for such a command. I've
significantly slimmed it down from the first couple revisions now
following the discussions at the Contributor's Summit yesterday. This
was my intent anyway, as I already mentioned on the last iteration.
Changes in v7:
- I've rebased the patch series on top of f0ef5b6d9b (The fifth batch,
2025-11-30) with pw/replay-exclude-gpgsig-fix at 9f3a115087 (replay:
do not copy "gpgsign-sha256" header, 2025-11-26) merged into it to
fix some conflicts.
- I refactored a bunch of code to be shared between split and reword.
- I am now using "--ancestry-path" and "--reverse" to enumerate
commits.
- `git history split` now asks for commit messages for both commits.
- `git history split` no longer allows the user to edit hunks so that
conflicts become impossible. This may be reintroduced at a later
point in time.
- A bunch of test improvements.
- Several commit message and documentation improvements.
- Link to v6: https://lore.kernel.org/r/20251027-b4-pks-history-builtin-v6-0-407dd3f57ad3@pks.im
Changes in v6:
- I've rebased the patch series again to pull in the latest updates
from sa/replay-atomic-ref-updates and fix conflicts. It is now based
on 4e98b730f1 (The twenty-fourth batch, 2025-10-24) with ab661bb1bb
(replay: add replay.refAction config option, 2025-10-23) merged into
it.
- I've dropped the "-m" options for now, so commit messages are always
asked for via the editor. These can be introduced in a subsequent
patch series once discussion around them has settled.
- We don't use the merge machinery anymore to pick the commits.
- Drop the commit to parse commits in the replay machinery. It didn't
seem to be necessary in v5 anymore, and now that we don't use the
merge machinery at all we don't ever take that code path in the
first place.
- Link to v5:
https://lore.kernel.org/r/20251021-b4-pks-history-builtin-v5-0-78d23f578fe6@pks.im
Changes in v5:
- I've changed the patch series to be based on top of 133d151831 (The
twenty-first batch, 2025-10-20) with sa/replay-atomic-ref-updates at
a1c22e627e (SQAUASH??? t0450 band-aid, 2025-10-14) merged into it.
This is one the one hand to fix a conflict, but also to get some of
the CI updates to make GitLab CI work again.
- Some slight commit message improvements.
- Deduplicate subcommand usage strings by using defines.
- Fix the desendancy checks to properly verify that HEAD is a
descendant of the commit to be rewritten. Also add some tests for
this.
- Fix the hint that mentions that lines starting with the comment
character will be tripped after having written the commit message.
- Move an include to the correct commit.
- Link to v4: https://lore.kernel.org/r/20251001-b4-pks-history-builtin-v4-0-8e61ddb86317@pks.im
Changes in v4:
- I've rebuilt the patch series. It is now based on 821f583da6 (The
thirteenth batcn, 2025-09-29) with sa/replay-atomic-ref-updates
at 665c66a743 (replay: make atomic ref updates the default behavior,
2025-09-27) merged into it. This should fix all conflicts with seen.
- I've reworked this patch series to use the same infra as
git-replay(1), as discussed during the Contributor's Summit.
- I've slimmed down the patch series to only tackle those commands
that cannot result in a conflict to keep it simple. I also learned
that Elijah has been working on a "git replay edit" command, so I
dropped that command so that we can instead use his version.
- During the Contributor's Summit we have agreed that for now, we
won't care about hook execution just yet. This may be backfilled at
a later point in time.
- I dropped "commit.verbose" handling for now, as my understanding of
it was wrong at first. This is something we should backfill.
- Link to v3: https://lore.kernel.org/r/20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im
Changes in v3:
- Add logic to drive the "post-rewrite" hook and add tests to verify
that all hooks are executed as expected.
- Deduplicate logic to turn a replay action into a todo command.
- Move the addition of tests for the top-level git-history(1) command
to the correct commit.
- Some smaller commit message fixes.
- Honor "commit.verbose".
- Fix copy-paste error with an error message.
- Link to v2: https://lore.kernel.org/r/20250824-b4-pks-history-builtin-v2-0-964ac12f65bd@pks.im
Changes in v2:
- Add a new "reword" subcommand.
- List git-history(1) in "command-list.txt".
- Add some missing error handling.
- Simplify calling convention of `apply_commits()` to handle root
commits internally instead of requiring every caller to do so.
- Add tests to verify that git-history(1) refuses to work with changes
in the worktree or index.
- Mark git-history(1) as experimental.
- Introduce commands to manage interrupted history edits.
- A bunch of improvements to the manpage.
- Link to v1: https://lore.kernel.org/r/20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im
Thanks!
Patrick
---
Patrick Steinhardt (12):
wt-status: provide function to expose status for trees
replay: extract logic to pick commits
replay: stop using `the_repository`
builtin: add new "history" command
builtin/history: implement "reword" subcommand
add-patch: split out header from "add-interactive.h"
add-patch: split out `struct interactive_options`
add-patch: remove dependency on "add-interactive" subsystem
add-patch: add support for in-memory index patching
add-patch: allow disabling editing of hunks
cache-tree: allow writing in-memory index as tree
builtin/history: implement "split" subcommand
.gitignore | 1 +
Documentation/git-history.adoc | 109 ++++++++
Documentation/meson.build | 1 +
Makefile | 2 +
add-interactive.c | 174 +++----------
add-interactive.h | 46 +---
add-patch.c | 335 +++++++++++++++++++++----
add-patch.h | 71 ++++++
builtin.h | 1 +
builtin/add.c | 22 +-
builtin/checkout.c | 9 +-
builtin/commit.c | 16 +-
builtin/history.c | 558 +++++++++++++++++++++++++++++++++++++++++
builtin/replay.c | 110 +-------
builtin/reset.c | 16 +-
builtin/stash.c | 46 ++--
cache-tree.c | 4 +-
cache-tree.h | 3 +-
command-list.txt | 1 +
commit.h | 2 +-
git.c | 1 +
meson.build | 2 +
replay.c | 115 +++++++++
replay.h | 23 ++
t/meson.build | 3 +
t/t3450-history.sh | 17 ++
t/t3451-history-reword.sh | 236 +++++++++++++++++
t/t3452-history-split.sh | 452 +++++++++++++++++++++++++++++++++
wt-status.c | 24 ++
wt-status.h | 9 +
30 files changed, 2014 insertions(+), 395 deletions(-)
Range-diff versus v6:
1: d59b2ff389 = 1: ccf19f9067 wt-status: provide function to expose status for trees
2: 7b80b4d482 ! 2: 39c5a05ee1 replay: extract logic to pick commits
@@ Commit message
Signed-off-by: Patrick Steinhardt <ps@pks.im>
## Makefile ##
-@@ Makefile: LIB_OBJS += reftable/tree.o
- LIB_OBJS += reftable/writer.o
- LIB_OBJS += remote.o
+@@ Makefile: LIB_OBJS += repack-geometry.o
+ LIB_OBJS += repack-midx.o
+ LIB_OBJS += repack-promisor.o
LIB_OBJS += replace-object.o
+LIB_OBJS += replay.o
LIB_OBJS += repo-settings.o
@@ builtin/replay.c
#include "strmap.h"
#include <oidset.h>
@@ builtin/replay.c: enum ref_action_mode {
- REF_ACTION_PRINT
+ REF_ACTION_PRINT,
};
-static const char *short_commit_name(struct repository *repo,
@@ builtin/replay.c: static struct commit *peel_committish(struct repository *repo,
- const char *message = repo_logmsg_reencode(repo, based_on,
- NULL, out_enc);
- const char *orig_message = NULL;
-- const char *exclude_gpgsig[] = { "gpgsig", NULL };
+- const char *exclude_gpgsig[] = { "gpgsig", "gpgsig-sha256", NULL };
-
- commit_list_insert(parent, &parents);
- extra = read_commit_extra_headers(based_on, exclude_gpgsig);
@@ builtin/replay.c: static void determine_replay_mode(struct repository *repo,
- return create_commit(repo, result->tree, pickme, replayed_base);
-}
-
- static int handle_ref_update(enum ref_action_mode mode,
- struct ref_transaction *transaction,
- const char *refname,
+ static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)
+ {
+ if (!ref_action || !strcmp(ref_action, "update"))
@@ builtin/replay.c: int cmd_replay(int argc,
if (commit->parents->next)
die(_("replaying merge commits is not supported yet!"));
@@ builtin/replay.c: int cmd_replay(int argc,
## meson.build ##
@@ meson.build: libgit_sources = [
- 'reftable/writer.c',
- 'remote.c',
+ 'repack-midx.c',
+ 'repack-promisor.c',
'replace-object.c',
+ 'replay.c',
'repo-settings.c',
@@ replay.c (new)
+ const char *message = repo_logmsg_reencode(repo, based_on,
+ NULL, out_enc);
+ const char *orig_message = NULL;
-+ const char *exclude_gpgsig[] = { "gpgsig", NULL };
++ const char *exclude_gpgsig[] = { "gpgsig", "gpgsig-sha256", NULL };
+
+ commit_list_insert(parent, &parents);
+ extra = read_commit_extra_headers(based_on, exclude_gpgsig);
3: 3f66e8423d = 3: fe73ba4059 replay: stop using `the_repository`
4: f64dba0b08 ! 4: c162f407db builtin: add new "history" command
@@ Metadata
## Commit message ##
builtin: add new "history" command
- When rewriting history via git-rebase(1) there are a couple of very
- common use cases:
+ When rewriting history via git-rebase(1) there are a few very common use
+ cases:
- The ordering of two commits should be reversed.
@@ Commit message
- Multiple commits should be squashed into one.
+ - Editing an existing commit that is not the tip of the current
+ branch.
+
While these operations are all doable, it often feels needlessly kludgey
to do so by doing an interactive rebase, using the editor to say what
one wants, and then perform the actions. Furthermore, some operations
@@ Commit message
Add a new "history" command to plug this gap. This command will have
several different subcommands to imperatively rewrite history for common
- use cases like the above. These subcommands will be implemented in
- subsequent commits.
+ use cases like the above. Some of these subcommands will be implemented
+ in subsequent commits.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
@@ Documentation/git-history.adoc (new)
+
+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
+
-+This command is similar to linkgit:git-rebase[1] and uses the same
-+underlying machinery. You should use rebases if you want to reapply a range of
-+commits onto a different base, or interactive rebases if you want to edit a
++This command is related to linkgit:git-rebase[1] in that both commands can be
++used to rewrite history. You should use rebases if you want to reapply a range
++of commits onto a different base, or interactive rebases if you want to edit a
+range of commits.
+
+Note that this command does not (yet) work with histories that contain
@@ Documentation/git-history.adoc (new)
+
+include::includes/cmd-config-section-all.adoc[]
+
-+include::config/sequencer.adoc[]
-+
+GIT
+---
+Part of the linkgit:git[1] suite
5: 3c1c599e9f ! 5: 3ecbe6ec83 builtin/history: implement "reword" subcommand
@@ Commit message
builtin/history: implement "reword" subcommand
Implement a new "reword" subcommand for git-history(1). This subcommand
- is essentially the same as if a user performed an interactive rebase
- with a single commit changed to use the "reword" verb.
+ is similar to the user performing an interactive rebase with a single
+ commit changed to use the "reword" instruction.
+
+ The major difference is that we do not check out the commit that is to
+ be reworded. This has the obvious benefit of being significantly faster
+ compared to git-rebase(1), but even more importantly it allows the user
+ to rewrite history even if there are local changes in the working tree
+ or in the index.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
@@ builtin/history.c
+ int ret;
+
+ repo_init_revisions(repo, &rev, NULL);
++ rev.reverse = 1;
+ strvec_push(&revisions, "");
+ strvec_push(&revisions, oid_to_hex(&new_commit->object.oid));
-+ if (old_commit)
++ if (old_commit) {
+ strvec_pushf(&revisions, "^%s", oid_to_hex(&old_commit->object.oid));
++ strvec_pushf(&revisions, "--ancestry-path=%s", oid_to_hex(&old_commit->object.oid));
++ }
+
+ setup_revisions_from_strvec(&revisions, &rev, &revision_opts);
+ if (revisions.nr != 1 || prepare_revision_walk(&rev)) {
@@ builtin/history.c
+ }
+
+ strvec_push(out, oid_to_hex(&child->object.oid));
-+
-+ if (child->parents && old_commit &&
-+ commit_list_contains(old_commit, child->parents))
-+ break;
+ }
+
-+ /*
-+ * Revisions are in newest-order-first. We have to reverse the
-+ * array though so that we pick the oldest commits first.
-+ */
-+ for (size_t i = 0, j = out->nr - 1; i < j; i++, j--)
-+ SWAP(out->v[i], out->v[j]);
-+
+ ret = 0;
+
+out:
@@ builtin/history.c
+ return ret;
+}
+
++static int gather_commits_between_head_and_revision(struct repository *repo,
++ const char *revision,
++ struct commit **original_commit,
++ struct commit **parent_commit,
++ struct commit **head,
++ struct strvec *commits)
++{
++ struct commit_list *from_list = NULL;
++ int ret;
++
++ *original_commit = lookup_commit_reference_by_name(revision);
++ if (!*original_commit) {
++ ret = error(_("commit cannot be found: %s"), revision);
++ goto out;
++ }
++
++ *parent_commit = (*original_commit)->parents ? (*original_commit)->parents->item : NULL;
++ if (*parent_commit && repo_parse_commit(repo, *parent_commit)) {
++ ret = error(_("unable to parse commit %s"),
++ oid_to_hex(&(*parent_commit)->object.oid));
++ goto out;
++ }
++
++ *head = lookup_commit_reference_by_name("HEAD");
++ if (!(*head)) {
++ ret = error(_("could not resolve HEAD to a commit"));
++ goto out;
++ }
++
++ commit_list_append(*original_commit, &from_list);
++ if (!repo_is_descendant_of(repo, *head, from_list)) {
++ ret = error(_("commit must be reachable from current HEAD commit"));
++ goto out;
++ }
++
++ /*
++ * Collect the list of commits that we'll have to reapply now already.
++ * This ensures that we'll abort early on in case the range of commits
++ * contains merges, which we do not yet handle.
++ */
++ ret = collect_commits(repo, *parent_commit, *head, commits);
++ if (ret < 0)
++ goto out;
++
++out:
++ free_commit_list(from_list);
++ return ret;
++}
++
+static void replace_commits(struct strvec *commits,
+ const struct object_id *commit_to_replace,
+ const struct object_id *replacements,
@@ builtin/history.c
+ } else {
+ struct tree *tree = repo_get_commit_tree(repo, commit);
+ onto = replay_create_commit(repo, tree, commit, onto);
-+ if (!onto)
-+ break;
++ if (!onto) {
++ ret = -1;
++ goto out;
++ }
+ }
+ }
+
@@ builtin/history.c
+ const char *path = git_path_commit_editmsg();
+ const char *hint =
+ _("Please enter the commit message for the %s changes."
-+ " Lines starting\nwith '%s' will be ignored.\n");
++ " Lines starting\nwith '%s' will be ignored, and an"
++ " empty message aborts the commit.\n");
+ struct wt_status s;
+
+ strbuf_addstr(out, default_message);
@@ builtin/history.c
+
+ strbuf_reset(out);
+ if (launch_editor(path, out, NULL)) {
-+ fprintf(stderr, _("Please supply the message using the -m option.\n"));
++ fprintf(stderr, _("Aborting commit as launching the editor failed.\n"));
+ return -1;
+ }
+ strbuf_stripspace(out, comment_line_str);
@@ builtin/history.c
+ return 0;
+}
+
++static int commit_tree_with_edited_message(struct repository *repo,
++ const char *action,
++ struct commit *original_commit,
++ const struct object_id *new_tree_oid,
++ const struct commit_list *parents,
++ const struct object_id *parent_tree_oid,
++ struct object_id *out)
++{
++ const char *exclude_gpgsig[] = { "gpgsig", "gpgsig-sha256", NULL };
++ const char *original_message, *original_body, *ptr;
++ struct commit_extra_header *original_extra_headers = NULL;
++ struct strbuf commit_message = STRBUF_INIT;
++ char *original_author = NULL;
++ size_t len;
++ int ret;
++
++ /* We retain authorship of the original commit. */
++ original_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);
++ ptr = find_commit_header(original_message, "author", &len);
++ if (ptr)
++ original_author = xmemdupz(ptr, len);
++ find_commit_subject(original_message, &original_body);
++
++ ret = fill_commit_message(repo, parent_tree_oid, new_tree_oid,
++ original_body, action, &commit_message);
++ if (ret < 0)
++ goto out;
++
++ original_extra_headers = read_commit_extra_headers(original_commit, exclude_gpgsig);
++
++ ret = commit_tree_extended(commit_message.buf, commit_message.len, new_tree_oid,
++ parents, out, original_author, NULL, NULL,
++ original_extra_headers);
++ if (ret < 0)
++ goto out;
++
++out:
++ free_commit_extra_headers(original_extra_headers);
++ strbuf_release(&commit_message);
++ free(original_author);
++ return ret;
++}
++
+static int cmd_history_reword(int argc,
+ const char **argv,
+ const char *prefix,
@@ builtin/history.c
+ struct option options[] = {
+ OPT_END(),
+ };
-+ struct strbuf final_message = STRBUF_INIT;
+ struct commit *original_commit, *parent, *head;
+ struct strvec commits = STRVEC_INIT;
+ struct object_id parent_tree_oid, original_commit_tree_oid;
+ struct object_id rewritten_commit;
-+ struct commit_list *from_list = NULL;
-+ const char *original_message, *original_body, *ptr;
-+ char *original_author = NULL;
-+ size_t len;
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
@@ builtin/history.c
+ }
+ repo_config(repo, git_default_config, NULL);
+
-+ original_commit = lookup_commit_reference_by_name(argv[0]);
-+ if (!original_commit) {
-+ ret = error(_("commit to be reworded cannot be found: %s"), argv[0]);
++ ret = gather_commits_between_head_and_revision(repo, argv[0], &original_commit,
++ &parent, &head, &commits);
++ if (ret < 0)
+ goto out;
-+ }
-+ original_commit_tree_oid = repo_get_commit_tree(repo, original_commit)->object.oid;
+
-+ parent = original_commit->parents ? original_commit->parents->item : NULL;
-+ if (parent) {
-+ if (repo_parse_commit(repo, parent)) {
-+ ret = error(_("unable to parse commit %s"),
-+ oid_to_hex(&parent->object.oid));
-+ goto out;
-+ }
++ original_commit_tree_oid = repo_get_commit_tree(repo, original_commit)->object.oid;
++ if (parent)
+ parent_tree_oid = repo_get_commit_tree(repo, parent)->object.oid;
-+ } else {
++ else
+ oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
-+ }
-+
-+ head = lookup_commit_reference_by_name("HEAD");
-+ if (!head) {
-+ ret = error(_("could not resolve HEAD to a commit"));
-+ goto out;
-+ }
-+
-+ commit_list_append(original_commit, &from_list);
-+ if (!repo_is_descendant_of(repo, head, from_list)) {
-+ ret = error (_("split commit must be reachable from current HEAD commit"));
-+ goto out;
-+ }
-+
-+ /*
-+ * Collect the list of commits that we'll have to reapply now already.
-+ * This ensures that we'll abort early on in case the range of commits
-+ * contains merges, which we do not yet handle.
-+ */
-+ ret = collect_commits(repo, parent, head, &commits);
-+ if (ret < 0)
-+ goto out;
+
+ /* We retain authorship of the original commit. */
-+ original_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);
-+ ptr = find_commit_header(original_message, "author", &len);
-+ if (ptr)
-+ original_author = xmemdupz(ptr, len);
-+ find_commit_subject(original_message, &original_body);
-+
-+ ret = fill_commit_message(repo, &parent_tree_oid, &original_commit_tree_oid,
-+ original_body, "reworded", &final_message);
-+ if (ret < 0)
-+ goto out;
-+
-+ ret = commit_tree(final_message.buf, final_message.len, &original_commit_tree_oid,
-+ original_commit->parents, &rewritten_commit, original_author, NULL);
++ ret = commit_tree_with_edited_message(repo, "reworded", original_commit,
++ &original_commit_tree_oid,
++ original_commit->parents, &parent_tree_oid,
++ &rewritten_commit);
+ if (ret < 0) {
+ ret = error(_("failed writing reworded commit"));
+ goto out;
@@ builtin/history.c
+ ret = 0;
+
+out:
-+ strbuf_release(&final_message);
-+ free_commit_list(from_list);
+ strvec_clear(&commits);
-+ free(original_author);
+ return ret;
+}
@@ t/t3451-history-reword.sh (new)
+
+reword_with_message () {
+ cat >message &&
-+ write_script fake-editor.sh <<-EOF &&
-+ cp "$(pwd)/message" "\$1"
++ write_script fake-editor.sh <<-\EOF &&
++ cp message "$1"
+ EOF
+ test_set_editor "$(pwd)"/fake-editor.sh &&
+ git history reword "$@" &&
@@ t/t3451-history-reword.sh (new)
+ git switch branch &&
+ test_commit theirs &&
+ test_must_fail git history reword ours 2>err &&
-+ test_grep "split commit must be reachable from current HEAD commit" err
++ test_grep "commit must be reachable from current HEAD commit" err
+ )
+'
+
@@ t/t3451-history-reword.sh (new)
+ first
+ EOF
+ git log --format=%s >actual &&
-+ test_cmp expect actual
++ test_cmp expect actual &&
++
++ git reflog >reflog &&
++ test_grep "reword: updating HEAD" reflog
+ )
+'
+
@@ t/t3451-history-reword.sh (new)
+ )
+'
+
-+test_expect_success 'can use editor to rewrite commit message' '
++test_expect_success 'editor shows proper status' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
@@ t/t3451-history-reword.sh (new)
+ first
+
+ # Please enter the commit message for the reworded changes. Lines starting
-+ # with ${SQ}#${SQ} will be ignored.
++ # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
+ # Changes to be committed:
+ # new file: first.t
+ #
+ EOF
+ test_cmp expect COMMIT_EDITMSG &&
+
-+ cat >expect <<-EOF &&
++ test_commit_message HEAD <<-\EOF
+ first
+
+ amend a comment
-+
+ EOF
-+ git log --format=%B >actual &&
-+ test_cmp expect actual
+ )
+'
+
@@ t/t3451-history-reword.sh (new)
+ test_commit second &&
+ test_commit third &&
+
-+ write_script .git/hooks/prepare-commit-msg <<-EOF &&
-+ touch "$(pwd)/hooks.log
-+ EOF
-+ write_script .git/hooks/post-commit <<-EOF &&
-+ touch "$(pwd)/hooks.log
-+ EOF
-+ write_script .git/hooks/post-rewrite <<-EOF &&
-+ touch "$(pwd)/hooks.log
-+ EOF
++ ORIG_PATH="$(pwd)" &&
++ export ORIG_PATH &&
++ for hook in prepare-commit-msg pre-commit post-commit post-rewrite commit-msg
++ do
++ write_script .git/hooks/$hook <<-\EOF || exit 1
++ touch "$ORIG_PATH/hooks.log
++ EOF
++ done &&
+
+ reword_with_message HEAD~ <<-EOF &&
+ second reworded
6: 7d736c2f8d = 6: a1c6ad546d add-patch: split out header from "add-interactive.h"
7: da50162d06 ! 7: eea2ac7a18 add-patch: split out `struct interactive_options`
@@ add-patch.c: static int patch_update_file(struct add_p_state *s,
+ if (*s->s.cfg.reset_color_interactive)
+ fputs(s->s.cfg.reset_color_interactive, stdout);
fflush(stdout);
- if (read_single_character(s) == EOF)
- break;
+ if (read_single_character(s) == EOF) {
+ quit = 1;
@@ add-patch.c: static int patch_update_file(struct add_p_state *s,
err(s, _("Sorry, cannot split this hunk"));
} else if (!split_hunk(s, file_diff,
8: a888ae8a29 ! 8: 95be396a5a add-patch: remove dependency on "add-interactive" subsystem
@@ add-patch.c: static int patch_update_file(struct add_p_state *s,
+ if (*s->cfg.reset_color_interactive)
+ fputs(s->cfg.reset_color_interactive, stdout);
fflush(stdout);
- if (read_single_character(s) == EOF)
- break;
+ if (read_single_character(s) == EOF) {
+ quit = 1;
@@ add-patch.c: static int patch_update_file(struct add_p_state *s,
err(s, _("Sorry, cannot split this hunk"));
} else if (!split_hunk(s, file_diff,
9: e84e2e265d ! 9: d114c622a8 add-patch: add support for in-memory index patching
@@ add-patch.c: static int patch_update_file(struct add_p_state *s,
}
putchar('\n');
-@@ add-patch.c: int run_add_p(struct repository *r, enum add_p_mode mode,
+ return quit;
+ }
+
++static int run_add_p_common(struct add_p_state *state,
++ const struct pathspec *ps)
++{
++ size_t binary_count = 0;
++
++ if (parse_diff(state, ps) < 0)
++ return -1;
++
++ for (size_t i = 0; i < state->file_diff_nr; i++) {
++ if (state->file_diff[i].binary && !state->file_diff[i].hunk_nr)
++ binary_count++;
++ else if (patch_update_file(state, state->file_diff + i))
++ break;
++ }
++
++ if (state->file_diff_nr == 0)
++ err(state, _("No changes."));
++ else if (binary_count == state->file_diff_nr)
++ err(state, _("Only binary files changed."));
++
++ return 0;
++}
++
+ int run_add_p(struct repository *r, enum add_p_mode mode,
+ struct interactive_options *opts, const char *revision,
+ const struct pathspec *ps)
{
struct add_p_state s = {
.r = r,
@@ add-patch.c: int run_add_p(struct repository *r, enum add_p_mode mode,
.answer = STRBUF_INIT,
.buf = STRBUF_INIT,
.plain = STRBUF_INIT,
+ .colored = STRBUF_INIT,
+ };
+- size_t i, binary_count = 0;
++ int ret;
+
+ interactive_config_init(&s.cfg, r, opts);
+
@@ add-patch.c: int run_add_p(struct repository *r, enum add_p_mode mode,
+ if (repo_read_index(r) < 0 ||
+ (!s.mode->index_only &&
+ repo_refresh_and_write_index(r, REFRESH_QUIET, 0, 1,
+- NULL, NULL, NULL) < 0) ||
+- parse_diff(&s, ps) < 0) {
+- add_p_state_clear(&s);
+- return -1;
++ NULL, NULL, NULL) < 0)) {
++ ret = -1;
++ goto out;
+ }
+
+- for (i = 0; i < s.file_diff_nr; i++)
+- if (s.file_diff[i].binary && !s.file_diff[i].hunk_nr)
+- binary_count++;
+- else if (patch_update_file(&s, s.file_diff + i))
+- break;
++ ret = run_add_p_common(&s, ps);
++ if (ret < 0)
++ goto out;
+
+- if (s.file_diff_nr == 0)
+- err(&s, _("No changes."));
+- else if (binary_count == s.file_diff_nr)
+- err(&s, _("Only binary files changed."));
++ ret = 0;
+
++out:
add_p_state_clear(&s);
- return 0;
- }
+- return 0;
++ return ret;
++}
+
+int run_add_p_index(struct repository *r,
+ struct index_state *index,
@@ add-patch.c: int run_add_p(struct repository *r, enum add_p_mode mode,
+ };
+ struct strbuf parent_revision = STRBUF_INIT;
+ char parent_tree_oid[GIT_MAX_HEXSZ + 1];
-+ size_t binary_count = 0;
+ struct commit *commit;
+ int ret;
+
@@ add-patch.c: int run_add_p(struct repository *r, enum add_p_mode mode,
+
+ interactive_config_init(&s.cfg, r, opts);
+
-+ if (parse_diff(&s, ps) < 0) {
-+ ret = -1;
++ ret = run_add_p_common(&s, ps);
++ if (ret < 0)
+ goto out;
-+ }
-+
-+ for (size_t i = 0; i < s.file_diff_nr; i++) {
-+ if (s.file_diff[i].binary && !s.file_diff[i].hunk_nr)
-+ binary_count++;
-+ else if (patch_update_file(&s, s.file_diff + i))
-+ break;
-+ }
-+
-+ if (s.file_diff_nr == 0) {
-+ err(&s, _("No changes."));
-+ ret = -1;
-+ goto out;
-+ }
-+
-+ if (binary_count == s.file_diff_nr) {
-+ err(&s, _("Only binary files changed."));
-+ ret = -1;
-+ goto out;
-+ }
+
+ ret = 0;
+
@@ add-patch.c: int run_add_p(struct repository *r, enum add_p_mode mode,
+ strbuf_release(&parent_revision);
+ add_p_state_clear(&s);
+ return ret;
-+}
+ }
## add-patch.h ##
@@
-: ---------- > 10: 6f8c443002 add-patch: allow disabling editing of hunks
10: 2a43a306de ! 11: f5f4ab21bb cache-tree: allow writing in-memory index as tree
@@ cache-tree.c: static int write_index_as_tree_internal(struct object_id *oid,
was_valid = index_state->cache_tree &&
cache_tree_fully_valid(index_state->cache_tree);
-@@ cache-tree.c: struct tree* write_in_core_index_as_tree(struct repository *repo) {
- return lookup_tree(repo, &index_state->cache_tree->oid);
- }
-
--
- int write_index_as_tree(struct object_id *oid, struct index_state *index_state, const char *index_path, int flags, const char *prefix)
- {
- int entries, was_valid;
## cache-tree.h ##
@@ cache-tree.h: int cache_tree_verify(struct repository *, struct index_state *);
11: bbb837e8d6 ! 12: 0d294bf943 builtin/history: implement "split" subcommand
@@ Documentation/git-history.adoc: Several commands are available to rewrite histor
+ stays intact, except that its parent will be the newly split-out
+ commit.
++
-+The commit message of the new commit will be asked for by launching the
-+configured editor. Authorship of the commit will be the same as for the
++The commit messages of the split-up commits will be asked for by launching
++the configured editor. Authorship of the commit will be the same as for the
+original commit.
++
+If passed, _<pathspec>_ can be used to limit which changes shall be split out
@@ Documentation/git-history.adoc: Several commands are available to rewrite histor
CONFIGURATION
-------------
-@@ Documentation/git-history.adoc: include::includes/cmd-config-section-all.adoc[]
-
- include::config/sequencer.adoc[]
+ include::includes/cmd-config-section-all.adoc[]
+EXAMPLES
+--------
@@ builtin/history.c: static int cmd_history_reword(int argc,
+ struct object_id *out)
+{
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
-+ struct strbuf index_file = STRBUF_INIT, split_message = STRBUF_INIT;
++ struct strbuf index_file = STRBUF_INIT;
+ struct child_process read_tree_cmd = CHILD_PROCESS_INIT;
+ struct index_state index = INDEX_STATE_INIT(repo);
+ struct object_id original_commit_tree_oid, parent_tree_oid;
-+ const char *original_message, *original_body, *ptr;
+ char original_commit_oid[GIT_MAX_HEXSZ + 1];
-+ char *original_author = NULL;
+ struct commit_list *parents = NULL;
+ struct commit *first_commit;
+ struct tree *split_tree;
-+ size_t len;
+ int ret;
+
+ if (original_commit->parents)
@@ builtin/history.c: static int cmd_history_reword(int argc,
+
+ oid_to_hex_r(original_commit_oid, &original_commit->object.oid);
+ ret = run_add_p_index(repo, &index, index_file.buf, &interactive_opts,
-+ original_commit_oid, pathspec);
++ original_commit_oid, pathspec, ADD_P_DISALLOW_EDIT);
+ if (ret < 0)
+ goto out;
+
@@ builtin/history.c: static int cmd_history_reword(int argc,
+ goto out;
+ }
+
-+ /* We retain authorship of the original commit. */
-+ original_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);
-+ ptr = find_commit_header(original_message, "author", &len);
-+ if (ptr)
-+ original_author = xmemdupz(ptr, len);
-+
-+ ret = fill_commit_message(repo, &parent_tree_oid, &split_tree->object.oid,
-+ "", "split-out", &split_message);
-+ if (ret < 0)
-+ goto out;
-+
-+ ret = commit_tree(split_message.buf, split_message.len, &split_tree->object.oid,
-+ original_commit->parents, &out[0], original_author, NULL);
++ /*
++ * The first commit is constructed from the split-out tree. The base
++ * that shall be diffed against is the parent of the original commit.
++ */
++ ret = commit_tree_with_edited_message(repo, "split-out", original_commit,
++ &split_tree->object.oid,
++ original_commit->parents, &parent_tree_oid, &out[0]);
+ if (ret < 0) {
+ ret = error(_("failed writing split-out commit"));
+ goto out;
+ }
+
+ /*
-+ * The second commit is much simpler to construct, as we can simply use
-+ * the original commit details, except that we adjust its parent to be
-+ * the newly split-out commit.
++ * The second commit is constructed from the original tree. The base to
++ * diff against and the parent in this case is the first split-out
++ * commit.
+ */
-+ find_commit_subject(original_message, &original_body);
+ first_commit = lookup_commit_reference(repo, &out[0]);
+ commit_list_append(first_commit, &parents);
+
-+ ret = commit_tree(original_body, strlen(original_body), &original_commit_tree_oid,
-+ parents, &out[1], original_author, NULL);
++ ret = commit_tree_with_edited_message(repo, "split-out", original_commit,
++ &original_commit_tree_oid,
++ parents, get_commit_tree_oid(first_commit), &out[1]);
+ if (ret < 0) {
-+ ret = error(_("failed writing second commit"));
++ ret = error(_("failed writing split-out commit"));
+ goto out;
+ }
+
@@ builtin/history.c: static int cmd_history_reword(int argc,
+out:
+ if (index_file.len)
+ unlink(index_file.buf);
-+ strbuf_release(&split_message);
+ strbuf_release(&index_file);
+ free_commit_list(parents);
-+ free(original_author);
+ release_index(&index);
+ return ret;
+}
@@ builtin/history.c: static int cmd_history_reword(int argc,
+ struct option options[] = {
+ OPT_END(),
+ };
-+ struct oidmap rewritten_commits = OIDMAP_INIT;
+ struct commit *original_commit, *parent, *head;
+ struct strvec commits = STRVEC_INIT;
-+ struct commit_list *from_list = NULL;
+ struct object_id split_commits[2];
+ struct pathspec pathspec = { 0 };
+ int ret;
@@ builtin/history.c: static int cmd_history_reword(int argc,
+ }
+ repo_config(repo, git_default_config, NULL);
+
-+ original_commit = lookup_commit_reference_by_name(argv[0]);
-+ if (!original_commit) {
-+ ret = error(_("commit to be split cannot be found: %s"), argv[0]);
-+ goto out;
-+ }
-+
-+ parent = original_commit->parents ? original_commit->parents->item : NULL;
-+ if (parent && repo_parse_commit(repo, parent)) {
-+ ret = error(_("unable to parse commit %s"),
-+ oid_to_hex(&parent->object.oid));
-+ goto out;
-+ }
-+
-+ head = lookup_commit_reference_by_name("HEAD");
-+ if (!head) {
-+ ret = error(_("could not resolve HEAD to a commit"));
-+ goto out;
-+ }
-+
-+ commit_list_append(original_commit, &from_list);
-+ if (!repo_is_descendant_of(repo, head, from_list)) {
-+ ret = error(_("split commit must be reachable from current HEAD commit"));
-+ goto out;
-+ }
-+
+ parse_pathspec(&pathspec, 0,
+ PATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,
+ prefix, argv + 1);
+
-+ /*
-+ * Collect the list of commits that we'll have to reapply now already.
-+ * This ensures that we'll abort early on in case the range of commits
-+ * contains merges, which we do not yet handle.
-+ */
-+ ret = collect_commits(repo, parent, head, &commits);
++ ret = gather_commits_between_head_and_revision(repo, argv[0], &original_commit,
++ &parent, &head, &commits);
+ if (ret < 0)
+ goto out;
+
@@ builtin/history.c: static int cmd_history_reword(int argc,
+ ret = 0;
+
+out:
-+ oidmap_clear(&rewritten_commits, 0);
-+ free_commit_list(from_list);
+ clear_pathspec(&pathspec);
+ strvec_clear(&commits);
+ return ret;
@@ t/t3452-history-split.sh (new)
+
+. ./test-lib.sh
+
++# The fake editor takes multiple arguments, each of which represents a commit
++# message. Subsequent invocations of the editor will then yield those messages
++# in order.
++#
+set_fake_editor () {
-+ write_script fake-editor.sh <<-EOF &&
-+ echo "$@" >"\$1"
++ printf "%s\n" "$@" >fake-input &&
++ write_script fake-editor.sh <<-\EOF &&
++ head -n1 fake-input >"$1"
++ sed 1d fake-input >fake-input.trimmed &&
++ mv fake-input.trimmed fake-input
+ EOF
+ test_set_editor "$(pwd)"/fake-editor.sh
+}
@@ t/t3452-history-split.sh (new)
+ git switch branch &&
+ test_commit theirs &&
+ test_must_fail git history split ours 2>err &&
-+ test_grep "split commit must be reachable from current HEAD commit" err
++ test_grep "commit must be reachable from current HEAD commit" err
+ )
+'
+
@@ t/t3452-history-split.sh (new)
+ git commit -m split-me &&
+
+ git symbolic-ref HEAD >expect &&
-+ set_fake_editor "split-out commit" &&
++ set_fake_editor "first" "second" &&
+ git history split HEAD <<-EOF &&
+ y
+ n
@@ t/t3452-history-split.sh (new)
+ test_cmp expect actual &&
+
+ expect_log <<-EOF &&
-+ split-me
-+ split-out commit
++ second
++ first
+ initial
+ EOF
+
@@ t/t3452-history-split.sh (new)
+ initial.t
+ EOF
+
-+ expect_tree_entries HEAD <<-EOF
++ expect_tree_entries HEAD <<-EOF &&
+ bar
+ foo
+ initial.t
+ EOF
++
++ git reflog >reflog &&
++ test_grep "split: updating HEAD" reflog
+ )
+'
+
@@ t/t3452-history-split.sh (new)
+ git commit -m root &&
+ test_commit tip &&
+
-+ set_fake_editor "split-out commit" &&
++ set_fake_editor "first" "second" &&
+ git history split HEAD~ <<-EOF &&
+ y
+ n
@@ t/t3452-history-split.sh (new)
+
+ expect_log <<-EOF &&
+ tip
-+ root
-+ split-out commit
++ second
++ first
+ EOF
+
+ expect_tree_entries HEAD~2 <<-EOF &&
@@ t/t3452-history-split.sh (new)
+ git commit -m split-me &&
+ test_commit tip &&
+
-+ set_fake_editor "split-out commit" &&
++ set_fake_editor "first" "second" &&
+ git history split HEAD~ <<-EOF &&
+ y
+ n
@@ t/t3452-history-split.sh (new)
+
+ expect_log <<-EOF &&
+ tip
-+ split-me
-+ split-out commit
++ second
++ first
+ initial
+ EOF
+
@@ t/t3452-history-split.sh (new)
+ git add . &&
+ git commit -m split-me &&
+
-+ set_fake_editor "split-out-commit" &&
++ set_fake_editor "first" "second" &&
+ git history split HEAD <<-EOF &&
+ y
+ n
@@ t/t3452-history-split.sh (new)
+ git add . &&
+ git commit -m split-me &&
+
-+ set_fake_editor "split-out commit" &&
++ set_fake_editor "first" "second" &&
+ git history split HEAD <<-EOF &&
+ n
+ y
+ EOF
+
+ expect_log <<-EOF &&
-+ split-me
-+ split-out commit
++ second
++ first
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
@@ t/t3452-history-split.sh (new)
+ git commit -m split-me &&
+
+ write_script fake-editor.sh <<-\EOF &&
-+ cp "$1" . &&
-+ echo "some commit message" >>"$1"
++ cat "$1" >>MESSAGES &&
++ echo "some commit message" >"$1"
+ EOF
+ test_set_editor "$(pwd)"/fake-editor.sh &&
+
@@ t/t3452-history-split.sh (new)
+ n
+ EOF
+
++ # Note that we expect to see the messages twice, once for each
++ # of the commits. The committed files are different though.
+ cat >expect <<-EOF &&
++ split-me
+
+ # Please enter the commit message for the split-out changes. Lines starting
-+ # with ${SQ}#${SQ} will be ignored.
++ # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
+ # Changes to be committed:
+ # new file: bar
+ #
++ split-me
++
++ # Please enter the commit message for the split-out changes. Lines starting
++ # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
++ # Changes to be committed:
++ # new file: foo
++ #
+ EOF
-+ test_cmp expect COMMIT_EDITMSG &&
++ test_cmp expect MESSAGES &&
+
+ expect_log <<-EOF
-+ split-me
++ some commit message
+ some commit message
+ EOF
+ )
@@ t/t3452-history-split.sh (new)
+ git add . &&
+ git commit -m split-me &&
+
-+ set_fake_editor "split-out commit" &&
++ set_fake_editor "first" "second" &&
+ git history split HEAD -- foo <<-EOF &&
+ y
+ EOF
@@ t/t3452-history-split.sh (new)
+ )
+'
+
-+test_expect_success 'hooks are executed for rewritten commits' '
++test_expect_success 'hooks are not executed for rewritten commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
@@ t/t3452-history-split.sh (new)
+ git commit -m split-me &&
+ old_head=$(git rev-parse HEAD) &&
+
-+ write_script .git/hooks/prepare-commit-msg <<-EOF &&
-+ touch "$(pwd)/hooks.log"
-+ EOF
-+ write_script .git/hooks/post-commit <<-EOF &&
-+ touch "$(pwd)/hooks.log"
-+ EOF
-+ write_script .git/hooks/post-rewrite <<-EOF &&
-+ touch "$(pwd)/hooks.log"
-+ EOF
++ ORIG_PATH="$(pwd)" &&
++ export ORIG_PATH &&
++ for hook in prepare-commit-msg pre-commit post-commit post-rewrite commit-msg
++ do
++ write_script .git/hooks/$hook <<-\EOF || exit 1
++ touch "$ORIG_PATH/hooks.log
++ EOF
++ done &&
+
-+ set_fake_editor "split-out commit" &&
++ set_fake_editor "first" "second" &&
+ git history split HEAD <<-EOF &&
+ y
+ n
+ EOF
+
+ expect_log <<-EOF &&
-+ split-me
-+ split-out commit
++ second
++ first
+ EOF
+
+ test_path_is_missing hooks.log
@@ t/t3452-history-split.sh (new)
+ echo a-modified >a &&
+ echo b-modified >b &&
+ git add b &&
-+ set_fake_editor "a-only" &&
++ set_fake_editor "a-only" "remainder" &&
+ git history split HEAD <<-EOF &&
+ y
+ n
@@ t/t3452-history-split.sh (new)
+ ?? actual
+ ?? expect
+ ?? fake-editor.sh
++ ?? fake-input
+ EOF
+ git status --porcelain >actual &&
+ test_cmp expect actual
---
base-commit: 0d4583b45432bff1cdc7689c648fb51be3d7b321
change-id: 20250819-b4-pks-history-builtin-83398f9a05f0
^ permalink raw reply [flat|nested] 278+ messages in thread* [PATCH v7 01/12] wt-status: provide function to expose status for trees
2025-12-03 10:48 ` [PATCH v7 00/12] " Patrick Steinhardt
@ 2025-12-03 10:48 ` Patrick Steinhardt
2025-12-03 10:48 ` [PATCH v7 02/12] replay: extract logic to pick commits Patrick Steinhardt
` (10 subsequent siblings)
11 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-12-03 10:48 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
The "wt-status" subsystem is responsible for printing status information
around the current state of the working tree. This most importantly
includes information around whether the working tree or the index have
any changes.
We're about to introduce a new command where the changes in neither of
them are actually relevant to us. Instead, what we want is to format the
changes between two different trees. While it is a little bit of a
stretch to add this as functionality to _working tree_ status, it
doesn't make any sense to open-code this functionality, either.
Implement a new function `wt_status_collect_changes_trees()` that diffs
two trees and formats the status accordingly. This function is not yet
used, but will be in a subsequent commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
wt-status.c | 24 ++++++++++++++++++++++++
wt-status.h | 9 +++++++++
2 files changed, 33 insertions(+)
diff --git a/wt-status.c b/wt-status.c
index e12adb26b9..95942399f8 100644
--- a/wt-status.c
+++ b/wt-status.c
@@ -612,6 +612,30 @@ static void wt_status_collect_updated_cb(struct diff_queue_struct *q,
}
}
+void wt_status_collect_changes_trees(struct wt_status *s,
+ const struct object_id *old_treeish,
+ const struct object_id *new_treeish)
+{
+ struct diff_options opts = { 0 };
+
+ repo_diff_setup(s->repo, &opts);
+ opts.output_format = DIFF_FORMAT_CALLBACK;
+ opts.format_callback = wt_status_collect_updated_cb;
+ opts.format_callback_data = s;
+ opts.detect_rename = s->detect_rename >= 0 ? s->detect_rename : opts.detect_rename;
+ opts.rename_limit = s->rename_limit >= 0 ? s->rename_limit : opts.rename_limit;
+ opts.rename_score = s->rename_score >= 0 ? s->rename_score : opts.rename_score;
+ opts.flags.recursive = 1;
+ diff_setup_done(&opts);
+
+ diff_tree_oid(old_treeish, new_treeish, "", &opts);
+ diffcore_std(&opts);
+ diff_flush(&opts);
+ wt_status_get_state(s->repo, &s->state, 0);
+
+ diff_free(&opts);
+}
+
static void wt_status_collect_changes_worktree(struct wt_status *s)
{
struct rev_info rev;
diff --git a/wt-status.h b/wt-status.h
index e40a27214a..e9fe32e98c 100644
--- a/wt-status.h
+++ b/wt-status.h
@@ -153,6 +153,15 @@ void wt_status_add_cut_line(struct wt_status *s);
void wt_status_prepare(struct repository *r, struct wt_status *s);
void wt_status_print(struct wt_status *s);
void wt_status_collect(struct wt_status *s);
+
+/*
+ * Collect all changes between the two trees. Changes will be displayed as if
+ * they were staged into the index.
+ */
+void wt_status_collect_changes_trees(struct wt_status *s,
+ const struct object_id *old_treeish,
+ const struct object_id *new_treeish);
+
/*
* Frees the buffers allocated by wt_status_collect.
*/
--
2.52.0.239.gd5f0c6e74e.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH v7 02/12] replay: extract logic to pick commits
2025-12-03 10:48 ` [PATCH v7 00/12] " Patrick Steinhardt
2025-12-03 10:48 ` [PATCH v7 01/12] wt-status: provide function to expose status for trees Patrick Steinhardt
@ 2025-12-03 10:48 ` Patrick Steinhardt
2025-12-03 10:48 ` [PATCH v7 03/12] replay: stop using `the_repository` Patrick Steinhardt
` (9 subsequent siblings)
11 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-12-03 10:48 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
We're about to add a new git-history(1) command that will reuse some of
the same infrastructure as git-replay(1). To prepare for this, extract
the logic to pick a commit into a new "replay.c" file so that it can be
shared between both commands.
Rename the function to have a "replay_" prefix to clearly indicate its
subsystem.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Makefile | 1 +
builtin/replay.c | 110 ++--------------------------------------------------
meson.build | 1 +
replay.c | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
replay.h | 23 +++++++++++
5 files changed, 143 insertions(+), 107 deletions(-)
diff --git a/Makefile b/Makefile
index 237b56fc9d..809b094595 100644
--- a/Makefile
+++ b/Makefile
@@ -1274,6 +1274,7 @@ LIB_OBJS += repack-geometry.o
LIB_OBJS += repack-midx.o
LIB_OBJS += repack-promisor.o
LIB_OBJS += replace-object.o
+LIB_OBJS += replay.o
LIB_OBJS += repo-settings.o
LIB_OBJS += repository.o
LIB_OBJS += rerere.o
diff --git a/builtin/replay.c b/builtin/replay.c
index 1f9ffd2b3e..f974a8c963 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -2,7 +2,6 @@
* "git replay" builtin command
*/
-#define USE_THE_REPOSITORY_VARIABLE
#define DISABLE_SIGN_COMPARE_WARNINGS
#include "git-compat-util.h"
@@ -16,6 +15,7 @@
#include "object-name.h"
#include "parse-options.h"
#include "refs.h"
+#include "replay.h"
#include "revision.h"
#include "strmap.h"
#include <oidset.h>
@@ -26,13 +26,6 @@ enum ref_action_mode {
REF_ACTION_PRINT,
};
-static const char *short_commit_name(struct repository *repo,
- struct commit *commit)
-{
- return repo_find_unique_abbrev(repo, &commit->object.oid,
- DEFAULT_ABBREV);
-}
-
static struct commit *peel_committish(struct repository *repo, const char *name)
{
struct object *obj;
@@ -45,59 +38,6 @@ static struct commit *peel_committish(struct repository *repo, const char *name)
OBJ_COMMIT);
}
-static char *get_author(const char *message)
-{
- size_t len;
- const char *a;
-
- a = find_commit_header(message, "author", &len);
- if (a)
- return xmemdupz(a, len);
-
- return NULL;
-}
-
-static struct commit *create_commit(struct repository *repo,
- struct tree *tree,
- struct commit *based_on,
- struct commit *parent)
-{
- struct object_id ret;
- struct object *obj = NULL;
- struct commit_list *parents = NULL;
- char *author;
- char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
- struct commit_extra_header *extra = NULL;
- struct strbuf msg = STRBUF_INIT;
- const char *out_enc = get_commit_output_encoding();
- const char *message = repo_logmsg_reencode(repo, based_on,
- NULL, out_enc);
- const char *orig_message = NULL;
- const char *exclude_gpgsig[] = { "gpgsig", "gpgsig-sha256", NULL };
-
- commit_list_insert(parent, &parents);
- extra = read_commit_extra_headers(based_on, exclude_gpgsig);
- find_commit_subject(message, &orig_message);
- strbuf_addstr(&msg, orig_message);
- author = get_author(message);
- reset_ident_date();
- if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,
- &ret, author, NULL, sign_commit, extra)) {
- error(_("failed to write commit object"));
- goto out;
- }
-
- obj = parse_object(repo, &ret);
-
-out:
- repo_unuse_commit_buffer(the_repository, based_on, message);
- free_commit_extra_headers(extra);
- free_commit_list(parents);
- strbuf_release(&msg);
- free(author);
- return (struct commit *)obj;
-}
-
struct ref_info {
struct commit *onto;
struct strset positive_refs;
@@ -246,50 +186,6 @@ static void determine_replay_mode(struct repository *repo,
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 repository *repo,
- struct commit *pickme,
- kh_oid_map_t *replayed_commits,
- struct commit *onto,
- struct merge_options *merge_opt,
- struct merge_result *result)
-{
- 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(repo, replayed_base);
- pickme_tree = repo_get_commit_tree(repo, pickme);
- base_tree = repo_get_commit_tree(repo, base);
-
- merge_opt->branch1 = short_commit_name(repo, replayed_base);
- merge_opt->branch2 = short_commit_name(repo, 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(repo, result->tree, pickme, replayed_base);
-}
-
static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)
{
if (!ref_action || !strcmp(ref_action, "update"))
@@ -495,8 +391,8 @@ int cmd_replay(int argc,
if (commit->parents->next)
die(_("replaying merge commits is not supported yet!"));
- last_commit = pick_regular_commit(repo, commit, replayed_commits,
- onto, &merge_opt, &result);
+ last_commit = replay_pick_regular_commit(repo, commit, replayed_commits,
+ onto, &merge_opt, &result);
if (!last_commit)
break;
diff --git a/meson.build b/meson.build
index f1b3615659..1878dff2b1 100644
--- a/meson.build
+++ b/meson.build
@@ -470,6 +470,7 @@ libgit_sources = [
'repack-midx.c',
'repack-promisor.c',
'replace-object.c',
+ 'replay.c',
'repo-settings.c',
'repository.c',
'rerere.c',
diff --git a/replay.c b/replay.c
new file mode 100644
index 0000000000..fb906e9b51
--- /dev/null
+++ b/replay.c
@@ -0,0 +1,115 @@
+#define USE_THE_REPOSITORY_VARIABLE
+
+#include "git-compat-util.h"
+#include "commit.h"
+#include "environment.h"
+#include "gettext.h"
+#include "ident.h"
+#include "object.h"
+#include "object-name.h"
+#include "replay.h"
+#include "tree.h"
+
+static const char *short_commit_name(struct repository *repo,
+ struct commit *commit)
+{
+ return repo_find_unique_abbrev(repo, &commit->object.oid,
+ DEFAULT_ABBREV);
+}
+
+static char *get_author(const char *message)
+{
+ size_t len;
+ const char *a;
+
+ a = find_commit_header(message, "author", &len);
+ if (a)
+ return xmemdupz(a, len);
+
+ return NULL;
+}
+
+struct commit *replay_create_commit(struct repository *repo,
+ struct tree *tree,
+ struct commit *based_on,
+ struct commit *parent)
+{
+ struct object_id ret;
+ struct object *obj = NULL;
+ struct commit_list *parents = NULL;
+ char *author;
+ char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
+ struct commit_extra_header *extra = NULL;
+ struct strbuf msg = STRBUF_INIT;
+ const char *out_enc = get_commit_output_encoding();
+ const char *message = repo_logmsg_reencode(repo, based_on,
+ NULL, out_enc);
+ const char *orig_message = NULL;
+ const char *exclude_gpgsig[] = { "gpgsig", "gpgsig-sha256", NULL };
+
+ commit_list_insert(parent, &parents);
+ extra = read_commit_extra_headers(based_on, exclude_gpgsig);
+ find_commit_subject(message, &orig_message);
+ strbuf_addstr(&msg, orig_message);
+ author = get_author(message);
+ reset_ident_date();
+ if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,
+ &ret, author, NULL, sign_commit, extra)) {
+ error(_("failed to write commit object"));
+ goto out;
+ }
+
+ obj = parse_object(repo, &ret);
+
+out:
+ repo_unuse_commit_buffer(the_repository, based_on, message);
+ free_commit_extra_headers(extra);
+ free_commit_list(parents);
+ strbuf_release(&msg);
+ free(author);
+ return (struct commit *)obj;
+}
+
+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);
+}
+
+struct commit *replay_pick_regular_commit(struct repository *repo,
+ struct commit *pickme,
+ kh_oid_map_t *replayed_commits,
+ struct commit *onto,
+ struct merge_options *merge_opt,
+ struct merge_result *result)
+{
+ 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(repo, replayed_base);
+ pickme_tree = repo_get_commit_tree(repo, pickme);
+ base_tree = repo_get_commit_tree(repo, base);
+
+ merge_opt->branch1 = short_commit_name(repo, replayed_base);
+ merge_opt->branch2 = short_commit_name(repo, 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 replay_create_commit(repo, result->tree, pickme, replayed_base);
+}
diff --git a/replay.h b/replay.h
new file mode 100644
index 0000000000..d6535ee56c
--- /dev/null
+++ b/replay.h
@@ -0,0 +1,23 @@
+#ifndef REPLAY_H
+#define REPLAY_H
+
+#include "khash.h"
+#include "merge-ort.h"
+#include "repository.h"
+
+struct commit;
+struct tree;
+
+struct commit *replay_create_commit(struct repository *repo,
+ struct tree *tree,
+ struct commit *based_on,
+ struct commit *parent);
+
+struct commit *replay_pick_regular_commit(struct repository *repo,
+ struct commit *pickme,
+ kh_oid_map_t *replayed_commits,
+ struct commit *onto,
+ struct merge_options *merge_opt,
+ struct merge_result *result);
+
+#endif
--
2.52.0.239.gd5f0c6e74e.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH v7 03/12] replay: stop using `the_repository`
2025-12-03 10:48 ` [PATCH v7 00/12] " Patrick Steinhardt
2025-12-03 10:48 ` [PATCH v7 01/12] wt-status: provide function to expose status for trees Patrick Steinhardt
2025-12-03 10:48 ` [PATCH v7 02/12] replay: extract logic to pick commits Patrick Steinhardt
@ 2025-12-03 10:48 ` Patrick Steinhardt
2025-12-03 10:48 ` [PATCH v7 04/12] builtin: add new "history" command Patrick Steinhardt
` (8 subsequent siblings)
11 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-12-03 10:48 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
In `create_commit()` we're using `the_repository` even though we already
have a repository passed to use as an argument. Fix this.
Note that we still cannot get rid of `USE_THE_REPOSITORY_VARIABLE`. This
is because we use `DEFAULT_ABBREV and `get_commit_output_encoding()`,
both of which are stored as global variables that can be modified via
the Git configuration.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
replay.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/replay.c b/replay.c
index fb906e9b51..13983dbc56 100644
--- a/replay.c
+++ b/replay.c
@@ -62,7 +62,7 @@ struct commit *replay_create_commit(struct repository *repo,
obj = parse_object(repo, &ret);
out:
- repo_unuse_commit_buffer(the_repository, based_on, message);
+ repo_unuse_commit_buffer(repo, based_on, message);
free_commit_extra_headers(extra);
free_commit_list(parents);
strbuf_release(&msg);
--
2.52.0.239.gd5f0c6e74e.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH v7 04/12] builtin: add new "history" command
2025-12-03 10:48 ` [PATCH v7 00/12] " Patrick Steinhardt
` (2 preceding siblings ...)
2025-12-03 10:48 ` [PATCH v7 03/12] replay: stop using `the_repository` Patrick Steinhardt
@ 2025-12-03 10:48 ` Patrick Steinhardt
2025-12-22 17:11 ` Kristoffer Haugsbakk
2025-12-03 10:48 ` [PATCH v7 05/12] builtin/history: implement "reword" subcommand Patrick Steinhardt
` (7 subsequent siblings)
11 siblings, 1 reply; 278+ messages in thread
From: Patrick Steinhardt @ 2025-12-03 10:48 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
When rewriting history via git-rebase(1) there are a few very common use
cases:
- The ordering of two commits should be reversed.
- A commit should be split up into two commits.
- A commit should be dropped from the history completely.
- Multiple commits should be squashed into one.
- Editing an existing commit that is not the tip of the current
branch.
While these operations are all doable, it often feels needlessly kludgey
to do so by doing an interactive rebase, using the editor to say what
one wants, and then perform the actions. Furthermore, some operations
like splitting up a commit into two are way more involved than that and
require a whole series of commands.
Add a new "history" command to plug this gap. This command will have
several different subcommands to imperatively rewrite history for common
use cases like the above. Some of these subcommands will be implemented
in subsequent commits.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
.gitignore | 1 +
Documentation/git-history.adoc | 42 ++++++++++++++++++++++++++++++++++++++++++
Documentation/meson.build | 1 +
Makefile | 1 +
builtin.h | 1 +
builtin/history.c | 22 ++++++++++++++++++++++
command-list.txt | 1 +
git.c | 1 +
meson.build | 1 +
t/meson.build | 1 +
t/t3450-history.sh | 17 +++++++++++++++++
11 files changed, 89 insertions(+)
diff --git a/.gitignore b/.gitignore
index 78a45cb5be..24635cf2d6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -79,6 +79,7 @@
/git-grep
/git-hash-object
/git-help
+/git-history
/git-hook
/git-http-backend
/git-http-fetch
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
new file mode 100644
index 0000000000..67b8ce2040
--- /dev/null
+++ b/Documentation/git-history.adoc
@@ -0,0 +1,42 @@
+git-history(1)
+==============
+
+NAME
+----
+git-history - EXPERIMENTAL: Rewrite history of the current branch
+
+SYNOPSIS
+--------
+[synopsis]
+git history [<options>]
+
+DESCRIPTION
+-----------
+
+Rewrite history by rearranging or modifying specific commits in the
+history.
+
+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
+
+This command is related to linkgit:git-rebase[1] in that both commands can be
+used to rewrite history. You should use rebases if you want to reapply a range
+of commits onto a different base, or interactive rebases if you want to edit a
+range of commits.
+
+Note that this command does not (yet) work with histories that contain
+merges. You should use linkgit:git-rebase[1] with the `--rebase-merges`
+flag instead.
+
+COMMANDS
+--------
+
+Several commands are available to rewrite history in different ways:
+
+CONFIGURATION
+-------------
+
+include::includes/cmd-config-section-all.adoc[]
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Documentation/meson.build b/Documentation/meson.build
index f02dbc20cb..fd2e8cc02d 100644
--- a/Documentation/meson.build
+++ b/Documentation/meson.build
@@ -64,6 +64,7 @@ manpages = {
'git-gui.adoc' : 1,
'git-hash-object.adoc' : 1,
'git-help.adoc' : 1,
+ 'git-history.adoc' : 1,
'git-hook.adoc' : 1,
'git-http-backend.adoc' : 1,
'git-http-fetch.adoc' : 1,
diff --git a/Makefile b/Makefile
index 809b094595..458841178b 100644
--- a/Makefile
+++ b/Makefile
@@ -1408,6 +1408,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o
BUILTIN_OBJS += builtin/grep.o
BUILTIN_OBJS += builtin/hash-object.o
BUILTIN_OBJS += builtin/help.o
+BUILTIN_OBJS += builtin/history.o
BUILTIN_OBJS += builtin/hook.o
BUILTIN_OBJS += builtin/index-pack.o
BUILTIN_OBJS += builtin/init-db.o
diff --git a/builtin.h b/builtin.h
index 1b35565fbd..93c91d07d4 100644
--- a/builtin.h
+++ b/builtin.h
@@ -172,6 +172,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix, struc
int cmd_grep(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_hash_object(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_help(int argc, const char **argv, const char *prefix, struct repository *repo);
+int cmd_history(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_hook(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_index_pack(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_init_db(int argc, const char **argv, const char *prefix, struct repository *repo);
diff --git a/builtin/history.c b/builtin/history.c
new file mode 100644
index 0000000000..f6fe32610b
--- /dev/null
+++ b/builtin/history.c
@@ -0,0 +1,22 @@
+#include "builtin.h"
+#include "gettext.h"
+#include "parse-options.h"
+
+int cmd_history(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo UNUSED)
+{
+ const char * const usage[] = {
+ N_("git history [<options>]"),
+ NULL,
+ };
+ struct option options[] = {
+ OPT_END(),
+ };
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc)
+ usagef("unrecognized argument: %s", argv[0]);
+ return 0;
+}
diff --git a/command-list.txt b/command-list.txt
index accd3d0c4b..f9005cf459 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -115,6 +115,7 @@ git-grep mainporcelain info
git-gui mainporcelain
git-hash-object plumbingmanipulators
git-help ancillaryinterrogators complete
+git-history mainporcelain history
git-hook purehelpers
git-http-backend synchingrepositories
git-http-fetch synchelpers
diff --git a/git.c b/git.c
index c5fad56813..744cb6527e 100644
--- a/git.c
+++ b/git.c
@@ -586,6 +586,7 @@ static struct cmd_struct commands[] = {
{ "grep", cmd_grep, RUN_SETUP_GENTLY },
{ "hash-object", cmd_hash_object },
{ "help", cmd_help },
+ { "history", cmd_history, RUN_SETUP },
{ "hook", cmd_hook, RUN_SETUP },
{ "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },
{ "init", cmd_init_db },
diff --git a/meson.build b/meson.build
index 1878dff2b1..f07a308fb8 100644
--- a/meson.build
+++ b/meson.build
@@ -610,6 +610,7 @@ builtin_sources = [
'builtin/grep.c',
'builtin/hash-object.c',
'builtin/help.c',
+ 'builtin/history.c',
'builtin/hook.c',
'builtin/index-pack.c',
'builtin/init-db.c',
diff --git a/t/meson.build b/t/meson.build
index 7c994d4643..62f5dca098 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -386,6 +386,7 @@ integration_tests = [
't3436-rebase-more-options.sh',
't3437-rebase-fixup-options.sh',
't3438-rebase-broken-files.sh',
+ 't3450-history.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
diff --git a/t/t3450-history.sh b/t/t3450-history.sh
new file mode 100755
index 0000000000..417c343d43
--- /dev/null
+++ b/t/t3450-history.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+test_description='tests for git-history command'
+
+. ./test-lib.sh
+
+test_expect_success 'does nothing without any arguments' '
+ git history >out 2>&1 &&
+ test_must_be_empty out
+'
+
+test_expect_success 'raises an error with unknown argument' '
+ test_must_fail git history garbage 2>err &&
+ test_grep "unrecognized argument: garbage" err
+'
+
+test_done
--
2.52.0.239.gd5f0c6e74e.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* Re: [PATCH v7 04/12] builtin: add new "history" command
2025-12-03 10:48 ` [PATCH v7 04/12] builtin: add new "history" command Patrick Steinhardt
@ 2025-12-22 17:11 ` Kristoffer Haugsbakk
0 siblings, 0 replies; 278+ messages in thread
From: Kristoffer Haugsbakk @ 2025-12-22 17:11 UTC (permalink / raw)
To: Patrick Steinhardt, git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Elijah Newren,
Karthik Nayak
On Wed, Dec 3, 2025, at 11:48, Patrick Steinhardt wrote:
> When rewriting history via git-rebase(1) there are a few very common use
> cases:
>[snip]
> +CONFIGURATION
> +-------------
> +
> +include::includes/cmd-config-section-all.adoc[]
Now there are no config variables listed after
`include::config/sequencer.adoc[]` was removed.
> +
> +GIT
> +---
> +Part of the linkgit:git[1] suite
>[snip]
^ permalink raw reply [flat|nested] 278+ messages in thread
* [PATCH v7 05/12] builtin/history: implement "reword" subcommand
2025-12-03 10:48 ` [PATCH v7 00/12] " Patrick Steinhardt
` (3 preceding siblings ...)
2025-12-03 10:48 ` [PATCH v7 04/12] builtin: add new "history" command Patrick Steinhardt
@ 2025-12-03 10:48 ` Patrick Steinhardt
2025-12-03 10:48 ` [PATCH v7 06/12] add-patch: split out header from "add-interactive.h" Patrick Steinhardt
` (6 subsequent siblings)
11 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-12-03 10:48 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
Implement a new "reword" subcommand for git-history(1). This subcommand
is similar to the user performing an interactive rebase with a single
commit changed to use the "reword" instruction.
The major difference is that we do not check out the commit that is to
be reworded. This has the obvious benefit of being significantly faster
compared to git-rebase(1), but even more importantly it allows the user
to rewrite history even if there are local changes in the working tree
or in the index.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-history.adoc | 7 +-
builtin/history.c | 372 ++++++++++++++++++++++++++++++++++++++++-
t/meson.build | 1 +
t/t3450-history.sh | 6 +-
t/t3451-history-reword.sh | 236 ++++++++++++++++++++++++++
5 files changed, 613 insertions(+), 9 deletions(-)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index 67b8ce2040..160bf5d4d2 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -8,7 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history of the current branch
SYNOPSIS
--------
[synopsis]
-git history [<options>]
+git history reword <commit>
DESCRIPTION
-----------
@@ -32,6 +32,11 @@ COMMANDS
Several commands are available to rewrite history in different ways:
+`reword <commit>`::
+ Rewrite the commit message of the specified commit. All the other
+ details of this commit remain unchanged. This command will spawn an
+ editor with the current message of that commit.
+
CONFIGURATION
-------------
diff --git a/builtin/history.c b/builtin/history.c
index f6fe32610b..17bb150b95 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -1,22 +1,384 @@
+#define USE_THE_REPOSITORY_VARIABLE
+
#include "builtin.h"
+#include "commit-reach.h"
+#include "commit.h"
+#include "config.h"
+#include "editor.h"
+#include "environment.h"
#include "gettext.h"
+#include "hex.h"
#include "parse-options.h"
+#include "refs.h"
+#include "replay.h"
+#include "reset.h"
+#include "revision.h"
+#include "sequencer.h"
+#include "strvec.h"
+#include "tree.h"
+#include "wt-status.h"
+
+#define GIT_HISTORY_REWORD_USAGE N_("git history reword <commit>")
+
+static int collect_commits(struct repository *repo,
+ struct commit *old_commit,
+ struct commit *new_commit,
+ struct strvec *out)
+{
+ struct setup_revision_opt revision_opts = {
+ .assume_dashdash = 1,
+ };
+ struct strvec revisions = STRVEC_INIT;
+ struct commit *child;
+ struct rev_info rev = { 0 };
+ int ret;
+
+ repo_init_revisions(repo, &rev, NULL);
+ rev.reverse = 1;
+ strvec_push(&revisions, "");
+ strvec_push(&revisions, oid_to_hex(&new_commit->object.oid));
+ if (old_commit) {
+ strvec_pushf(&revisions, "^%s", oid_to_hex(&old_commit->object.oid));
+ strvec_pushf(&revisions, "--ancestry-path=%s", oid_to_hex(&old_commit->object.oid));
+ }
+
+ setup_revisions_from_strvec(&revisions, &rev, &revision_opts);
+ if (revisions.nr != 1 || prepare_revision_walk(&rev)) {
+ ret = error(_("revision walk setup failed"));
+ goto out;
+ }
+
+ while ((child = get_revision(&rev))) {
+ if (old_commit && !child->parents)
+ BUG("revision walk did not find child commit");
+ if (child->parents && child->parents->next) {
+ ret = error(_("cannot rearrange commit history with merges"));
+ goto out;
+ }
+
+ strvec_push(out, oid_to_hex(&child->object.oid));
+ }
+
+ ret = 0;
+
+out:
+ strvec_clear(&revisions);
+ release_revisions(&rev);
+ reset_revision_walk();
+ return ret;
+}
+
+static int gather_commits_between_head_and_revision(struct repository *repo,
+ const char *revision,
+ struct commit **original_commit,
+ struct commit **parent_commit,
+ struct commit **head,
+ struct strvec *commits)
+{
+ struct commit_list *from_list = NULL;
+ int ret;
+
+ *original_commit = lookup_commit_reference_by_name(revision);
+ if (!*original_commit) {
+ ret = error(_("commit cannot be found: %s"), revision);
+ goto out;
+ }
+
+ *parent_commit = (*original_commit)->parents ? (*original_commit)->parents->item : NULL;
+ if (*parent_commit && repo_parse_commit(repo, *parent_commit)) {
+ ret = error(_("unable to parse commit %s"),
+ oid_to_hex(&(*parent_commit)->object.oid));
+ goto out;
+ }
+
+ *head = lookup_commit_reference_by_name("HEAD");
+ if (!(*head)) {
+ ret = error(_("could not resolve HEAD to a commit"));
+ goto out;
+ }
+
+ commit_list_append(*original_commit, &from_list);
+ if (!repo_is_descendant_of(repo, *head, from_list)) {
+ ret = error(_("commit must be reachable from current HEAD commit"));
+ goto out;
+ }
+
+ /*
+ * Collect the list of commits that we'll have to reapply now already.
+ * This ensures that we'll abort early on in case the range of commits
+ * contains merges, which we do not yet handle.
+ */
+ ret = collect_commits(repo, *parent_commit, *head, commits);
+ if (ret < 0)
+ goto out;
+
+out:
+ free_commit_list(from_list);
+ return ret;
+}
+
+static void replace_commits(struct strvec *commits,
+ const struct object_id *commit_to_replace,
+ const struct object_id *replacements,
+ size_t replacements_nr)
+{
+ char commit_to_replace_oid[GIT_MAX_HEXSZ + 1];
+ struct strvec replacement_oids = STRVEC_INIT;
+ bool found = false;
+
+ oid_to_hex_r(commit_to_replace_oid, commit_to_replace);
+ for (size_t i = 0; i < replacements_nr; i++)
+ strvec_push(&replacement_oids, oid_to_hex(&replacements[i]));
+
+ for (size_t i = 0; i < commits->nr; i++) {
+ if (strcmp(commits->v[i], commit_to_replace_oid))
+ continue;
+ strvec_splice(commits, i, 1, replacement_oids.v, replacement_oids.nr);
+ found = true;
+ break;
+ }
+ if (!found)
+ BUG("could not find commit to replace");
+
+ strvec_clear(&replacement_oids);
+}
+
+static int apply_commits(struct repository *repo,
+ const struct strvec *commits,
+ struct commit *onto,
+ struct commit *orig_head,
+ const char *action)
+{
+ struct reset_head_opts reset_opts = { 0 };
+ struct strbuf buf = STRBUF_INIT;
+ int ret;
+
+ for (size_t i = 0; i < commits->nr; i++) {
+ struct object_id commit_id;
+ struct commit *commit;
+ const char *end;
+
+ if (parse_oid_hex_algop(commits->v[i], &commit_id, &end,
+ repo->hash_algo)) {
+ ret = error(_("invalid object ID: %s"), commits->v[i]);
+ goto out;
+ }
+
+ commit = lookup_commit(repo, &commit_id);
+ if (!commit || repo_parse_commit(repo, commit)) {
+ ret = error(_("failed to look up commit: %s"), oid_to_hex(&commit_id));
+ goto out;
+ }
+
+ if (!onto) {
+ onto = commit;
+ } else {
+ struct tree *tree = repo_get_commit_tree(repo, commit);
+ onto = replay_create_commit(repo, tree, commit, onto);
+ if (!onto) {
+ ret = -1;
+ goto out;
+ }
+ }
+ }
+
+ reset_opts.oid = &onto->object.oid;
+ strbuf_addf(&buf, "%s: switch to rewritten %s", action, oid_to_hex(reset_opts.oid));
+ reset_opts.flags = RESET_HEAD_REFS_ONLY | RESET_ORIG_HEAD;
+ reset_opts.orig_head = &orig_head->object.oid;
+ reset_opts.default_reflog_action = action;
+ if (reset_head(repo, &reset_opts) < 0) {
+ ret = error(_("could not switch to %s"), oid_to_hex(reset_opts.oid));
+ goto out;
+ }
+
+ ret = 0;
+
+out:
+ strbuf_release(&buf);
+ return ret;
+}
+
+static void change_data_free(void *util, const char *str UNUSED)
+{
+ struct wt_status_change_data *d = util;
+ free(d->rename_source);
+ free(d);
+}
+
+static int fill_commit_message(struct repository *repo,
+ const struct object_id *old_tree,
+ const struct object_id *new_tree,
+ const char *default_message,
+ const char *action,
+ struct strbuf *out)
+{
+ const char *path = git_path_commit_editmsg();
+ const char *hint =
+ _("Please enter the commit message for the %s changes."
+ " Lines starting\nwith '%s' will be ignored, and an"
+ " empty message aborts the commit.\n");
+ struct wt_status s;
+
+ strbuf_addstr(out, default_message);
+ strbuf_addch(out, '\n');
+ strbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);
+ write_file_buf(path, out->buf, out->len);
+
+ wt_status_prepare(repo, &s);
+ FREE_AND_NULL(s.branch);
+ s.ahead_behind_flags = AHEAD_BEHIND_QUICK;
+ s.commit_template = 1;
+ s.colopts = 0;
+ s.display_comment_prefix = 1;
+ s.hints = 0;
+ s.use_color = 0;
+ s.whence = FROM_COMMIT;
+ s.committable = 1;
+
+ s.fp = fopen(git_path_commit_editmsg(), "a");
+ if (!s.fp)
+ return error_errno(_("could not open '%s'"), git_path_commit_editmsg());
+
+ wt_status_collect_changes_trees(&s, old_tree, new_tree);
+ wt_status_print(&s);
+ wt_status_collect_free_buffers(&s);
+ string_list_clear_func(&s.change, change_data_free);
+
+ strbuf_reset(out);
+ if (launch_editor(path, out, NULL)) {
+ fprintf(stderr, _("Aborting commit as launching the editor failed.\n"));
+ return -1;
+ }
+ strbuf_stripspace(out, comment_line_str);
+
+ cleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);
+
+ if (!out->len) {
+ fprintf(stderr, _("Aborting commit due to empty commit message.\n"));
+ return -1;
+ }
+
+ return 0;
+}
+
+static int commit_tree_with_edited_message(struct repository *repo,
+ const char *action,
+ struct commit *original_commit,
+ const struct object_id *new_tree_oid,
+ const struct commit_list *parents,
+ const struct object_id *parent_tree_oid,
+ struct object_id *out)
+{
+ const char *exclude_gpgsig[] = { "gpgsig", "gpgsig-sha256", NULL };
+ const char *original_message, *original_body, *ptr;
+ struct commit_extra_header *original_extra_headers = NULL;
+ struct strbuf commit_message = STRBUF_INIT;
+ char *original_author = NULL;
+ size_t len;
+ int ret;
+
+ /* We retain authorship of the original commit. */
+ original_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);
+ ptr = find_commit_header(original_message, "author", &len);
+ if (ptr)
+ original_author = xmemdupz(ptr, len);
+ find_commit_subject(original_message, &original_body);
+
+ ret = fill_commit_message(repo, parent_tree_oid, new_tree_oid,
+ original_body, action, &commit_message);
+ if (ret < 0)
+ goto out;
+
+ original_extra_headers = read_commit_extra_headers(original_commit, exclude_gpgsig);
+
+ ret = commit_tree_extended(commit_message.buf, commit_message.len, new_tree_oid,
+ parents, out, original_author, NULL, NULL,
+ original_extra_headers);
+ if (ret < 0)
+ goto out;
+
+out:
+ free_commit_extra_headers(original_extra_headers);
+ strbuf_release(&commit_message);
+ free(original_author);
+ return ret;
+}
+
+static int cmd_history_reword(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo)
+{
+ const char * const usage[] = {
+ GIT_HISTORY_REWORD_USAGE,
+ NULL,
+ };
+ struct option options[] = {
+ OPT_END(),
+ };
+ struct commit *original_commit, *parent, *head;
+ struct strvec commits = STRVEC_INIT;
+ struct object_id parent_tree_oid, original_commit_tree_oid;
+ struct object_id rewritten_commit;
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc != 1) {
+ ret = error(_("command expects a single revision"));
+ goto out;
+ }
+ repo_config(repo, git_default_config, NULL);
+
+ ret = gather_commits_between_head_and_revision(repo, argv[0], &original_commit,
+ &parent, &head, &commits);
+ if (ret < 0)
+ goto out;
+
+ original_commit_tree_oid = repo_get_commit_tree(repo, original_commit)->object.oid;
+ if (parent)
+ parent_tree_oid = repo_get_commit_tree(repo, parent)->object.oid;
+ else
+ oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
+
+ /* We retain authorship of the original commit. */
+ ret = commit_tree_with_edited_message(repo, "reworded", original_commit,
+ &original_commit_tree_oid,
+ original_commit->parents, &parent_tree_oid,
+ &rewritten_commit);
+ if (ret < 0) {
+ ret = error(_("failed writing reworded commit"));
+ goto out;
+ }
+
+ replace_commits(&commits, &original_commit->object.oid, &rewritten_commit, 1);
+
+ ret = apply_commits(repo, &commits, parent, head, "reword");
+ if (ret < 0)
+ goto out;
+
+ ret = 0;
+
+out:
+ strvec_clear(&commits);
+ return ret;
+}
int cmd_history(int argc,
const char **argv,
const char *prefix,
- struct repository *repo UNUSED)
+ struct repository *repo)
{
const char * const usage[] = {
- N_("git history [<options>]"),
+ GIT_HISTORY_REWORD_USAGE,
NULL,
};
+ parse_opt_subcommand_fn *fn = NULL;
struct option options[] = {
+ OPT_SUBCOMMAND("reword", &fn, cmd_history_reword),
OPT_END(),
};
argc = parse_options(argc, argv, prefix, options, usage, 0);
- if (argc)
- usagef("unrecognized argument: %s", argv[0]);
- return 0;
+ return fn(argc, argv, prefix, repo);
}
diff --git a/t/meson.build b/t/meson.build
index 62f5dca098..e187aef31e 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -387,6 +387,7 @@ integration_tests = [
't3437-rebase-fixup-options.sh',
't3438-rebase-broken-files.sh',
't3450-history.sh',
+ 't3451-history-reword.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
diff --git a/t/t3450-history.sh b/t/t3450-history.sh
index 417c343d43..f513463b92 100755
--- a/t/t3450-history.sh
+++ b/t/t3450-history.sh
@@ -5,13 +5,13 @@ test_description='tests for git-history command'
. ./test-lib.sh
test_expect_success 'does nothing without any arguments' '
- git history >out 2>&1 &&
- test_must_be_empty out
+ test_must_fail git history 2>err &&
+ test_grep "need a subcommand" err
'
test_expect_success 'raises an error with unknown argument' '
test_must_fail git history garbage 2>err &&
- test_grep "unrecognized argument: garbage" err
+ test_grep "unknown subcommand: .garbage." err
'
test_done
diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
new file mode 100755
index 0000000000..2a638d2378
--- /dev/null
+++ b/t/t3451-history-reword.sh
@@ -0,0 +1,236 @@
+#!/bin/sh
+
+test_description='tests for git-history reword subcommand'
+
+. ./test-lib.sh
+
+reword_with_message () {
+ cat >message &&
+ write_script fake-editor.sh <<-\EOF &&
+ cp message "$1"
+ EOF
+ test_set_editor "$(pwd)"/fake-editor.sh &&
+ git history reword "$@" &&
+ rm fake-editor.sh message
+}
+
+test_expect_success 'refuses to work with merge commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ git branch branch &&
+ test_commit ours &&
+ git switch branch &&
+ test_commit theirs &&
+ git switch - &&
+ git merge theirs &&
+ test_must_fail git history reword HEAD~ 2>err &&
+ test_grep "cannot rearrange commit history with merges" err &&
+ test_must_fail git history reword HEAD 2>err &&
+ test_grep "cannot rearrange commit history with merges" err
+ )
+'
+
+test_expect_success 'refuses to work with unrelated commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ git branch branch &&
+ test_commit ours &&
+ git switch branch &&
+ test_commit theirs &&
+ test_must_fail git history reword ours 2>err &&
+ test_grep "commit must be reachable from current HEAD commit" err
+ )
+'
+
+test_expect_success 'can reword tip of a branch' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ git symbolic-ref HEAD >expect &&
+ reword_with_message HEAD <<-EOF &&
+ third reworded
+ EOF
+ git symbolic-ref HEAD >actual &&
+ test_cmp expect actual &&
+
+ cat >expect <<-EOF &&
+ third reworded
+ second
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual &&
+
+ git reflog >reflog &&
+ test_grep "reword: updating HEAD" reflog
+ )
+'
+
+test_expect_success 'can reword commit in the middle' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ git symbolic-ref HEAD >expect &&
+ reword_with_message HEAD~ <<-EOF &&
+ second reworded
+ EOF
+ git symbolic-ref HEAD >actual &&
+ test_cmp expect actual &&
+
+ cat >expect <<-EOF &&
+ third
+ second reworded
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'can reword root commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ reword_with_message HEAD~2 <<-EOF &&
+ first reworded
+ EOF
+
+ cat >expect <<-EOF &&
+ third
+ second
+ first reworded
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'editor shows proper status' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+
+ write_script fake-editor.sh <<-\EOF &&
+ cp "$1" . &&
+ printf "\namend a comment\n" >>"$1"
+ EOF
+ test_set_editor "$(pwd)"/fake-editor.sh &&
+ git history reword HEAD &&
+
+ cat >expect <<-EOF &&
+ first
+
+ # Please enter the commit message for the reworded changes. Lines starting
+ # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
+ # Changes to be committed:
+ # new file: first.t
+ #
+ EOF
+ test_cmp expect COMMIT_EDITMSG &&
+
+ test_commit_message HEAD <<-\EOF
+ first
+
+ amend a comment
+ EOF
+ )
+'
+
+# For now, git-history(1) does not yet execute any hooks. This is subject to
+# change in the future, and if it does this test here is expected to start
+# failing. In other words, this test is not an endorsement of the current
+# status quo.
+test_expect_success 'hooks are not executed for rewritten commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ ORIG_PATH="$(pwd)" &&
+ export ORIG_PATH &&
+ for hook in prepare-commit-msg pre-commit post-commit post-rewrite commit-msg
+ do
+ write_script .git/hooks/$hook <<-\EOF || exit 1
+ touch "$ORIG_PATH/hooks.log
+ EOF
+ done &&
+
+ reword_with_message HEAD~ <<-EOF &&
+ second reworded
+ EOF
+
+ cat >expect <<-EOF &&
+ third
+ second reworded
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual &&
+
+ test_path_is_missing hooks.log
+ )
+'
+
+test_expect_success 'aborts with empty commit message' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+
+ ! reword_with_message HEAD 2>err </dev/null &&
+ test_grep "Aborting commit due to empty commit message." err
+ )
+'
+
+test_expect_success 'retains changes in the worktree and index' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch a b &&
+ git add . &&
+ git commit -m "initial commit" &&
+ echo foo >a &&
+ echo bar >b &&
+ git add b &&
+ reword_with_message HEAD <<-EOF &&
+ message
+ EOF
+ cat >expect <<-\EOF &&
+ M a
+ M b
+ ?? actual
+ ?? expect
+ EOF
+ git status --porcelain >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_done
--
2.52.0.239.gd5f0c6e74e.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH v7 06/12] add-patch: split out header from "add-interactive.h"
2025-12-03 10:48 ` [PATCH v7 00/12] " Patrick Steinhardt
` (4 preceding siblings ...)
2025-12-03 10:48 ` [PATCH v7 05/12] builtin/history: implement "reword" subcommand Patrick Steinhardt
@ 2025-12-03 10:48 ` Patrick Steinhardt
2025-12-03 10:48 ` [PATCH v7 07/12] add-patch: split out `struct interactive_options` Patrick Steinhardt
` (5 subsequent siblings)
11 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-12-03 10:48 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
While we have a "add-patch.c" code file, its declarations are part of
"add-interactive.h". This makes it somewhat harder than necessary to
find relevant code and to identify clear boundaries between the two
subsystems.
Split up concerns and move declarations that relate to "add-patch.c"
into a new "add-patch.h" header.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
add-interactive.h | 23 +++--------------------
add-patch.c | 1 +
add-patch.h | 26 ++++++++++++++++++++++++++
3 files changed, 30 insertions(+), 20 deletions(-)
diff --git a/add-interactive.h b/add-interactive.h
index da49502b76..2e3d1d871d 100644
--- a/add-interactive.h
+++ b/add-interactive.h
@@ -1,14 +1,11 @@
#ifndef ADD_INTERACTIVE_H
#define ADD_INTERACTIVE_H
+#include "add-patch.h"
#include "color.h"
-struct add_p_opt {
- int context;
- int interhunkcontext;
-};
-
-#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }
+struct pathspec;
+struct repository;
struct add_i_state {
struct repository *r;
@@ -35,21 +32,7 @@ void init_add_i_state(struct add_i_state *s, struct repository *r,
struct add_p_opt *add_p_opt);
void clear_add_i_state(struct add_i_state *s);
-struct repository;
-struct pathspec;
int run_add_i(struct repository *r, const struct pathspec *ps,
struct add_p_opt *add_p_opt);
-enum add_p_mode {
- ADD_P_ADD,
- ADD_P_STASH,
- ADD_P_RESET,
- ADD_P_CHECKOUT,
- ADD_P_WORKTREE,
-};
-
-int run_add_p(struct repository *r, enum add_p_mode mode,
- struct add_p_opt *o, const char *revision,
- const struct pathspec *ps);
-
#endif
diff --git a/add-patch.c b/add-patch.c
index 173a53241e..5e3481083b 100644
--- a/add-patch.c
+++ b/add-patch.c
@@ -3,6 +3,7 @@
#include "git-compat-util.h"
#include "add-interactive.h"
+#include "add-patch.h"
#include "advice.h"
#include "editor.h"
#include "environment.h"
diff --git a/add-patch.h b/add-patch.h
new file mode 100644
index 0000000000..4394c74107
--- /dev/null
+++ b/add-patch.h
@@ -0,0 +1,26 @@
+#ifndef ADD_PATCH_H
+#define ADD_PATCH_H
+
+struct pathspec;
+struct repository;
+
+struct add_p_opt {
+ int context;
+ int interhunkcontext;
+};
+
+#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }
+
+enum add_p_mode {
+ ADD_P_ADD,
+ ADD_P_STASH,
+ ADD_P_RESET,
+ ADD_P_CHECKOUT,
+ ADD_P_WORKTREE,
+};
+
+int run_add_p(struct repository *r, enum add_p_mode mode,
+ struct add_p_opt *o, const char *revision,
+ const struct pathspec *ps);
+
+#endif
--
2.52.0.239.gd5f0c6e74e.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH v7 07/12] add-patch: split out `struct interactive_options`
2025-12-03 10:48 ` [PATCH v7 00/12] " Patrick Steinhardt
` (5 preceding siblings ...)
2025-12-03 10:48 ` [PATCH v7 06/12] add-patch: split out header from "add-interactive.h" Patrick Steinhardt
@ 2025-12-03 10:48 ` Patrick Steinhardt
2025-12-03 10:48 ` [PATCH v7 08/12] add-patch: remove dependency on "add-interactive" subsystem Patrick Steinhardt
` (4 subsequent siblings)
11 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-12-03 10:48 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
The `struct add_p_opt` is reused both by our infra for "git add -p" and
"git add -i". Users of `run_add_i()` for example are expected to pass
`struct add_p_opt`. This is somewhat confusing and raises the question
of which options apply to what part of the stack.
But things are even more confusing than that: while callers are expected
to pass in `struct add_p_opt`, these options ultimately get used to
initialize a `struct add_i_state` that is used by both subsystems. So we
are basically going full circle here.
Refactor the code and split out a new `struct interactive_options` that
hosts common options used by both. These options are then applied to a
`struct interactive_config` that hosts common configuration.
This refactoring doesn't yet fully detangle the two subsystems from one
another, as we still end up calling `init_add_i_state()` in the "git add
-p" subsystem. This will be fixed in a subsequent commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
add-interactive.c | 174 +++++++++++------------------------------------------
add-interactive.h | 23 +------
add-patch.c | 170 +++++++++++++++++++++++++++++++++++++++++++--------
add-patch.h | 36 ++++++++++-
builtin/add.c | 22 +++----
builtin/checkout.c | 4 +-
builtin/commit.c | 16 ++---
builtin/reset.c | 16 ++---
builtin/stash.c | 46 +++++++-------
commit.h | 2 +-
10 files changed, 270 insertions(+), 239 deletions(-)
diff --git a/add-interactive.c b/add-interactive.c
index 68fc09547d..05d2e7eefe 100644
--- a/add-interactive.c
+++ b/add-interactive.c
@@ -3,7 +3,6 @@
#include "git-compat-util.h"
#include "add-interactive.h"
#include "color.h"
-#include "config.h"
#include "diffcore.h"
#include "gettext.h"
#include "hash.h"
@@ -20,119 +19,18 @@
#include "prompt.h"
#include "tree.h"
-static void init_color(struct repository *r, enum git_colorbool use_color,
- const char *section_and_slot, char *dst,
- const char *default_color)
-{
- char *key = xstrfmt("color.%s", section_and_slot);
- const char *value;
-
- if (!want_color(use_color))
- dst[0] = '\0';
- else if (repo_config_get_value(r, key, &value) ||
- color_parse(value, dst))
- strlcpy(dst, default_color, COLOR_MAXLEN);
-
- free(key);
-}
-
-static enum git_colorbool check_color_config(struct repository *r, const char *var)
-{
- const char *value;
- enum git_colorbool ret;
-
- if (repo_config_get_value(r, var, &value))
- ret = GIT_COLOR_UNKNOWN;
- else
- ret = git_config_colorbool(var, value);
-
- /*
- * Do not rely on want_color() to fall back to color.ui for us. It uses
- * the value parsed by git_color_config(), which may not have been
- * called by the main command.
- */
- if (ret == GIT_COLOR_UNKNOWN &&
- !repo_config_get_value(r, "color.ui", &value))
- ret = git_config_colorbool("color.ui", value);
-
- return ret;
-}
-
void init_add_i_state(struct add_i_state *s, struct repository *r,
- struct add_p_opt *add_p_opt)
+ struct interactive_options *opts)
{
s->r = r;
- s->context = -1;
- s->interhunkcontext = -1;
-
- s->use_color_interactive = check_color_config(r, "color.interactive");
-
- init_color(r, s->use_color_interactive, "interactive.header",
- s->header_color, GIT_COLOR_BOLD);
- init_color(r, s->use_color_interactive, "interactive.help",
- s->help_color, GIT_COLOR_BOLD_RED);
- init_color(r, s->use_color_interactive, "interactive.prompt",
- s->prompt_color, GIT_COLOR_BOLD_BLUE);
- init_color(r, s->use_color_interactive, "interactive.error",
- s->error_color, GIT_COLOR_BOLD_RED);
- strlcpy(s->reset_color_interactive,
- want_color(s->use_color_interactive) ? GIT_COLOR_RESET : "", COLOR_MAXLEN);
-
- s->use_color_diff = check_color_config(r, "color.diff");
-
- init_color(r, s->use_color_diff, "diff.frag", s->fraginfo_color,
- diff_get_color(s->use_color_diff, DIFF_FRAGINFO));
- init_color(r, s->use_color_diff, "diff.context", s->context_color,
- "fall back");
- if (!strcmp(s->context_color, "fall back"))
- init_color(r, s->use_color_diff, "diff.plain",
- s->context_color,
- diff_get_color(s->use_color_diff, DIFF_CONTEXT));
- init_color(r, s->use_color_diff, "diff.old", s->file_old_color,
- diff_get_color(s->use_color_diff, DIFF_FILE_OLD));
- init_color(r, s->use_color_diff, "diff.new", s->file_new_color,
- diff_get_color(s->use_color_diff, DIFF_FILE_NEW));
- strlcpy(s->reset_color_diff,
- want_color(s->use_color_diff) ? GIT_COLOR_RESET : "", COLOR_MAXLEN);
-
- FREE_AND_NULL(s->interactive_diff_filter);
- repo_config_get_string(r, "interactive.difffilter",
- &s->interactive_diff_filter);
-
- FREE_AND_NULL(s->interactive_diff_algorithm);
- repo_config_get_string(r, "diff.algorithm",
- &s->interactive_diff_algorithm);
-
- if (!repo_config_get_int(r, "diff.context", &s->context))
- if (s->context < 0)
- die(_("%s cannot be negative"), "diff.context");
- if (!repo_config_get_int(r, "diff.interHunkContext", &s->interhunkcontext))
- if (s->interhunkcontext < 0)
- die(_("%s cannot be negative"), "diff.interHunkContext");
-
- repo_config_get_bool(r, "interactive.singlekey", &s->use_single_key);
- if (s->use_single_key)
- setbuf(stdin, NULL);
-
- if (add_p_opt->context != -1) {
- if (add_p_opt->context < 0)
- die(_("%s cannot be negative"), "--unified");
- s->context = add_p_opt->context;
- }
- if (add_p_opt->interhunkcontext != -1) {
- if (add_p_opt->interhunkcontext < 0)
- die(_("%s cannot be negative"), "--inter-hunk-context");
- s->interhunkcontext = add_p_opt->interhunkcontext;
- }
+ interactive_config_init(&s->cfg, r, opts);
}
void clear_add_i_state(struct add_i_state *s)
{
- FREE_AND_NULL(s->interactive_diff_filter);
- FREE_AND_NULL(s->interactive_diff_algorithm);
+ interactive_config_clear(&s->cfg);
memset(s, 0, sizeof(*s));
- s->use_color_interactive = GIT_COLOR_UNKNOWN;
- s->use_color_diff = GIT_COLOR_UNKNOWN;
+ interactive_config_clear(&s->cfg);
}
/*
@@ -286,7 +184,7 @@ static void list(struct add_i_state *s, struct string_list *list, int *selected,
return;
if (opts->header)
- color_fprintf_ln(stdout, s->header_color,
+ color_fprintf_ln(stdout, s->cfg.header_color,
"%s", opts->header);
for (i = 0; i < list->nr; i++) {
@@ -354,7 +252,7 @@ static ssize_t list_and_choose(struct add_i_state *s,
list(s, &items->items, items->selected, &opts->list_opts);
- color_fprintf(stdout, s->prompt_color, "%s", opts->prompt);
+ color_fprintf(stdout, s->cfg.prompt_color, "%s", opts->prompt);
fputs(singleton ? "> " : ">> ", stdout);
fflush(stdout);
@@ -432,7 +330,7 @@ static ssize_t list_and_choose(struct add_i_state *s,
if (from < 0 || from >= items->items.nr ||
(singleton && from + 1 != to)) {
- color_fprintf_ln(stderr, s->error_color,
+ color_fprintf_ln(stderr, s->cfg.error_color,
_("Huh (%s)?"), p);
break;
} else if (singleton) {
@@ -992,7 +890,7 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps,
free(files->items.items[i].string);
} else if (item->index.unmerged ||
item->worktree.unmerged) {
- color_fprintf_ln(stderr, s->error_color,
+ color_fprintf_ln(stderr, s->cfg.error_color,
_("ignoring unmerged: %s"),
files->items.items[i].string);
free(item);
@@ -1014,9 +912,9 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps,
opts->prompt = N_("Patch update");
count = list_and_choose(s, files, opts);
if (count > 0) {
- struct add_p_opt add_p_opt = {
- .context = s->context,
- .interhunkcontext = s->interhunkcontext,
+ struct interactive_options opts = {
+ .context = s->cfg.context,
+ .interhunkcontext = s->cfg.interhunkcontext,
};
struct strvec args = STRVEC_INIT;
struct pathspec ps_selected = { 0 };
@@ -1028,7 +926,7 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps,
parse_pathspec(&ps_selected,
PATHSPEC_ALL_MAGIC & ~PATHSPEC_LITERAL,
PATHSPEC_LITERAL_PATH, "", args.v);
- res = run_add_p(s->r, ADD_P_ADD, &add_p_opt, NULL, &ps_selected);
+ res = run_add_p(s->r, ADD_P_ADD, &opts, NULL, &ps_selected);
strvec_clear(&args);
clear_pathspec(&ps_selected);
}
@@ -1064,10 +962,10 @@ static int run_diff(struct add_i_state *s, const struct pathspec *ps,
struct child_process cmd = CHILD_PROCESS_INIT;
strvec_pushl(&cmd.args, "git", "diff", "-p", "--cached", NULL);
- if (s->context != -1)
- strvec_pushf(&cmd.args, "--unified=%i", s->context);
- if (s->interhunkcontext != -1)
- strvec_pushf(&cmd.args, "--inter-hunk-context=%i", s->interhunkcontext);
+ if (s->cfg.context != -1)
+ strvec_pushf(&cmd.args, "--unified=%i", s->cfg.context);
+ if (s->cfg.interhunkcontext != -1)
+ strvec_pushf(&cmd.args, "--inter-hunk-context=%i", s->cfg.interhunkcontext);
strvec_pushl(&cmd.args, oid_to_hex(!is_initial ? &oid :
s->r->hash_algo->empty_tree), "--", NULL);
for (i = 0; i < files->items.nr; i++)
@@ -1085,17 +983,17 @@ static int run_help(struct add_i_state *s, const struct pathspec *ps UNUSED,
struct prefix_item_list *files UNUSED,
struct list_and_choose_options *opts UNUSED)
{
- color_fprintf_ln(stdout, s->help_color, "status - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "status - %s",
_("show paths with changes"));
- color_fprintf_ln(stdout, s->help_color, "update - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "update - %s",
_("add working tree state to the staged set of changes"));
- color_fprintf_ln(stdout, s->help_color, "revert - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "revert - %s",
_("revert staged set of changes back to the HEAD version"));
- color_fprintf_ln(stdout, s->help_color, "patch - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "patch - %s",
_("pick hunks and update selectively"));
- color_fprintf_ln(stdout, s->help_color, "diff - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "diff - %s",
_("view diff between HEAD and index"));
- color_fprintf_ln(stdout, s->help_color, "add untracked - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "add untracked - %s",
_("add contents of untracked files to the staged set of changes"));
return 0;
@@ -1103,21 +1001,21 @@ static int run_help(struct add_i_state *s, const struct pathspec *ps UNUSED,
static void choose_prompt_help(struct add_i_state *s)
{
- color_fprintf_ln(stdout, s->help_color, "%s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "%s",
_("Prompt help:"));
- color_fprintf_ln(stdout, s->help_color, "1 - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "1 - %s",
_("select a single item"));
- color_fprintf_ln(stdout, s->help_color, "3-5 - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "3-5 - %s",
_("select a range of items"));
- color_fprintf_ln(stdout, s->help_color, "2-3,6-9 - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "2-3,6-9 - %s",
_("select multiple ranges"));
- color_fprintf_ln(stdout, s->help_color, "foo - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "foo - %s",
_("select item based on unique prefix"));
- color_fprintf_ln(stdout, s->help_color, "-... - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "-... - %s",
_("unselect specified items"));
- color_fprintf_ln(stdout, s->help_color, "* - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, "* - %s",
_("choose all items"));
- color_fprintf_ln(stdout, s->help_color, " - %s",
+ color_fprintf_ln(stdout, s->cfg.help_color, " - %s",
_("(empty) finish selecting"));
}
@@ -1152,7 +1050,7 @@ static void print_command_item(int i, int selected UNUSED,
static void command_prompt_help(struct add_i_state *s)
{
- const char *help_color = s->help_color;
+ const char *help_color = s->cfg.help_color;
color_fprintf_ln(stdout, help_color, "%s", _("Prompt help:"));
color_fprintf_ln(stdout, help_color, "1 - %s",
_("select a numbered item"));
@@ -1163,7 +1061,7 @@ static void command_prompt_help(struct add_i_state *s)
}
int run_add_i(struct repository *r, const struct pathspec *ps,
- struct add_p_opt *add_p_opt)
+ struct interactive_options *interactive_opts)
{
struct add_i_state s = { NULL };
struct print_command_item_data data = { "[", "]" };
@@ -1206,15 +1104,15 @@ int run_add_i(struct repository *r, const struct pathspec *ps,
->util = util;
}
- init_add_i_state(&s, r, add_p_opt);
+ init_add_i_state(&s, r, interactive_opts);
/*
* When color was asked for, use the prompt color for
* highlighting, otherwise use square brackets.
*/
- if (want_color(s.use_color_interactive)) {
- data.color = s.prompt_color;
- data.reset = s.reset_color_interactive;
+ if (want_color(s.cfg.use_color_interactive)) {
+ data.color = s.cfg.prompt_color;
+ data.reset = s.cfg.reset_color_interactive;
}
print_file_item_data.color = data.color;
print_file_item_data.reset = data.reset;
diff --git a/add-interactive.h b/add-interactive.h
index 2e3d1d871d..eefa2edc7c 100644
--- a/add-interactive.h
+++ b/add-interactive.h
@@ -2,37 +2,20 @@
#define ADD_INTERACTIVE_H
#include "add-patch.h"
-#include "color.h"
struct pathspec;
struct repository;
struct add_i_state {
struct repository *r;
- enum git_colorbool use_color_interactive;
- enum git_colorbool use_color_diff;
- char header_color[COLOR_MAXLEN];
- char help_color[COLOR_MAXLEN];
- char prompt_color[COLOR_MAXLEN];
- char error_color[COLOR_MAXLEN];
- char reset_color_interactive[COLOR_MAXLEN];
-
- char fraginfo_color[COLOR_MAXLEN];
- char context_color[COLOR_MAXLEN];
- char file_old_color[COLOR_MAXLEN];
- char file_new_color[COLOR_MAXLEN];
- char reset_color_diff[COLOR_MAXLEN];
-
- int use_single_key;
- char *interactive_diff_filter, *interactive_diff_algorithm;
- int context, interhunkcontext;
+ struct interactive_config cfg;
};
void init_add_i_state(struct add_i_state *s, struct repository *r,
- struct add_p_opt *add_p_opt);
+ struct interactive_options *opts);
void clear_add_i_state(struct add_i_state *s);
int run_add_i(struct repository *r, const struct pathspec *ps,
- struct add_p_opt *add_p_opt);
+ struct interactive_options *opts);
#endif
diff --git a/add-patch.c b/add-patch.c
index 5e3481083b..6797d2f690 100644
--- a/add-patch.c
+++ b/add-patch.c
@@ -5,6 +5,8 @@
#include "add-interactive.h"
#include "add-patch.h"
#include "advice.h"
+#include "config.h"
+#include "diff.h"
#include "editor.h"
#include "environment.h"
#include "gettext.h"
@@ -279,6 +281,122 @@ struct add_p_state {
const char *revision;
};
+static void init_color(struct repository *r,
+ enum git_colorbool use_color,
+ const char *section_and_slot, char *dst,
+ const char *default_color)
+{
+ char *key = xstrfmt("color.%s", section_and_slot);
+ const char *value;
+
+ if (!want_color(use_color))
+ dst[0] = '\0';
+ else if (repo_config_get_value(r, key, &value) ||
+ color_parse(value, dst))
+ strlcpy(dst, default_color, COLOR_MAXLEN);
+
+ free(key);
+}
+
+static enum git_colorbool check_color_config(struct repository *r, const char *var)
+{
+ const char *value;
+ enum git_colorbool ret;
+
+ if (repo_config_get_value(r, var, &value))
+ ret = GIT_COLOR_UNKNOWN;
+ else
+ ret = git_config_colorbool(var, value);
+
+ /*
+ * Do not rely on want_color() to fall back to color.ui for us. It uses
+ * the value parsed by git_color_config(), which may not have been
+ * called by the main command.
+ */
+ if (ret == GIT_COLOR_UNKNOWN &&
+ !repo_config_get_value(r, "color.ui", &value))
+ ret = git_config_colorbool("color.ui", value);
+
+ return ret;
+}
+
+void interactive_config_init(struct interactive_config *cfg,
+ struct repository *r,
+ struct interactive_options *opts)
+{
+ cfg->context = -1;
+ cfg->interhunkcontext = -1;
+
+ cfg->use_color_interactive = check_color_config(r, "color.interactive");
+
+ init_color(r, cfg->use_color_interactive, "interactive.header",
+ cfg->header_color, GIT_COLOR_BOLD);
+ init_color(r, cfg->use_color_interactive, "interactive.help",
+ cfg->help_color, GIT_COLOR_BOLD_RED);
+ init_color(r, cfg->use_color_interactive, "interactive.prompt",
+ cfg->prompt_color, GIT_COLOR_BOLD_BLUE);
+ init_color(r, cfg->use_color_interactive, "interactive.error",
+ cfg->error_color, GIT_COLOR_BOLD_RED);
+ strlcpy(cfg->reset_color_interactive,
+ want_color(cfg->use_color_interactive) ? GIT_COLOR_RESET : "", COLOR_MAXLEN);
+
+ cfg->use_color_diff = check_color_config(r, "color.diff");
+
+ init_color(r, cfg->use_color_diff, "diff.frag", cfg->fraginfo_color,
+ diff_get_color(cfg->use_color_diff, DIFF_FRAGINFO));
+ init_color(r, cfg->use_color_diff, "diff.context", cfg->context_color,
+ "fall back");
+ if (!strcmp(cfg->context_color, "fall back"))
+ init_color(r, cfg->use_color_diff, "diff.plain",
+ cfg->context_color,
+ diff_get_color(cfg->use_color_diff, DIFF_CONTEXT));
+ init_color(r, cfg->use_color_diff, "diff.old", cfg->file_old_color,
+ diff_get_color(cfg->use_color_diff, DIFF_FILE_OLD));
+ init_color(r, cfg->use_color_diff, "diff.new", cfg->file_new_color,
+ diff_get_color(cfg->use_color_diff, DIFF_FILE_NEW));
+ strlcpy(cfg->reset_color_diff,
+ want_color(cfg->use_color_diff) ? GIT_COLOR_RESET : "", COLOR_MAXLEN);
+
+ FREE_AND_NULL(cfg->interactive_diff_filter);
+ repo_config_get_string(r, "interactive.difffilter",
+ &cfg->interactive_diff_filter);
+
+ FREE_AND_NULL(cfg->interactive_diff_algorithm);
+ repo_config_get_string(r, "diff.algorithm",
+ &cfg->interactive_diff_algorithm);
+
+ if (!repo_config_get_int(r, "diff.context", &cfg->context))
+ if (cfg->context < 0)
+ die(_("%s cannot be negative"), "diff.context");
+ if (!repo_config_get_int(r, "diff.interHunkContext", &cfg->interhunkcontext))
+ if (cfg->interhunkcontext < 0)
+ die(_("%s cannot be negative"), "diff.interHunkContext");
+
+ repo_config_get_bool(r, "interactive.singlekey", &cfg->use_single_key);
+ if (cfg->use_single_key)
+ setbuf(stdin, NULL);
+
+ if (opts->context != -1) {
+ if (opts->context < 0)
+ die(_("%s cannot be negative"), "--unified");
+ cfg->context = opts->context;
+ }
+ if (opts->interhunkcontext != -1) {
+ if (opts->interhunkcontext < 0)
+ die(_("%s cannot be negative"), "--inter-hunk-context");
+ cfg->interhunkcontext = opts->interhunkcontext;
+ }
+}
+
+void interactive_config_clear(struct interactive_config *cfg)
+{
+ FREE_AND_NULL(cfg->interactive_diff_filter);
+ FREE_AND_NULL(cfg->interactive_diff_algorithm);
+ memset(cfg, 0, sizeof(*cfg));
+ cfg->use_color_interactive = GIT_COLOR_UNKNOWN;
+ cfg->use_color_diff = GIT_COLOR_UNKNOWN;
+}
+
static void add_p_state_clear(struct add_p_state *s)
{
size_t i;
@@ -299,9 +417,9 @@ static void err(struct add_p_state *s, const char *fmt, ...)
va_list args;
va_start(args, fmt);
- fputs(s->s.error_color, stdout);
+ fputs(s->s.cfg.error_color, stdout);
vprintf(fmt, args);
- puts(s->s.reset_color_interactive);
+ puts(s->s.cfg.reset_color_interactive);
va_end(args);
}
@@ -424,12 +542,12 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
int res;
strvec_pushv(&args, s->mode->diff_cmd);
- if (s->s.context != -1)
- strvec_pushf(&args, "--unified=%i", s->s.context);
- if (s->s.interhunkcontext != -1)
- strvec_pushf(&args, "--inter-hunk-context=%i", s->s.interhunkcontext);
- if (s->s.interactive_diff_algorithm)
- strvec_pushf(&args, "--diff-algorithm=%s", s->s.interactive_diff_algorithm);
+ if (s->s.cfg.context != -1)
+ strvec_pushf(&args, "--unified=%i", s->s.cfg.context);
+ if (s->s.cfg.interhunkcontext != -1)
+ strvec_pushf(&args, "--inter-hunk-context=%i", s->s.cfg.interhunkcontext);
+ if (s->s.cfg.interactive_diff_algorithm)
+ strvec_pushf(&args, "--diff-algorithm=%s", s->s.cfg.interactive_diff_algorithm);
if (s->revision) {
struct object_id oid;
strvec_push(&args,
@@ -458,9 +576,9 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
}
strbuf_complete_line(plain);
- if (want_color_fd(1, s->s.use_color_diff)) {
+ if (want_color_fd(1, s->s.cfg.use_color_diff)) {
struct child_process colored_cp = CHILD_PROCESS_INIT;
- const char *diff_filter = s->s.interactive_diff_filter;
+ const char *diff_filter = s->s.cfg.interactive_diff_filter;
setup_child_process(s, &colored_cp, NULL);
xsnprintf((char *)args.v[color_arg_index], 8, "--color");
@@ -693,7 +811,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
hunk->colored_end - hunk->colored_start);
return;
} else {
- strbuf_addstr(out, s->s.fraginfo_color);
+ strbuf_addstr(out, s->s.cfg.fraginfo_color);
p = s->colored.buf + header->colored_extra_start;
len = header->colored_extra_end
- header->colored_extra_start;
@@ -715,7 +833,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
if (len)
strbuf_add(out, p, len);
else if (colored)
- strbuf_addf(out, "%s\n", s->s.reset_color_diff);
+ strbuf_addf(out, "%s\n", s->s.cfg.reset_color_diff);
else
strbuf_addch(out, '\n');
}
@@ -1104,12 +1222,12 @@ static void recolor_hunk(struct add_p_state *s, struct hunk *hunk)
strbuf_addstr(&s->colored,
plain[current] == '-' ?
- s->s.file_old_color :
+ s->s.cfg.file_old_color :
plain[current] == '+' ?
- s->s.file_new_color :
- s->s.context_color);
+ s->s.cfg.file_new_color :
+ s->s.cfg.context_color);
strbuf_add(&s->colored, plain + current, eol - current);
- strbuf_addstr(&s->colored, s->s.reset_color_diff);
+ strbuf_addstr(&s->colored, s->s.cfg.reset_color_diff);
if (next > eol)
strbuf_add(&s->colored, plain + eol, next - eol);
current = next;
@@ -1238,7 +1356,7 @@ static int run_apply_check(struct add_p_state *s,
static int read_single_character(struct add_p_state *s)
{
- if (s->s.use_single_key) {
+ if (s->s.cfg.use_single_key) {
int res = read_key_without_echo(&s->answer);
printf("%s\n", res == EOF ? "" : s->answer.buf);
return res;
@@ -1252,7 +1370,7 @@ static int read_single_character(struct add_p_state *s)
static int prompt_yesno(struct add_p_state *s, const char *prompt)
{
for (;;) {
- color_fprintf(stdout, s->s.prompt_color, "%s", _(prompt));
+ color_fprintf(stdout, s->s.cfg.prompt_color, "%s", _(prompt));
fflush(stdout);
if (read_single_character(s) == EOF)
return -1;
@@ -1560,15 +1678,15 @@ static int patch_update_file(struct add_p_state *s,
else
prompt_mode_type = PROMPT_HUNK;
- printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->s.prompt_color,
+ printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->s.cfg.prompt_color,
(uintmax_t)hunk_index + 1,
(uintmax_t)(file_diff->hunk_nr
? file_diff->hunk_nr
: 1));
printf(_(s->mode->prompt_mode[prompt_mode_type]),
s->buf.buf);
- if (*s->s.reset_color_interactive)
- fputs(s->s.reset_color_interactive, stdout);
+ if (*s->s.cfg.reset_color_interactive)
+ fputs(s->s.cfg.reset_color_interactive, stdout);
fflush(stdout);
if (read_single_character(s) == EOF) {
quit = 1;
@@ -1731,7 +1849,7 @@ static int patch_update_file(struct add_p_state *s,
err(s, _("Sorry, cannot split this hunk"));
} else if (!split_hunk(s, file_diff,
hunk - file_diff->hunk)) {
- color_fprintf_ln(stdout, s->s.header_color,
+ color_fprintf_ln(stdout, s->s.cfg.header_color,
_("Split into %d hunks."),
(int)splittable_into);
rendered_hunk_index = -1;
@@ -1749,7 +1867,7 @@ static int patch_update_file(struct add_p_state *s,
} else if (s->answer.buf[0] == '?') {
const char *p = _(help_patch_remainder), *eol = p;
- color_fprintf(stdout, s->s.help_color, "%s",
+ color_fprintf(stdout, s->s.cfg.help_color, "%s",
_(s->mode->help_patch_text));
/*
@@ -1767,7 +1885,7 @@ static int patch_update_file(struct add_p_state *s,
if (*p != '?' && !strchr(s->buf.buf, *p))
continue;
- color_fprintf_ln(stdout, s->s.help_color,
+ color_fprintf_ln(stdout, s->s.cfg.help_color,
"%.*s", (int)(eol - p), p);
}
} else {
@@ -1808,7 +1926,7 @@ static int patch_update_file(struct add_p_state *s,
}
int run_add_p(struct repository *r, enum add_p_mode mode,
- struct add_p_opt *o, const char *revision,
+ struct interactive_options *opts, const char *revision,
const struct pathspec *ps)
{
struct add_p_state s = {
@@ -1816,7 +1934,7 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
};
size_t i, binary_count = 0;
- init_add_i_state(&s.s, r, o);
+ init_add_i_state(&s.s, r, opts);
if (mode == ADD_P_STASH)
s.mode = &patch_mode_stash;
diff --git a/add-patch.h b/add-patch.h
index 4394c74107..a4a05d9d14 100644
--- a/add-patch.h
+++ b/add-patch.h
@@ -1,15 +1,45 @@
#ifndef ADD_PATCH_H
#define ADD_PATCH_H
+#include "color.h"
+
struct pathspec;
struct repository;
-struct add_p_opt {
+struct interactive_options {
int context;
int interhunkcontext;
};
-#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }
+#define INTERACTIVE_OPTIONS_INIT { \
+ .context = -1, \
+ .interhunkcontext = -1, \
+}
+
+struct interactive_config {
+ enum git_colorbool use_color_interactive;
+ enum git_colorbool use_color_diff;
+ char header_color[COLOR_MAXLEN];
+ char help_color[COLOR_MAXLEN];
+ char prompt_color[COLOR_MAXLEN];
+ char error_color[COLOR_MAXLEN];
+ char reset_color_interactive[COLOR_MAXLEN];
+
+ char fraginfo_color[COLOR_MAXLEN];
+ char context_color[COLOR_MAXLEN];
+ char file_old_color[COLOR_MAXLEN];
+ char file_new_color[COLOR_MAXLEN];
+ char reset_color_diff[COLOR_MAXLEN];
+
+ int use_single_key;
+ char *interactive_diff_filter, *interactive_diff_algorithm;
+ int context, interhunkcontext;
+};
+
+void interactive_config_init(struct interactive_config *cfg,
+ struct repository *r,
+ struct interactive_options *opts);
+void interactive_config_clear(struct interactive_config *cfg);
enum add_p_mode {
ADD_P_ADD,
@@ -20,7 +50,7 @@ enum add_p_mode {
};
int run_add_p(struct repository *r, enum add_p_mode mode,
- struct add_p_opt *o, const char *revision,
+ struct interactive_options *opts, const char *revision,
const struct pathspec *ps);
#endif
diff --git a/builtin/add.c b/builtin/add.c
index 32709794b3..6f1e213052 100644
--- a/builtin/add.c
+++ b/builtin/add.c
@@ -31,7 +31,7 @@ static const char * const builtin_add_usage[] = {
NULL
};
static int patch_interactive, add_interactive, edit_interactive;
-static struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+static struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
static int take_worktree_changes;
static int add_renormalize;
static int pathspec_file_nul;
@@ -160,7 +160,7 @@ static int refresh(struct repository *repo, int verbose, const struct pathspec *
int interactive_add(struct repository *repo,
const char **argv,
const char *prefix,
- int patch, struct add_p_opt *add_p_opt)
+ int patch, struct interactive_options *interactive_opts)
{
struct pathspec pathspec;
int ret;
@@ -172,9 +172,9 @@ int interactive_add(struct repository *repo,
prefix, argv);
if (patch)
- ret = !!run_add_p(repo, ADD_P_ADD, add_p_opt, NULL, &pathspec);
+ ret = !!run_add_p(repo, ADD_P_ADD, interactive_opts, NULL, &pathspec);
else
- ret = !!run_add_i(repo, &pathspec, add_p_opt);
+ ret = !!run_add_i(repo, &pathspec, interactive_opts);
clear_pathspec(&pathspec);
return ret;
@@ -256,8 +256,8 @@ static struct option builtin_add_options[] = {
OPT_GROUP(""),
OPT_BOOL('i', "interactive", &add_interactive, N_("interactive picking")),
OPT_BOOL('p', "patch", &patch_interactive, N_("select hunks interactively")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT_BOOL('e', "edit", &edit_interactive, N_("edit current diff and apply")),
OPT__FORCE(&ignored_too, N_("allow adding otherwise ignored files"), 0),
OPT_BOOL('u', "update", &take_worktree_changes, N_("update tracked files")),
@@ -400,9 +400,9 @@ int cmd_add(int argc,
prepare_repo_settings(repo);
repo->settings.command_requires_full_index = 0;
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
if (patch_interactive)
@@ -412,11 +412,11 @@ int cmd_add(int argc,
die(_("options '%s' and '%s' cannot be used together"), "--dry-run", "--interactive/--patch");
if (pathspec_from_file)
die(_("options '%s' and '%s' cannot be used together"), "--pathspec-from-file", "--interactive/--patch");
- exit(interactive_add(repo, argv + 1, prefix, patch_interactive, &add_p_opt));
+ exit(interactive_add(repo, argv + 1, prefix, patch_interactive, &interactive_opts));
} else {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--interactive/--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--interactive/--patch");
}
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 66b69df6e6..530ae956ad 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -546,7 +546,7 @@ static int checkout_paths(const struct checkout_opts *opts,
if (opts->patch_mode) {
enum add_p_mode patch_mode;
- struct add_p_opt add_p_opt = {
+ struct interactive_options interactive_opts = {
.context = opts->patch_context,
.interhunkcontext = opts->patch_interhunk_context,
};
@@ -575,7 +575,7 @@ static int checkout_paths(const struct checkout_opts *opts,
else
BUG("either flag must have been set, worktree=%d, index=%d",
opts->checkout_worktree, opts->checkout_index);
- return !!run_add_p(the_repository, patch_mode, &add_p_opt,
+ return !!run_add_p(the_repository, patch_mode, &interactive_opts,
rev, &opts->pathspec);
}
diff --git a/builtin/commit.c b/builtin/commit.c
index 0243f17d53..640495cc57 100644
--- a/builtin/commit.c
+++ b/builtin/commit.c
@@ -123,7 +123,7 @@ static const char *edit_message, *use_message;
static char *fixup_message, *fixup_commit, *squash_message;
static const char *fixup_prefix;
static int all, also, interactive, patch_interactive, only, amend, signoff;
-static struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+static struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
static int edit_flag = -1; /* unspecified */
static int quiet, verbose, no_verify, allow_empty, dry_run, renew_authorship;
static int config_commit_verbose = -1; /* unspecified */
@@ -356,9 +356,9 @@ static const char *prepare_index(const char **argv, const char *prefix,
const char *ret;
char *path = NULL;
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
if (is_status)
@@ -407,7 +407,7 @@ static const char *prepare_index(const char **argv, const char *prefix,
old_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT));
setenv(INDEX_ENVIRONMENT, the_repository->index_file, 1);
- if (interactive_add(the_repository, argv, prefix, patch_interactive, &add_p_opt) != 0)
+ if (interactive_add(the_repository, argv, prefix, patch_interactive, &interactive_opts) != 0)
die(_("interactive add failed"));
the_repository->index_file = old_repo_index_file;
@@ -432,9 +432,9 @@ static const char *prepare_index(const char **argv, const char *prefix,
ret = get_lock_file_path(&index_lock);
goto out;
} else {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--interactive/--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--interactive/--patch");
}
@@ -1742,8 +1742,8 @@ int cmd_commit(int argc,
OPT_BOOL('i', "include", &also, N_("add specified files to index for commit")),
OPT_BOOL(0, "interactive", &interactive, N_("interactively add files")),
OPT_BOOL('p', "patch", &patch_interactive, N_("interactively add changes")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT_BOOL('o', "only", &only, N_("commit only specified files")),
OPT_BOOL('n', "no-verify", &no_verify, N_("bypass pre-commit and commit-msg hooks")),
OPT_BOOL(0, "dry-run", &dry_run, N_("show what would be committed")),
diff --git a/builtin/reset.c b/builtin/reset.c
index ed35802af1..088449e120 100644
--- a/builtin/reset.c
+++ b/builtin/reset.c
@@ -346,7 +346,7 @@ int cmd_reset(int argc,
struct object_id oid;
struct pathspec pathspec;
int intent_to_add = 0;
- struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
const struct option options[] = {
OPT__QUIET(&quiet, N_("be quiet, only report errors")),
OPT_BOOL(0, "no-refresh", &no_refresh,
@@ -371,8 +371,8 @@ int cmd_reset(int argc,
PARSE_OPT_OPTARG,
option_parse_recurse_submodules_worktree_updater),
OPT_BOOL('p', "patch", &patch_mode, N_("select hunks interactively")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT_BOOL('N', "intent-to-add", &intent_to_add,
N_("record only the fact that removed paths will be added later")),
OPT_PATHSPEC_FROM_FILE(&pathspec_from_file),
@@ -423,9 +423,9 @@ int cmd_reset(int argc,
oidcpy(&oid, &tree->object.oid);
}
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
prepare_repo_settings(the_repository);
@@ -436,12 +436,12 @@ int cmd_reset(int argc,
die(_("options '%s' and '%s' cannot be used together"), "--patch", "--{hard,mixed,soft}");
trace2_cmd_mode("patch-interactive");
update_ref_status = !!run_add_p(the_repository, ADD_P_RESET,
- &add_p_opt, rev, &pathspec);
+ &interactive_opts, rev, &pathspec);
goto cleanup;
} else {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--patch");
}
diff --git a/builtin/stash.c b/builtin/stash.c
index 948eba06fb..3b50905233 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -1306,7 +1306,7 @@ static int stash_staged(struct stash_info *info, struct strbuf *out_patch,
static int stash_patch(struct stash_info *info, const struct pathspec *ps,
struct strbuf *out_patch, int quiet,
- struct add_p_opt *add_p_opt)
+ struct interactive_options *interactive_opts)
{
int ret = 0;
struct child_process cp_read_tree = CHILD_PROCESS_INIT;
@@ -1331,7 +1331,7 @@ static int stash_patch(struct stash_info *info, const struct pathspec *ps,
old_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT));
setenv(INDEX_ENVIRONMENT, the_repository->index_file, 1);
- ret = !!run_add_p(the_repository, ADD_P_STASH, add_p_opt, NULL, ps);
+ ret = !!run_add_p(the_repository, ADD_P_STASH, interactive_opts, NULL, ps);
the_repository->index_file = old_repo_index_file;
if (old_index_env && *old_index_env)
@@ -1427,7 +1427,8 @@ static int stash_working_tree(struct stash_info *info, const struct pathspec *ps
}
static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_buf,
- int include_untracked, int patch_mode, struct add_p_opt *add_p_opt,
+ int include_untracked, int patch_mode,
+ struct interactive_options *interactive_opts,
int only_staged, struct stash_info *info, struct strbuf *patch,
int quiet)
{
@@ -1509,7 +1510,7 @@ static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_b
untracked_commit_option = 1;
}
if (patch_mode) {
- ret = stash_patch(info, ps, patch, quiet, add_p_opt);
+ ret = stash_patch(info, ps, patch, quiet, interactive_opts);
if (ret < 0) {
if (!quiet)
fprintf_ln(stderr, _("Cannot save the current "
@@ -1595,7 +1596,8 @@ static int create_stash(int argc, const char **argv, const char *prefix UNUSED,
}
static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int quiet,
- int keep_index, int patch_mode, struct add_p_opt *add_p_opt,
+ int keep_index, int patch_mode,
+ struct interactive_options *interactive_opts,
int include_untracked, int only_staged)
{
int ret = 0;
@@ -1667,7 +1669,7 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q
if (stash_msg)
strbuf_addstr(&stash_msg_buf, stash_msg);
if (do_create_stash(ps, &stash_msg_buf, include_untracked, patch_mode,
- add_p_opt, only_staged, &info, &patch, quiet)) {
+ interactive_opts, only_staged, &info, &patch, quiet)) {
ret = -1;
goto done;
}
@@ -1841,7 +1843,7 @@ static int push_stash(int argc, const char **argv, const char *prefix,
const char *stash_msg = NULL;
char *pathspec_from_file = NULL;
struct pathspec ps;
- struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
struct option options[] = {
OPT_BOOL('k', "keep-index", &keep_index,
N_("keep index")),
@@ -1849,8 +1851,8 @@ static int push_stash(int argc, const char **argv, const char *prefix,
N_("stash staged changes only")),
OPT_BOOL('p', "patch", &patch_mode,
N_("stash in patch mode")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT__QUIET(&quiet, N_("quiet mode")),
OPT_BOOL('u', "include-untracked", &include_untracked,
N_("include untracked files in stash")),
@@ -1907,19 +1909,19 @@ static int push_stash(int argc, const char **argv, const char *prefix,
}
if (!patch_mode) {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--patch");
}
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
ret = do_push_stash(&ps, stash_msg, quiet, keep_index, patch_mode,
- &add_p_opt, include_untracked, only_staged);
+ &interactive_opts, include_untracked, only_staged);
clear_pathspec(&ps);
free(pathspec_from_file);
@@ -1944,7 +1946,7 @@ static int save_stash(int argc, const char **argv, const char *prefix,
const char *stash_msg = NULL;
struct pathspec ps;
struct strbuf stash_msg_buf = STRBUF_INIT;
- struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
struct option options[] = {
OPT_BOOL('k', "keep-index", &keep_index,
N_("keep index")),
@@ -1952,8 +1954,8 @@ static int save_stash(int argc, const char **argv, const char *prefix,
N_("stash staged changes only")),
OPT_BOOL('p', "patch", &patch_mode,
N_("stash in patch mode")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT__QUIET(&quiet, N_("quiet mode")),
OPT_BOOL('u', "include-untracked", &include_untracked,
N_("include untracked files in stash")),
@@ -1973,20 +1975,20 @@ static int save_stash(int argc, const char **argv, const char *prefix,
memset(&ps, 0, sizeof(ps));
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
if (!patch_mode) {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--patch");
}
ret = do_push_stash(&ps, stash_msg, quiet, keep_index,
- patch_mode, &add_p_opt, include_untracked,
+ patch_mode, &interactive_opts, include_untracked,
only_staged);
strbuf_release(&stash_msg_buf);
diff --git a/commit.h b/commit.h
index 1d6e0c7518..7b6e59d6c1 100644
--- a/commit.h
+++ b/commit.h
@@ -258,7 +258,7 @@ int for_each_commit_graft(each_commit_graft_fn, void *);
int interactive_add(struct repository *repo,
const char **argv,
const char *prefix,
- int patch, struct add_p_opt *add_p_opt);
+ int patch, struct interactive_options *opts);
struct commit_extra_header {
struct commit_extra_header *next;
--
2.52.0.239.gd5f0c6e74e.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH v7 08/12] add-patch: remove dependency on "add-interactive" subsystem
2025-12-03 10:48 ` [PATCH v7 00/12] " Patrick Steinhardt
` (6 preceding siblings ...)
2025-12-03 10:48 ` [PATCH v7 07/12] add-patch: split out `struct interactive_options` Patrick Steinhardt
@ 2025-12-03 10:48 ` Patrick Steinhardt
2025-12-03 10:48 ` [PATCH v7 09/12] add-patch: add support for in-memory index patching Patrick Steinhardt
` (3 subsequent siblings)
11 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-12-03 10:48 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
With the preceding commit we have split out interactive configuration
that is used by both "git add -p" and "git add -i". But we still
initialize that configuration in the "add -p" subsystem by calling
`init_add_i_state()`, even though we only do so to initialize the
interactive configuration as well as a repository pointer.
Stop doing so and instead store and initialize the interactive
configuration in `struct add_p_state` directly.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
add-patch.c | 70 ++++++++++++++++++++++++++++++++-----------------------------
1 file changed, 37 insertions(+), 33 deletions(-)
diff --git a/add-patch.c b/add-patch.c
index 6797d2f690..2780738153 100644
--- a/add-patch.c
+++ b/add-patch.c
@@ -2,7 +2,6 @@
#define DISABLE_SIGN_COMPARE_WARNINGS
#include "git-compat-util.h"
-#include "add-interactive.h"
#include "add-patch.h"
#include "advice.h"
#include "config.h"
@@ -263,7 +262,8 @@ struct hunk {
};
struct add_p_state {
- struct add_i_state s;
+ struct repository *r;
+ struct interactive_config cfg;
struct strbuf answer, buf;
/* parsed diff */
@@ -408,7 +408,7 @@ static void add_p_state_clear(struct add_p_state *s)
for (i = 0; i < s->file_diff_nr; i++)
free(s->file_diff[i].hunk);
free(s->file_diff);
- clear_add_i_state(&s->s);
+ interactive_config_clear(&s->cfg);
}
__attribute__((format (printf, 2, 3)))
@@ -417,9 +417,9 @@ static void err(struct add_p_state *s, const char *fmt, ...)
va_list args;
va_start(args, fmt);
- fputs(s->s.cfg.error_color, stdout);
+ fputs(s->cfg.error_color, stdout);
vprintf(fmt, args);
- puts(s->s.cfg.reset_color_interactive);
+ puts(s->cfg.reset_color_interactive);
va_end(args);
}
@@ -437,7 +437,7 @@ static void setup_child_process(struct add_p_state *s,
cp->git_cmd = 1;
strvec_pushf(&cp->env,
- INDEX_ENVIRONMENT "=%s", s->s.r->index_file);
+ INDEX_ENVIRONMENT "=%s", s->r->index_file);
}
static int parse_range(const char **p,
@@ -542,12 +542,12 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
int res;
strvec_pushv(&args, s->mode->diff_cmd);
- if (s->s.cfg.context != -1)
- strvec_pushf(&args, "--unified=%i", s->s.cfg.context);
- if (s->s.cfg.interhunkcontext != -1)
- strvec_pushf(&args, "--inter-hunk-context=%i", s->s.cfg.interhunkcontext);
- if (s->s.cfg.interactive_diff_algorithm)
- strvec_pushf(&args, "--diff-algorithm=%s", s->s.cfg.interactive_diff_algorithm);
+ if (s->cfg.context != -1)
+ strvec_pushf(&args, "--unified=%i", s->cfg.context);
+ if (s->cfg.interhunkcontext != -1)
+ strvec_pushf(&args, "--inter-hunk-context=%i", s->cfg.interhunkcontext);
+ if (s->cfg.interactive_diff_algorithm)
+ strvec_pushf(&args, "--diff-algorithm=%s", s->cfg.interactive_diff_algorithm);
if (s->revision) {
struct object_id oid;
strvec_push(&args,
@@ -576,9 +576,9 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
}
strbuf_complete_line(plain);
- if (want_color_fd(1, s->s.cfg.use_color_diff)) {
+ if (want_color_fd(1, s->cfg.use_color_diff)) {
struct child_process colored_cp = CHILD_PROCESS_INIT;
- const char *diff_filter = s->s.cfg.interactive_diff_filter;
+ const char *diff_filter = s->cfg.interactive_diff_filter;
setup_child_process(s, &colored_cp, NULL);
xsnprintf((char *)args.v[color_arg_index], 8, "--color");
@@ -811,7 +811,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
hunk->colored_end - hunk->colored_start);
return;
} else {
- strbuf_addstr(out, s->s.cfg.fraginfo_color);
+ strbuf_addstr(out, s->cfg.fraginfo_color);
p = s->colored.buf + header->colored_extra_start;
len = header->colored_extra_end
- header->colored_extra_start;
@@ -833,7 +833,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
if (len)
strbuf_add(out, p, len);
else if (colored)
- strbuf_addf(out, "%s\n", s->s.cfg.reset_color_diff);
+ strbuf_addf(out, "%s\n", s->cfg.reset_color_diff);
else
strbuf_addch(out, '\n');
}
@@ -1222,12 +1222,12 @@ static void recolor_hunk(struct add_p_state *s, struct hunk *hunk)
strbuf_addstr(&s->colored,
plain[current] == '-' ?
- s->s.cfg.file_old_color :
+ s->cfg.file_old_color :
plain[current] == '+' ?
- s->s.cfg.file_new_color :
- s->s.cfg.context_color);
+ s->cfg.file_new_color :
+ s->cfg.context_color);
strbuf_add(&s->colored, plain + current, eol - current);
- strbuf_addstr(&s->colored, s->s.cfg.reset_color_diff);
+ strbuf_addstr(&s->colored, s->cfg.reset_color_diff);
if (next > eol)
strbuf_add(&s->colored, plain + eol, next - eol);
current = next;
@@ -1356,7 +1356,7 @@ static int run_apply_check(struct add_p_state *s,
static int read_single_character(struct add_p_state *s)
{
- if (s->s.cfg.use_single_key) {
+ if (s->cfg.use_single_key) {
int res = read_key_without_echo(&s->answer);
printf("%s\n", res == EOF ? "" : s->answer.buf);
return res;
@@ -1370,7 +1370,7 @@ static int read_single_character(struct add_p_state *s)
static int prompt_yesno(struct add_p_state *s, const char *prompt)
{
for (;;) {
- color_fprintf(stdout, s->s.cfg.prompt_color, "%s", _(prompt));
+ color_fprintf(stdout, s->cfg.prompt_color, "%s", _(prompt));
fflush(stdout);
if (read_single_character(s) == EOF)
return -1;
@@ -1678,15 +1678,15 @@ static int patch_update_file(struct add_p_state *s,
else
prompt_mode_type = PROMPT_HUNK;
- printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->s.cfg.prompt_color,
+ printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->cfg.prompt_color,
(uintmax_t)hunk_index + 1,
(uintmax_t)(file_diff->hunk_nr
? file_diff->hunk_nr
: 1));
printf(_(s->mode->prompt_mode[prompt_mode_type]),
s->buf.buf);
- if (*s->s.cfg.reset_color_interactive)
- fputs(s->s.cfg.reset_color_interactive, stdout);
+ if (*s->cfg.reset_color_interactive)
+ fputs(s->cfg.reset_color_interactive, stdout);
fflush(stdout);
if (read_single_character(s) == EOF) {
quit = 1;
@@ -1849,7 +1849,7 @@ static int patch_update_file(struct add_p_state *s,
err(s, _("Sorry, cannot split this hunk"));
} else if (!split_hunk(s, file_diff,
hunk - file_diff->hunk)) {
- color_fprintf_ln(stdout, s->s.cfg.header_color,
+ color_fprintf_ln(stdout, s->cfg.header_color,
_("Split into %d hunks."),
(int)splittable_into);
rendered_hunk_index = -1;
@@ -1867,7 +1867,7 @@ static int patch_update_file(struct add_p_state *s,
} else if (s->answer.buf[0] == '?') {
const char *p = _(help_patch_remainder), *eol = p;
- color_fprintf(stdout, s->s.cfg.help_color, "%s",
+ color_fprintf(stdout, s->cfg.help_color, "%s",
_(s->mode->help_patch_text));
/*
@@ -1885,7 +1885,7 @@ static int patch_update_file(struct add_p_state *s,
if (*p != '?' && !strchr(s->buf.buf, *p))
continue;
- color_fprintf_ln(stdout, s->s.cfg.help_color,
+ color_fprintf_ln(stdout, s->cfg.help_color,
"%.*s", (int)(eol - p), p);
}
} else {
@@ -1905,7 +1905,7 @@ static int patch_update_file(struct add_p_state *s,
strbuf_reset(&s->buf);
reassemble_patch(s, file_diff, 0, &s->buf);
- discard_index(s->s.r->index);
+ discard_index(s->r->index);
if (s->mode->apply_for_checkout)
apply_for_checkout(s, &s->buf,
s->mode->is_reverse);
@@ -1916,8 +1916,8 @@ static int patch_update_file(struct add_p_state *s,
NULL, 0, NULL, 0))
error(_("'git apply' failed"));
}
- if (repo_read_index(s->s.r) >= 0)
- repo_refresh_and_write_index(s->s.r, REFRESH_QUIET, 0,
+ if (repo_read_index(s->r) >= 0)
+ repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,
1, NULL, NULL, NULL);
}
@@ -1930,11 +1930,15 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
const struct pathspec *ps)
{
struct add_p_state s = {
- { r }, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
+ .r = r,
+ .answer = STRBUF_INIT,
+ .buf = STRBUF_INIT,
+ .plain = STRBUF_INIT,
+ .colored = STRBUF_INIT,
};
size_t i, binary_count = 0;
- init_add_i_state(&s.s, r, opts);
+ interactive_config_init(&s.cfg, r, opts);
if (mode == ADD_P_STASH)
s.mode = &patch_mode_stash;
--
2.52.0.239.gd5f0c6e74e.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH v7 09/12] add-patch: add support for in-memory index patching
2025-12-03 10:48 ` [PATCH v7 00/12] " Patrick Steinhardt
` (7 preceding siblings ...)
2025-12-03 10:48 ` [PATCH v7 08/12] add-patch: remove dependency on "add-interactive" subsystem Patrick Steinhardt
@ 2025-12-03 10:48 ` Patrick Steinhardt
2025-12-03 10:48 ` [PATCH v7 10/12] add-patch: allow disabling editing of hunks Patrick Steinhardt
` (2 subsequent siblings)
11 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-12-03 10:48 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
With `run_add_p()` callers have the ability to apply changes from a
specific revision to a repository's index. This infra supports several
different modes, like for example applying changes to the index,
working tree or both.
One feature that is missing though is the ability to apply changes to an
in-memory index different from the repository's index. Add a new
function `run_add_p_index()` to plug this gap.
This new function will be used in a subsequent commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
add-patch.c | 139 +++++++++++++++++++++++++++++++++++++++++++++++++++---------
add-patch.h | 8 ++++
2 files changed, 128 insertions(+), 19 deletions(-)
diff --git a/add-patch.c b/add-patch.c
index 2780738153..31d82a3e22 100644
--- a/add-patch.c
+++ b/add-patch.c
@@ -4,11 +4,13 @@
#include "git-compat-util.h"
#include "add-patch.h"
#include "advice.h"
+#include "commit.h"
#include "config.h"
#include "diff.h"
#include "editor.h"
#include "environment.h"
#include "gettext.h"
+#include "hex.h"
#include "object-name.h"
#include "pager.h"
#include "read-cache-ll.h"
@@ -47,7 +49,7 @@ static struct patch_mode patch_mode_add = {
N_("Stage mode change [y,n,q,a,d%s,?]? "),
N_("Stage deletion [y,n,q,a,d%s,?]? "),
N_("Stage addition [y,n,q,a,d%s,?]? "),
- N_("Stage this hunk [y,n,q,a,d%s,?]? ")
+ N_("Stage this hunk [y,n,q,a,d%s,?]? "),
},
.edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk "
"will immediately be marked for staging."),
@@ -263,6 +265,8 @@ struct hunk {
struct add_p_state {
struct repository *r;
+ struct index_state *index;
+ const char *index_file;
struct interactive_config cfg;
struct strbuf answer, buf;
@@ -437,7 +441,7 @@ static void setup_child_process(struct add_p_state *s,
cp->git_cmd = 1;
strvec_pushf(&cp->env,
- INDEX_ENVIRONMENT "=%s", s->r->index_file);
+ INDEX_ENVIRONMENT "=%s", s->index_file);
}
static int parse_range(const char **p,
@@ -1905,7 +1909,7 @@ static int patch_update_file(struct add_p_state *s,
strbuf_reset(&s->buf);
reassemble_patch(s, file_diff, 0, &s->buf);
- discard_index(s->r->index);
+ discard_index(s->index);
if (s->mode->apply_for_checkout)
apply_for_checkout(s, &s->buf,
s->mode->is_reverse);
@@ -1916,27 +1920,54 @@ static int patch_update_file(struct add_p_state *s,
NULL, 0, NULL, 0))
error(_("'git apply' failed"));
}
- if (repo_read_index(s->r) >= 0)
+ if (read_index_from(s->index, s->index_file, s->r->gitdir) >= 0 &&
+ s->index == s->r->index) {
repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,
1, NULL, NULL, NULL);
+ }
}
putchar('\n');
return quit;
}
+static int run_add_p_common(struct add_p_state *state,
+ const struct pathspec *ps)
+{
+ size_t binary_count = 0;
+
+ if (parse_diff(state, ps) < 0)
+ return -1;
+
+ for (size_t i = 0; i < state->file_diff_nr; i++) {
+ if (state->file_diff[i].binary && !state->file_diff[i].hunk_nr)
+ binary_count++;
+ else if (patch_update_file(state, state->file_diff + i))
+ break;
+ }
+
+ if (state->file_diff_nr == 0)
+ err(state, _("No changes."));
+ else if (binary_count == state->file_diff_nr)
+ err(state, _("Only binary files changed."));
+
+ return 0;
+}
+
int run_add_p(struct repository *r, enum add_p_mode mode,
struct interactive_options *opts, const char *revision,
const struct pathspec *ps)
{
struct add_p_state s = {
.r = r,
+ .index = r->index,
+ .index_file = r->index_file,
.answer = STRBUF_INIT,
.buf = STRBUF_INIT,
.plain = STRBUF_INIT,
.colored = STRBUF_INIT,
};
- size_t i, binary_count = 0;
+ int ret;
interactive_config_init(&s.cfg, r, opts);
@@ -1969,23 +2000,93 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
if (repo_read_index(r) < 0 ||
(!s.mode->index_only &&
repo_refresh_and_write_index(r, REFRESH_QUIET, 0, 1,
- NULL, NULL, NULL) < 0) ||
- parse_diff(&s, ps) < 0) {
- add_p_state_clear(&s);
- return -1;
+ NULL, NULL, NULL) < 0)) {
+ ret = -1;
+ goto out;
}
- for (i = 0; i < s.file_diff_nr; i++)
- if (s.file_diff[i].binary && !s.file_diff[i].hunk_nr)
- binary_count++;
- else if (patch_update_file(&s, s.file_diff + i))
- break;
+ ret = run_add_p_common(&s, ps);
+ if (ret < 0)
+ goto out;
- if (s.file_diff_nr == 0)
- err(&s, _("No changes."));
- else if (binary_count == s.file_diff_nr)
- err(&s, _("Only binary files changed."));
+ ret = 0;
+out:
add_p_state_clear(&s);
- return 0;
+ return ret;
+}
+
+int run_add_p_index(struct repository *r,
+ struct index_state *index,
+ const char *index_file,
+ struct interactive_options *opts,
+ const char *revision,
+ const struct pathspec *ps)
+{
+ struct patch_mode mode = {
+ .apply_args = { "--cached", NULL },
+ .apply_check_args = { "--cached", NULL },
+ .prompt_mode = {
+ N_("Stage mode change [y,n,q,a,d%s,?]? "),
+ N_("Stage deletion [y,n,q,a,d%s,?]? "),
+ N_("Stage addition [y,n,q,a,d%s,?]? "),
+ N_("Stage this hunk [y,n,q,a,d%s,?]? ")
+ },
+ .edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk "
+ "will immediately be marked for staging."),
+ .help_patch_text =
+ N_("y - stage this hunk\n"
+ "n - do not stage this hunk\n"
+ "q - quit; do not stage this hunk or any of the remaining "
+ "ones\n"
+ "a - stage this hunk and all later hunks in the file\n"
+ "d - do not stage this hunk or any of the later hunks in "
+ "the file\n"),
+ .index_only = 1,
+ };
+ struct add_p_state s = {
+ .r = r,
+ .index = index,
+ .index_file = index_file,
+ .answer = STRBUF_INIT,
+ .buf = STRBUF_INIT,
+ .plain = STRBUF_INIT,
+ .colored = STRBUF_INIT,
+ .mode = &mode,
+ .revision = revision,
+ };
+ struct strbuf parent_revision = STRBUF_INIT;
+ char parent_tree_oid[GIT_MAX_HEXSZ + 1];
+ struct commit *commit;
+ int ret;
+
+ commit = lookup_commit_reference_by_name(revision);
+ if (!commit) {
+ err(&s, _("Revision does not refer to a commit"));
+ ret = -1;
+ goto out;
+ }
+
+ if (commit->parents)
+ oid_to_hex_r(parent_tree_oid, get_commit_tree_oid(commit->parents->item));
+ else
+ oid_to_hex_r(parent_tree_oid, r->hash_algo->empty_tree);
+
+ strbuf_addf(&parent_revision, "%s~", revision);
+ mode.diff_cmd[0] = "diff-tree";
+ mode.diff_cmd[1] = "-r";
+ mode.diff_cmd[2] = parent_tree_oid;
+
+ interactive_config_init(&s.cfg, r, opts);
+
+ ret = run_add_p_common(&s, ps);
+ if (ret < 0)
+ goto out;
+
+ ret = 0;
+
+out:
+ strbuf_release(&parent_revision);
+ add_p_state_clear(&s);
+ return ret;
}
diff --git a/add-patch.h b/add-patch.h
index a4a05d9d14..901c42fd7b 100644
--- a/add-patch.h
+++ b/add-patch.h
@@ -3,6 +3,7 @@
#include "color.h"
+struct index_state;
struct pathspec;
struct repository;
@@ -53,4 +54,11 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
struct interactive_options *opts, const char *revision,
const struct pathspec *ps);
+int run_add_p_index(struct repository *r,
+ struct index_state *index,
+ const char *index_file,
+ struct interactive_options *opts,
+ const char *revision,
+ const struct pathspec *ps);
+
#endif
--
2.52.0.239.gd5f0c6e74e.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH v7 10/12] add-patch: allow disabling editing of hunks
2025-12-03 10:48 ` [PATCH v7 00/12] " Patrick Steinhardt
` (8 preceding siblings ...)
2025-12-03 10:48 ` [PATCH v7 09/12] add-patch: add support for in-memory index patching Patrick Steinhardt
@ 2025-12-03 10:48 ` Patrick Steinhardt
2025-12-03 10:48 ` [PATCH v7 11/12] cache-tree: allow writing in-memory index as tree Patrick Steinhardt
2025-12-03 10:48 ` [PATCH v7 12/12] builtin/history: implement "split" subcommand Patrick Steinhardt
11 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-12-03 10:48 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
The "add-patch" mode allows the user to edit hunks to apply custom
changes. This is incompatible with a new `git history split` command
that we're about to introduce in a subsequent commit, so we need a way
to disable this mode.
Add a new flag to disable editing hunks.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
add-interactive.c | 2 +-
add-patch.c | 21 +++++++++++++--------
add-patch.h | 11 +++++++++--
builtin/add.c | 2 +-
builtin/checkout.c | 2 +-
builtin/reset.c | 2 +-
builtin/stash.c | 2 +-
7 files changed, 27 insertions(+), 15 deletions(-)
diff --git a/add-interactive.c b/add-interactive.c
index 05d2e7eefe..d41e039bc3 100644
--- a/add-interactive.c
+++ b/add-interactive.c
@@ -926,7 +926,7 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps,
parse_pathspec(&ps_selected,
PATHSPEC_ALL_MAGIC & ~PATHSPEC_LITERAL,
PATHSPEC_LITERAL_PATH, "", args.v);
- res = run_add_p(s->r, ADD_P_ADD, &opts, NULL, &ps_selected);
+ res = run_add_p(s->r, ADD_P_ADD, &opts, NULL, &ps_selected, 0);
strvec_clear(&args);
clear_pathspec(&ps_selected);
}
diff --git a/add-patch.c b/add-patch.c
index 31d82a3e22..51a7b371a2 100644
--- a/add-patch.c
+++ b/add-patch.c
@@ -1565,7 +1565,8 @@ static bool get_first_undecided(const struct file_diff *file_diff, size_t *idx)
}
static int patch_update_file(struct add_p_state *s,
- struct file_diff *file_diff)
+ struct file_diff *file_diff,
+ unsigned flags)
{
size_t hunk_index = 0;
ssize_t i, undecided_previous, undecided_next, rendered_hunk_index = -1;
@@ -1666,7 +1667,8 @@ static int patch_update_file(struct add_p_state *s,
permitted |= ALLOW_SPLIT;
strbuf_addstr(&s->buf, ",s");
}
- if (hunk_index + 1 > file_diff->mode_change &&
+ if (!(flags & ADD_P_DISALLOW_EDIT) &&
+ hunk_index + 1 > file_diff->mode_change &&
!file_diff->deleted) {
permitted |= ALLOW_EDIT;
strbuf_addstr(&s->buf, ",e");
@@ -1932,7 +1934,8 @@ static int patch_update_file(struct add_p_state *s,
}
static int run_add_p_common(struct add_p_state *state,
- const struct pathspec *ps)
+ const struct pathspec *ps,
+ unsigned flags)
{
size_t binary_count = 0;
@@ -1942,7 +1945,7 @@ static int run_add_p_common(struct add_p_state *state,
for (size_t i = 0; i < state->file_diff_nr; i++) {
if (state->file_diff[i].binary && !state->file_diff[i].hunk_nr)
binary_count++;
- else if (patch_update_file(state, state->file_diff + i))
+ else if (patch_update_file(state, state->file_diff + i, flags))
break;
}
@@ -1956,7 +1959,8 @@ static int run_add_p_common(struct add_p_state *state,
int run_add_p(struct repository *r, enum add_p_mode mode,
struct interactive_options *opts, const char *revision,
- const struct pathspec *ps)
+ const struct pathspec *ps,
+ unsigned flags)
{
struct add_p_state s = {
.r = r,
@@ -2005,7 +2009,7 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
goto out;
}
- ret = run_add_p_common(&s, ps);
+ ret = run_add_p_common(&s, ps, flags);
if (ret < 0)
goto out;
@@ -2021,7 +2025,8 @@ int run_add_p_index(struct repository *r,
const char *index_file,
struct interactive_options *opts,
const char *revision,
- const struct pathspec *ps)
+ const struct pathspec *ps,
+ unsigned flags)
{
struct patch_mode mode = {
.apply_args = { "--cached", NULL },
@@ -2079,7 +2084,7 @@ int run_add_p_index(struct repository *r,
interactive_config_init(&s.cfg, r, opts);
- ret = run_add_p_common(&s, ps);
+ ret = run_add_p_common(&s, ps, flags);
if (ret < 0)
goto out;
diff --git a/add-patch.h b/add-patch.h
index 901c42fd7b..1facf19f96 100644
--- a/add-patch.h
+++ b/add-patch.h
@@ -50,15 +50,22 @@ enum add_p_mode {
ADD_P_WORKTREE,
};
+enum add_p_flags {
+ /* Disallow "editing" hunks. */
+ ADD_P_DISALLOW_EDIT = (1 << 0),
+};
+
int run_add_p(struct repository *r, enum add_p_mode mode,
struct interactive_options *opts, const char *revision,
- const struct pathspec *ps);
+ const struct pathspec *ps,
+ unsigned flags);
int run_add_p_index(struct repository *r,
struct index_state *index,
const char *index_file,
struct interactive_options *opts,
const char *revision,
- const struct pathspec *ps);
+ const struct pathspec *ps,
+ unsigned flags);
#endif
diff --git a/builtin/add.c b/builtin/add.c
index 6f1e213052..dfe9636079 100644
--- a/builtin/add.c
+++ b/builtin/add.c
@@ -172,7 +172,7 @@ int interactive_add(struct repository *repo,
prefix, argv);
if (patch)
- ret = !!run_add_p(repo, ADD_P_ADD, interactive_opts, NULL, &pathspec);
+ ret = !!run_add_p(repo, ADD_P_ADD, interactive_opts, NULL, &pathspec, 0);
else
ret = !!run_add_i(repo, &pathspec, interactive_opts);
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 530ae956ad..d325083ff3 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -576,7 +576,7 @@ static int checkout_paths(const struct checkout_opts *opts,
BUG("either flag must have been set, worktree=%d, index=%d",
opts->checkout_worktree, opts->checkout_index);
return !!run_add_p(the_repository, patch_mode, &interactive_opts,
- rev, &opts->pathspec);
+ rev, &opts->pathspec, 0);
}
repo_hold_locked_index(the_repository, &lock_file, LOCK_DIE_ON_ERROR);
diff --git a/builtin/reset.c b/builtin/reset.c
index 088449e120..008929bc7c 100644
--- a/builtin/reset.c
+++ b/builtin/reset.c
@@ -436,7 +436,7 @@ int cmd_reset(int argc,
die(_("options '%s' and '%s' cannot be used together"), "--patch", "--{hard,mixed,soft}");
trace2_cmd_mode("patch-interactive");
update_ref_status = !!run_add_p(the_repository, ADD_P_RESET,
- &interactive_opts, rev, &pathspec);
+ &interactive_opts, rev, &pathspec, 0);
goto cleanup;
} else {
if (interactive_opts.context != -1)
diff --git a/builtin/stash.c b/builtin/stash.c
index 3b50905233..eb8142565e 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -1331,7 +1331,7 @@ static int stash_patch(struct stash_info *info, const struct pathspec *ps,
old_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT));
setenv(INDEX_ENVIRONMENT, the_repository->index_file, 1);
- ret = !!run_add_p(the_repository, ADD_P_STASH, interactive_opts, NULL, ps);
+ ret = !!run_add_p(the_repository, ADD_P_STASH, interactive_opts, NULL, ps, 0);
the_repository->index_file = old_repo_index_file;
if (old_index_env && *old_index_env)
--
2.52.0.239.gd5f0c6e74e.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH v7 11/12] cache-tree: allow writing in-memory index as tree
2025-12-03 10:48 ` [PATCH v7 00/12] " Patrick Steinhardt
` (9 preceding siblings ...)
2025-12-03 10:48 ` [PATCH v7 10/12] add-patch: allow disabling editing of hunks Patrick Steinhardt
@ 2025-12-03 10:48 ` Patrick Steinhardt
2025-12-03 10:48 ` [PATCH v7 12/12] builtin/history: implement "split" subcommand Patrick Steinhardt
11 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-12-03 10:48 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
The function `write_in_core_index_as_tree()` takes a repository and
writes its index into a tree object. What this function cannot do though
is to take an _arbitrary_ in-memory index.
Introduce a new `struct index_state` parameter so that the caller can
pass a different index than the one belonging to the repository. This
will be used in a subsequent commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
builtin/checkout.c | 3 ++-
cache-tree.c | 4 ++--
cache-tree.h | 3 ++-
3 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/builtin/checkout.c b/builtin/checkout.c
index d325083ff3..aa00f0f483 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -902,7 +902,8 @@ static int merge_working_tree(const struct checkout_opts *opts,
0);
init_ui_merge_options(&o, the_repository);
o.verbosity = 0;
- work = write_in_core_index_as_tree(the_repository);
+ work = write_in_core_index_as_tree(the_repository,
+ the_repository->index);
ret = reset_tree(new_tree,
opts, 1,
diff --git a/cache-tree.c b/cache-tree.c
index 2d8947b518..2976092270 100644
--- a/cache-tree.c
+++ b/cache-tree.c
@@ -723,11 +723,11 @@ static int write_index_as_tree_internal(struct object_id *oid,
return 0;
}
-struct tree* write_in_core_index_as_tree(struct repository *repo) {
+struct tree *write_in_core_index_as_tree(struct repository *repo,
+ struct index_state *index_state) {
struct object_id o;
int was_valid, ret;
- struct index_state *index_state = repo->index;
was_valid = index_state->cache_tree &&
cache_tree_fully_valid(index_state->cache_tree);
diff --git a/cache-tree.h b/cache-tree.h
index b82c4963e7..f8bddae523 100644
--- a/cache-tree.h
+++ b/cache-tree.h
@@ -47,7 +47,8 @@ int cache_tree_verify(struct repository *, struct index_state *);
#define WRITE_TREE_UNMERGED_INDEX (-2)
#define WRITE_TREE_PREFIX_ERROR (-3)
-struct tree* write_in_core_index_as_tree(struct repository *repo);
+struct tree *write_in_core_index_as_tree(struct repository *repo,
+ struct index_state *index_state);
int write_index_as_tree(struct object_id *oid, struct index_state *index_state, const char *index_path, int flags, const char *prefix);
void prime_cache_tree(struct repository *, struct index_state *, struct tree *);
--
2.52.0.239.gd5f0c6e74e.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread* [PATCH v7 12/12] builtin/history: implement "split" subcommand
2025-12-03 10:48 ` [PATCH v7 00/12] " Patrick Steinhardt
` (10 preceding siblings ...)
2025-12-03 10:48 ` [PATCH v7 11/12] cache-tree: allow writing in-memory index as tree Patrick Steinhardt
@ 2025-12-03 10:48 ` Patrick Steinhardt
11 siblings, 0 replies; 278+ messages in thread
From: Patrick Steinhardt @ 2025-12-03 10:48 UTC (permalink / raw)
To: git
Cc: D. Ben Knoble, Junio C Hamano, Sergey Organov,
Jean-Noël AVILA, Martin von Zweigbergk, Kristoffer Haugsbakk,
Elijah Newren, Karthik Nayak
It is quite a common use case that one wants to split up one commit into
multiple commits by moving parts of the changes of the original commit
out into a separate commit. This is quite an involved operation though:
1. Identify the commit in question that is to be dropped.
2. Perform an interactive rebase on top of that commit's parent.
3. Modify the instruction sheet to "edit" the commit that is to be
split up.
4. Drop the commit via "git reset HEAD~".
5. Stage changes that should go into the first commit and commit it.
6. Stage changes that should go into the second commit and commit it.
7. Finalize the rebase.
This is quite complex, and overall I would claim that most people who
are not experts in Git would struggle with this flow.
Introduce a new "split" subcommand for git-history(1) to make this way
easier. All the user needs to do is to say `git history split $COMMIT`.
From hereon, Git asks the user which parts of the commit shall be moved
out into a separate commit and, once done, asks the user for the commit
message. Git then creates that split-out commit and applies the original
commit on top of it.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-history.adoc | 62 ++++++
builtin/history.c | 174 ++++++++++++++++
t/meson.build | 1 +
t/t3452-history-split.sh | 452 +++++++++++++++++++++++++++++++++++++++++
4 files changed, 689 insertions(+)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index 160bf5d4d2..f1252baa2f 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -9,6 +9,7 @@ SYNOPSIS
--------
[synopsis]
git history reword <commit>
+git history split <commit> [--] [<pathspec>...]
DESCRIPTION
-----------
@@ -37,11 +38,72 @@ Several commands are available to rewrite history in different ways:
details of this commit remain unchanged. This command will spawn an
editor with the current message of that commit.
+`split <commit> [--] [<pathspec>...]`::
+ Interactively split up <commit> into two commits by choosing
+ hunks introduced by it that will be moved into the new split-out
+ commit. These hunks will then be written into a new commit that
+ becomes the parent of the previous commit. The original commit
+ stays intact, except that its parent will be the newly split-out
+ commit.
++
+The commit messages of the split-up commits will be asked for by launching
+the configured editor. Authorship of the commit will be the same as for the
+original commit.
++
+If passed, _<pathspec>_ can be used to limit which changes shall be split out
+of the original commit. Files not matching any of the pathspecs will remain
+part of the original commit. For more details, see the 'pathspec' entry in
+linkgit:gitglossary[7].
++
+It is invalid to select either all or no hunks, as that would lead to
+one of the commits becoming empty.
+
CONFIGURATION
-------------
include::includes/cmd-config-section-all.adoc[]
+EXAMPLES
+--------
+
+Split a commit
+~~~~~~~~~~~~~~
+
+----------
+$ git log --stat --oneline
+3f81232 (HEAD -> main) original
+ bar | 1 +
+ foo | 1 +
+ 2 files changed, 2 insertions(+)
+
+$ git history split HEAD
+diff --git a/bar b/bar
+new file mode 100644
+index 0000000..5716ca5
+--- /dev/null
++++ b/bar
+@@ -0,0 +1 @@
++bar
+(1/1) Stage addition [y,n,q,a,d,e,p,?]? y
+
+diff --git a/foo b/foo
+new file mode 100644
+index 0000000..257cc56
+--- /dev/null
++++ b/foo
+@@ -0,0 +1 @@
++foo
+(1/1) Stage addition [y,n,q,a,d,e,p,?]? n
+
+$ git log --stat --oneline
+7cebe64 (HEAD -> main) original
+ foo | 1 +
+ 1 file changed, 1 insertion(+)
+d1582f3 split-out commit
+ bar | 1 +
+ 1 file changed, 1 insertion(+)
+----------
+
GIT
---
Part of the linkgit:git[1] suite
diff --git a/builtin/history.c b/builtin/history.c
index 17bb150b95..c5262d964a 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -1,6 +1,7 @@
#define USE_THE_REPOSITORY_VARIABLE
#include "builtin.h"
+#include "cache-tree.h"
#include "commit-reach.h"
#include "commit.h"
#include "config.h"
@@ -8,17 +9,22 @@
#include "environment.h"
#include "gettext.h"
#include "hex.h"
+#include "oidmap.h"
#include "parse-options.h"
+#include "path.h"
+#include "read-cache.h"
#include "refs.h"
#include "replay.h"
#include "reset.h"
#include "revision.h"
+#include "run-command.h"
#include "sequencer.h"
#include "strvec.h"
#include "tree.h"
#include "wt-status.h"
#define GIT_HISTORY_REWORD_USAGE N_("git history reword <commit>")
+#define GIT_HISTORY_SPLIT_USAGE N_("git history split <commit> [--] [<pathspec>...]")
static int collect_commits(struct repository *repo,
struct commit *old_commit,
@@ -364,6 +370,172 @@ static int cmd_history_reword(int argc,
return ret;
}
+static int split_commit(struct repository *repo,
+ struct commit *original_commit,
+ struct pathspec *pathspec,
+ struct object_id *out)
+{
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
+ struct strbuf index_file = STRBUF_INIT;
+ struct child_process read_tree_cmd = CHILD_PROCESS_INIT;
+ struct index_state index = INDEX_STATE_INIT(repo);
+ struct object_id original_commit_tree_oid, parent_tree_oid;
+ char original_commit_oid[GIT_MAX_HEXSZ + 1];
+ struct commit_list *parents = NULL;
+ struct commit *first_commit;
+ struct tree *split_tree;
+ int ret;
+
+ if (original_commit->parents)
+ parent_tree_oid = *get_commit_tree_oid(original_commit->parents->item);
+ else
+ oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
+ original_commit_tree_oid = *get_commit_tree_oid(original_commit);
+
+ /*
+ * Construct the first commit. This is done by taking the original
+ * commit parent's tree and selectively patching changes from the diff
+ * between that parent and its child.
+ */
+ repo_git_path_replace(repo, &index_file, "%s", "history-split.index");
+
+ read_tree_cmd.git_cmd = 1;
+ strvec_pushf(&read_tree_cmd.env, "GIT_INDEX_FILE=%s", index_file.buf);
+ strvec_push(&read_tree_cmd.args, "read-tree");
+ strvec_push(&read_tree_cmd.args, oid_to_hex(&parent_tree_oid));
+ ret = run_command(&read_tree_cmd);
+ if (ret < 0)
+ goto out;
+
+ ret = read_index_from(&index, index_file.buf, repo->gitdir);
+ if (ret < 0) {
+ ret = error(_("failed reading temporary index"));
+ goto out;
+ }
+
+ oid_to_hex_r(original_commit_oid, &original_commit->object.oid);
+ ret = run_add_p_index(repo, &index, index_file.buf, &interactive_opts,
+ original_commit_oid, pathspec, ADD_P_DISALLOW_EDIT);
+ if (ret < 0)
+ goto out;
+
+ split_tree = write_in_core_index_as_tree(repo, &index);
+ if (!split_tree) {
+ ret = error(_("failed split tree"));
+ goto out;
+ }
+
+ unlink(index_file.buf);
+
+ /*
+ * We disallow the cases where either the split-out commit or the
+ * original commit would become empty. Consequently, if we see that the
+ * new tree ID matches either of those trees we abort.
+ */
+ if (oideq(&split_tree->object.oid, &parent_tree_oid)) {
+ ret = error(_("split commit is empty"));
+ goto out;
+ } else if (oideq(&split_tree->object.oid, &original_commit_tree_oid)) {
+ ret = error(_("split commit tree matches original commit"));
+ goto out;
+ }
+
+ /*
+ * The first commit is constructed from the split-out tree. The base
+ * that shall be diffed against is the parent of the original commit.
+ */
+ ret = commit_tree_with_edited_message(repo, "split-out", original_commit,
+ &split_tree->object.oid,
+ original_commit->parents, &parent_tree_oid, &out[0]);
+ if (ret < 0) {
+ ret = error(_("failed writing split-out commit"));
+ goto out;
+ }
+
+ /*
+ * The second commit is constructed from the original tree. The base to
+ * diff against and the parent in this case is the first split-out
+ * commit.
+ */
+ first_commit = lookup_commit_reference(repo, &out[0]);
+ commit_list_append(first_commit, &parents);
+
+ ret = commit_tree_with_edited_message(repo, "split-out", original_commit,
+ &original_commit_tree_oid,
+ parents, get_commit_tree_oid(first_commit), &out[1]);
+ if (ret < 0) {
+ ret = error(_("failed writing split-out commit"));
+ goto out;
+ }
+
+ ret = 0;
+
+out:
+ if (index_file.len)
+ unlink(index_file.buf);
+ strbuf_release(&index_file);
+ free_commit_list(parents);
+ release_index(&index);
+ return ret;
+}
+
+static int cmd_history_split(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo)
+{
+ const char * const usage[] = {
+ GIT_HISTORY_SPLIT_USAGE,
+ NULL,
+ };
+ struct option options[] = {
+ OPT_END(),
+ };
+ struct commit *original_commit, *parent, *head;
+ struct strvec commits = STRVEC_INIT;
+ struct object_id split_commits[2];
+ struct pathspec pathspec = { 0 };
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc < 1) {
+ ret = error(_("command expects a revision"));
+ goto out;
+ }
+ repo_config(repo, git_default_config, NULL);
+
+ parse_pathspec(&pathspec, 0,
+ PATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,
+ prefix, argv + 1);
+
+ ret = gather_commits_between_head_and_revision(repo, argv[0], &original_commit,
+ &parent, &head, &commits);
+ if (ret < 0)
+ goto out;
+
+ /*
+ * Then we split up the commit and replace the original commit with the
+ * new ones.
+ */
+ ret = split_commit(repo, original_commit, &pathspec, split_commits);
+ if (ret < 0)
+ goto out;
+
+ replace_commits(&commits, &original_commit->object.oid,
+ split_commits, ARRAY_SIZE(split_commits));
+
+ ret = apply_commits(repo, &commits, parent, head, "split");
+ if (ret < 0)
+ goto out;
+
+ ret = 0;
+
+out:
+ clear_pathspec(&pathspec);
+ strvec_clear(&commits);
+ return ret;
+}
+
int cmd_history(int argc,
const char **argv,
const char *prefix,
@@ -371,11 +543,13 @@ int cmd_history(int argc,
{
const char * const usage[] = {
GIT_HISTORY_REWORD_USAGE,
+ GIT_HISTORY_SPLIT_USAGE,
NULL,
};
parse_opt_subcommand_fn *fn = NULL;
struct option options[] = {
OPT_SUBCOMMAND("reword", &fn, cmd_history_reword),
+ OPT_SUBCOMMAND("split", &fn, cmd_history_split),
OPT_END(),
};
diff --git a/t/meson.build b/t/meson.build
index e187aef31e..1eef433207 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -388,6 +388,7 @@ integration_tests = [
't3438-rebase-broken-files.sh',
't3450-history.sh',
't3451-history-reword.sh',
+ 't3452-history-split.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
diff --git a/t/t3452-history-split.sh b/t/t3452-history-split.sh
new file mode 100755
index 0000000000..02ae86a7e6
--- /dev/null
+++ b/t/t3452-history-split.sh
@@ -0,0 +1,452 @@
+#!/bin/sh
+
+test_description='tests for git-history split subcommand'
+
+. ./test-lib.sh
+
+# The fake editor takes multiple arguments, each of which represents a commit
+# message. Subsequent invocations of the editor will then yield those messages
+# in order.
+#
+set_fake_editor () {
+ printf "%s\n" "$@" >fake-input &&
+ write_script fake-editor.sh <<-\EOF &&
+ head -n1 fake-input >"$1"
+ sed 1d fake-input >fake-input.trimmed &&
+ mv fake-input.trimmed fake-input
+ EOF
+ test_set_editor "$(pwd)"/fake-editor.sh
+}
+
+expect_log () {
+ git log --format="%s" >actual &&
+ cat >expect &&
+ test_cmp expect actual
+}
+
+expect_tree_entries () {
+ git ls-tree --name-only "$1" >actual &&
+ cat >expect &&
+ test_cmp expect actual
+}
+
+test_expect_success 'refuses to work with merge commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ git branch branch &&
+ test_commit ours &&
+ git switch branch &&
+ test_commit theirs &&
+ git switch - &&
+ git merge theirs &&
+ test_must_fail git history split HEAD 2>err &&
+ test_grep "cannot rearrange commit history with merges" err &&
+ test_must_fail git history split HEAD~ 2>err &&
+ test_grep "cannot rearrange commit history with merges" err
+ )
+'
+
+test_expect_success 'refuses to work with unrelated commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ git branch branch &&
+ test_commit ours &&
+ git switch branch &&
+ test_commit theirs &&
+ test_must_fail git history split ours 2>err &&
+ test_grep "commit must be reachable from current HEAD commit" err
+ )
+'
+
+test_expect_success 'can split up tip commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit initial &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ git symbolic-ref HEAD >expect &&
+ set_fake_editor "first" "second" &&
+ git history split HEAD <<-EOF &&
+ y
+ n
+ EOF
+ git symbolic-ref HEAD >actual &&
+ test_cmp expect actual &&
+
+ expect_log <<-EOF &&
+ second
+ first
+ initial
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ bar
+ initial.t
+ EOF
+
+ expect_tree_entries HEAD <<-EOF &&
+ bar
+ foo
+ initial.t
+ EOF
+
+ git reflog >reflog &&
+ test_grep "split: updating HEAD" reflog
+ )
+'
+
+test_expect_success 'can split up root commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m root &&
+ test_commit tip &&
+
+ set_fake_editor "first" "second" &&
+ git history split HEAD~ <<-EOF &&
+ y
+ n
+ EOF
+
+ expect_log <<-EOF &&
+ tip
+ second
+ first
+ EOF
+
+ expect_tree_entries HEAD~2 <<-EOF &&
+ bar
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ bar
+ foo
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ tip.t
+ EOF
+ )
+'
+
+test_expect_success 'can split up in-between commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit initial &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+ test_commit tip &&
+
+ set_fake_editor "first" "second" &&
+ git history split HEAD~ <<-EOF &&
+ y
+ n
+ EOF
+
+ expect_log <<-EOF &&
+ tip
+ second
+ first
+ initial
+ EOF
+
+ expect_tree_entries HEAD~2 <<-EOF &&
+ bar
+ initial.t
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ bar
+ foo
+ initial.t
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ initial.t
+ tip.t
+ EOF
+ )
+'
+
+test_expect_success 'can pick multiple hunks' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar baz foo qux &&
+ git add . &&
+ git commit -m split-me &&
+
+ set_fake_editor "first" "second" &&
+ git history split HEAD <<-EOF &&
+ y
+ n
+ y
+ n
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ bar
+ foo
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ baz
+ foo
+ qux
+ EOF
+ )
+'
+
+
+test_expect_success 'can use only last hunk' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ set_fake_editor "first" "second" &&
+ git history split HEAD <<-EOF &&
+ n
+ y
+ EOF
+
+ expect_log <<-EOF &&
+ second
+ first
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ foo
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ EOF
+ )
+'
+
+test_expect_success 'aborts with empty commit message' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ set_fake_editor "" &&
+ test_must_fail git history split HEAD <<-EOF 2>err &&
+ y
+ n
+ EOF
+ test_grep "Aborting commit due to empty commit message." err
+ )
+'
+
+test_expect_success 'commit message editor sees split-out changes' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ write_script fake-editor.sh <<-\EOF &&
+ cat "$1" >>MESSAGES &&
+ echo "some commit message" >"$1"
+ EOF
+ test_set_editor "$(pwd)"/fake-editor.sh &&
+
+ git history split HEAD <<-EOF &&
+ y
+ n
+ EOF
+
+ # Note that we expect to see the messages twice, once for each
+ # of the commits. The committed files are different though.
+ cat >expect <<-EOF &&
+ split-me
+
+ # Please enter the commit message for the split-out changes. Lines starting
+ # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
+ # Changes to be committed:
+ # new file: bar
+ #
+ split-me
+
+ # Please enter the commit message for the split-out changes. Lines starting
+ # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
+ # Changes to be committed:
+ # new file: foo
+ #
+ EOF
+ test_cmp expect MESSAGES &&
+
+ expect_log <<-EOF
+ some commit message
+ some commit message
+ EOF
+ )
+'
+
+test_expect_success 'can use pathspec to limit what gets split' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ set_fake_editor "first" "second" &&
+ git history split HEAD -- foo <<-EOF &&
+ y
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ foo
+ EOF
+
+ expect_tree_entries HEAD <<-EOF
+ bar
+ foo
+ EOF
+ )
+'
+
+test_expect_success 'refuses to create empty split-out commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ test_must_fail git history split HEAD 2>err <<-EOF &&
+ n
+ n
+ EOF
+ test_grep "split commit is empty" err
+ )
+'
+
+test_expect_success 'hooks are not executed for rewritten commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+ old_head=$(git rev-parse HEAD) &&
+
+ ORIG_PATH="$(pwd)" &&
+ export ORIG_PATH &&
+ for hook in prepare-commit-msg pre-commit post-commit post-rewrite commit-msg
+ do
+ write_script .git/hooks/$hook <<-\EOF || exit 1
+ touch "$ORIG_PATH/hooks.log
+ EOF
+ done &&
+
+ set_fake_editor "first" "second" &&
+ git history split HEAD <<-EOF &&
+ y
+ n
+ EOF
+
+ expect_log <<-EOF &&
+ second
+ first
+ EOF
+
+ test_path_is_missing hooks.log
+ )
+'
+
+test_expect_success 'refuses to create empty original commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+
+ test_must_fail git history split HEAD 2>err <<-EOF &&
+ y
+ y
+ EOF
+ test_grep "split commit tree matches original commit" err
+ )
+'
+
+test_expect_success 'retains changes in the worktree and index' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ echo a >a &&
+ echo b >b &&
+ git add . &&
+ git commit -m "initial commit" &&
+ echo a-modified >a &&
+ echo b-modified >b &&
+ git add b &&
+ set_fake_editor "a-only" "remainder" &&
+ git history split HEAD <<-EOF &&
+ y
+ n
+ EOF
+
+ expect_tree_entries HEAD~ <<-EOF &&
+ a
+ EOF
+ expect_tree_entries HEAD <<-EOF &&
+ a
+ b
+ EOF
+
+ cat >expect <<-\EOF &&
+ M a
+ M b
+ ?? actual
+ ?? expect
+ ?? fake-editor.sh
+ ?? fake-input
+ EOF
+ git status --porcelain >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_done
--
2.52.0.239.gd5f0c6e74e.dirty
^ permalink raw reply related [flat|nested] 278+ messages in thread