builtin: add new "history" command

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. Also, some operations like splitting up a commit into two are way more involved than that and require a whole series of commands. Rebases also do not update dependent branches. The use of stacked branches has grown quite common with competing version control systems like Jujutsu though, so it clearly is a need that users have. While rebases _can_ serve this use case if one always works on the latest stacked branch, it is somewhat awkward and very easy to get wrong. Add a new "history" command to plug these gaps. This command will have several different subcommands to imperatively rewrite history for common use cases like the above. Signed-off-by: Patrick Steinhardt <ps@pks.im> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Patrick Steinhardt committed Jan 13, 2026 at 10:54 UTC a675183d483cb106c076395936cd9e602ae94404
11 files changed +104
.gitignore
+1
@@ -79,6 +79,7 @@
79 /git-grep
80 /git-hash-object
81 /git-help
82 +/git-history
83 /git-hook
84 /git-http-backend
85 /git-http-fetch
Documentation/git-history.adoc new
+57
@@ -0,0 +1,57 @@
1 +git-history(1)
2 +==============
3 +
4 +NAME
5 +----
6 +git-history - EXPERIMENTAL: Rewrite history
7 +
8 +SYNOPSIS
9 +--------
10 +[synopsis]
11 +git history [<options>]
12 +
13 +DESCRIPTION
14 +-----------
15 +
16 +Rewrite history by rearranging or modifying specific commits in the
17 +history.
18 +
19 +THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
20 +
21 +This command is related to linkgit:git-rebase[1] in that both commands can be
22 +used to rewrite history. There are a couple of major differences though:
23 +
24 +* linkgit:git-history[1] can work in a bare repository as it does not need to
25 + touch either the index or the worktree.
26 +* linkgit:git-history[1] does not execute any linkgit:githooks[5] at the
27 + current point in time. This may change in the future.
28 +* linkgit:git-history[1] by default updates all branches that are descendants
29 + of the original commit to point to the rewritten commit.
30 +
31 +Overall, linkgit:git-history[1] aims to provide a more opinionated way to modify
32 +your commit history that is simpler to use compared to linkgit:git-rebase[1] in
33 +general.
34 +
35 +Use linkgit:git-rebase[1] if you want to reapply a range of commits onto a
36 +different base, or interactive rebases if you want to edit a range of commits
37 +at once.
38 +
39 +LIMITATIONS
40 +-----------
41 +
42 +This command does not (yet) work with histories that contain merges. You
43 +should use linkgit:git-rebase[1] with the `--rebase-merges` flag instead.
44 +
45 +Furthermore, the command does not support operations that can result in merge
46 +conflicts. This limitation is by design as history rewrites are not intended to
47 +be stateful operations. The limitation can be lifted once (if) Git learns about
48 +first-class conflicts.
49 +
50 +COMMANDS
51 +--------
52 +
53 +No commands are supported yet.
54 +
55 +GIT
56 +---
57 +Part of the linkgit:git[1] suite
Documentation/meson.build
+1
@@ -64,6 +64,7 @@ manpages = {
64 'git-gui.adoc' : 1,
65 'git-hash-object.adoc' : 1,
66 'git-help.adoc' : 1,
67 + 'git-history.adoc' : 1,
68 'git-hook.adoc' : 1,
69 'git-http-backend.adoc' : 1,
70 'git-http-fetch.adoc' : 1,
Makefile
+1
@@ -1418,6 +1418,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o
1418 BUILTIN_OBJS += builtin/grep.o
1419 BUILTIN_OBJS += builtin/hash-object.o
1420 BUILTIN_OBJS += builtin/help.o
1421 +BUILTIN_OBJS += builtin/history.o
1422 BUILTIN_OBJS += builtin/hook.o
1423 BUILTIN_OBJS += builtin/index-pack.o
1424 BUILTIN_OBJS += builtin/init-db.o
builtin.h
+1
@@ -172,6 +172,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix, struc
172 int cmd_grep(int argc, const char **argv, const char *prefix, struct repository *repo);
173 int cmd_hash_object(int argc, const char **argv, const char *prefix, struct repository *repo);
174 int cmd_help(int argc, const char **argv, const char *prefix, struct repository *repo);
175 +int cmd_history(int argc, const char **argv, const char *prefix, struct repository *repo);
176 int cmd_hook(int argc, const char **argv, const char *prefix, struct repository *repo);
177 int cmd_index_pack(int argc, const char **argv, const char *prefix, struct repository *repo);
178 int cmd_init_db(int argc, const char **argv, const char *prefix, struct repository *repo);
builtin/history.c new
+22
@@ -0,0 +1,22 @@
1 +#include "builtin.h"
2 +#include "gettext.h"
3 +#include "parse-options.h"
4 +
5 +int cmd_history(int argc,
6 + const char **argv,
7 + const char *prefix,
8 + struct repository *repo UNUSED)
9 +{
10 + const char * const usage[] = {
11 + N_("git history [<options>]"),
12 + NULL,
13 + };
14 + struct option options[] = {
15 + OPT_END(),
16 + };
17 +
18 + argc = parse_options(argc, argv, prefix, options, usage, 0);
19 + if (argc)
20 + usagef("unrecognized argument: %s", argv[0]);
21 + return 0;
22 +}
command-list.txt
+1
@@ -115,6 +115,7 @@ git-grep mainporcelain info
115 git-gui mainporcelain
116 git-hash-object plumbingmanipulators
117 git-help ancillaryinterrogators complete
118 +git-history mainporcelain history
119 git-hook purehelpers
120 git-http-backend synchingrepositories
121 git-http-fetch synchelpers
git.c
+1
@@ -586,6 +586,7 @@ static struct cmd_struct commands[] = {
586 { "grep", cmd_grep, RUN_SETUP_GENTLY },
587 { "hash-object", cmd_hash_object },
588 { "help", cmd_help },
589 + { "history", cmd_history, RUN_SETUP },
590 { "hook", cmd_hook, RUN_SETUP },
591 { "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },
592 { "init", cmd_init_db },
meson.build
+1
@@ -610,6 +610,7 @@ builtin_sources = [
610 'builtin/grep.c',
611 'builtin/hash-object.c',
612 'builtin/help.c',
613 + 'builtin/history.c',
614 'builtin/hook.c',
615 'builtin/index-pack.c',
616 'builtin/init-db.c',
t/meson.build
+1
@@ -387,6 +387,7 @@ integration_tests = [
387 't3436-rebase-more-options.sh',
388 't3437-rebase-fixup-options.sh',
389 't3438-rebase-broken-files.sh',
390 + 't3450-history.sh',
391 't3500-cherry.sh',
392 't3501-revert-cherry-pick.sh',
393 't3502-cherry-pick-merge.sh',
t/t3450-history.sh new
+17
@@ -0,0 +1,17 @@
1 +#!/bin/sh
2 +
3 +test_description='tests for git-history command'
4 +
5 +. ./test-lib.sh
6 +
7 +test_expect_success 'does nothing without any arguments' '
8 + git history >out 2>&1 &&
9 + test_must_be_empty out
10 +'
11 +
12 +test_expect_success 'raises an error with unknown argument' '
13 + test_must_fail git history garbage 2>err &&
14 + test_grep "unrecognized argument: garbage" err
15 +'
16 +
17 +test_done