Merge branch 'ps/history' into seen

"git history" history rewriting UI.

* ps/history:
  builtin/history: implement "split" subcommand
  cache-tree: allow writing in-memory index as tree
  add-patch: add support for in-memory index patching
  add-patch: remove dependency on "add-interactive" subsystem
  add-patch: split out `struct interactive_options`
  add-patch: split out header from "add-interactive.h"
  builtin/history: implement "reword" subcommand
  builtin: add new "history" command
  replay: parse commits before dereferencing them
  replay: stop using `the_repository`
  replay: extract logic to pick commits
  wt-status: provide function to expose status for trees
Junio C Hamano 2025-10-06 10:32:21 -07:00
commit 5abf519c7a
30 changed files with 1984 additions and 376 deletions

1
.gitignore vendored
View File

@ -79,6 +79,7 @@
/git-grep /git-grep
/git-hash-object /git-hash-object
/git-help /git-help
/git-history
/git-hook /git-hook
/git-http-backend /git-http-backend
/git-http-fetch /git-http-fetch

View File

@ -0,0 +1,114 @@
git-history(1)
==============

NAME
----
git-history - EXPERIMENTAL: Rewrite history of the current branch

SYNOPSIS
--------
[synopsis]
git history [<options>]
git history reword [<options>] <commit>
git history split [<options>] <commit> [--] [<pathspec>...]

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:

`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.

`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
-------------

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

View File

@ -64,6 +64,7 @@ manpages = {
'git-gui.adoc' : 1, 'git-gui.adoc' : 1,
'git-hash-object.adoc' : 1, 'git-hash-object.adoc' : 1,
'git-help.adoc' : 1, 'git-help.adoc' : 1,
'git-history.adoc' : 1,
'git-hook.adoc' : 1, 'git-hook.adoc' : 1,
'git-http-backend.adoc' : 1, 'git-http-backend.adoc' : 1,
'git-http-fetch.adoc' : 1, 'git-http-fetch.adoc' : 1,

View File

@ -1250,6 +1250,7 @@ LIB_OBJS += refs/ref-cache.o
LIB_OBJS += refspec.o LIB_OBJS += refspec.o
LIB_OBJS += remote.o LIB_OBJS += remote.o
LIB_OBJS += replace-object.o LIB_OBJS += replace-object.o
LIB_OBJS += replay.o
LIB_OBJS += repo-settings.o LIB_OBJS += repo-settings.o
LIB_OBJS += repository.o LIB_OBJS += repository.o
LIB_OBJS += rerere.o LIB_OBJS += rerere.o
@ -1376,6 +1377,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o
BUILTIN_OBJS += builtin/grep.o BUILTIN_OBJS += builtin/grep.o
BUILTIN_OBJS += builtin/hash-object.o BUILTIN_OBJS += builtin/hash-object.o
BUILTIN_OBJS += builtin/help.o BUILTIN_OBJS += builtin/help.o
BUILTIN_OBJS += builtin/history.o
BUILTIN_OBJS += builtin/hook.o BUILTIN_OBJS += builtin/hook.o
BUILTIN_OBJS += builtin/index-pack.o BUILTIN_OBJS += builtin/index-pack.o
BUILTIN_OBJS += builtin/init-db.o BUILTIN_OBJS += builtin/init-db.o

View File

@ -3,7 +3,6 @@
#include "git-compat-util.h" #include "git-compat-util.h"
#include "add-interactive.h" #include "add-interactive.h"
#include "color.h" #include "color.h"
#include "config.h"
#include "diffcore.h" #include "diffcore.h"
#include "gettext.h" #include "gettext.h"
#include "hash.h" #include "hash.h"
@ -20,119 +19,18 @@
#include "prompt.h" #include "prompt.h"
#include "tree.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, 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->r = r;
s->context = -1; interactive_config_init(&s->cfg, r, opts);
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;
}
} }


void clear_add_i_state(struct add_i_state *s) void clear_add_i_state(struct add_i_state *s)
{ {
FREE_AND_NULL(s->interactive_diff_filter); interactive_config_clear(&s->cfg);
FREE_AND_NULL(s->interactive_diff_algorithm);
memset(s, 0, sizeof(*s)); memset(s, 0, sizeof(*s));
s->use_color_interactive = GIT_COLOR_UNKNOWN; interactive_config_clear(&s->cfg);
s->use_color_diff = GIT_COLOR_UNKNOWN;
} }


/* /*
@ -286,7 +184,7 @@ static void list(struct add_i_state *s, struct string_list *list, int *selected,
return; return;


if (opts->header) if (opts->header)
color_fprintf_ln(stdout, s->header_color, color_fprintf_ln(stdout, s->cfg.header_color,
"%s", opts->header); "%s", opts->header);


for (i = 0; i < list->nr; i++) { 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); 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); fputs(singleton ? "> " : ">> ", stdout);
fflush(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 || if (from < 0 || from >= items->items.nr ||
(singleton && from + 1 != to)) { (singleton && from + 1 != to)) {
color_fprintf_ln(stderr, s->error_color, color_fprintf_ln(stderr, s->cfg.error_color,
_("Huh (%s)?"), p); _("Huh (%s)?"), p);
break; break;
} else if (singleton) { } 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); free(files->items.items[i].string);
} else if (item->index.unmerged || } else if (item->index.unmerged ||
item->worktree.unmerged) { item->worktree.unmerged) {
color_fprintf_ln(stderr, s->error_color, color_fprintf_ln(stderr, s->cfg.error_color,
_("ignoring unmerged: %s"), _("ignoring unmerged: %s"),
files->items.items[i].string); files->items.items[i].string);
free(item); free(item);
@ -1014,9 +912,9 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps,
opts->prompt = N_("Patch update"); opts->prompt = N_("Patch update");
count = list_and_choose(s, files, opts); count = list_and_choose(s, files, opts);
if (count > 0) { if (count > 0) {
struct add_p_opt add_p_opt = { struct interactive_options opts = {
.context = s->context, .context = s->cfg.context,
.interhunkcontext = s->interhunkcontext, .interhunkcontext = s->cfg.interhunkcontext,
}; };
struct strvec args = STRVEC_INIT; struct strvec args = STRVEC_INIT;
struct pathspec ps_selected = { 0 }; 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, parse_pathspec(&ps_selected,
PATHSPEC_ALL_MAGIC & ~PATHSPEC_LITERAL, PATHSPEC_ALL_MAGIC & ~PATHSPEC_LITERAL,
PATHSPEC_LITERAL_PATH, "", args.v); 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); strvec_clear(&args);
clear_pathspec(&ps_selected); 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; struct child_process cmd = CHILD_PROCESS_INIT;


strvec_pushl(&cmd.args, "git", "diff", "-p", "--cached", NULL); strvec_pushl(&cmd.args, "git", "diff", "-p", "--cached", NULL);
if (s->context != -1) if (s->cfg.context != -1)
strvec_pushf(&cmd.args, "--unified=%i", s->context); strvec_pushf(&cmd.args, "--unified=%i", s->cfg.context);
if (s->interhunkcontext != -1) if (s->cfg.interhunkcontext != -1)
strvec_pushf(&cmd.args, "--inter-hunk-context=%i", s->interhunkcontext); strvec_pushf(&cmd.args, "--inter-hunk-context=%i", s->cfg.interhunkcontext);
strvec_pushl(&cmd.args, oid_to_hex(!is_initial ? &oid : strvec_pushl(&cmd.args, oid_to_hex(!is_initial ? &oid :
s->r->hash_algo->empty_tree), "--", NULL); s->r->hash_algo->empty_tree), "--", NULL);
for (i = 0; i < files->items.nr; i++) 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 prefix_item_list *files UNUSED,
struct list_and_choose_options *opts 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")); _("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")); _("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")); _("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")); _("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")); _("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")); _("add contents of untracked files to the staged set of changes"));


return 0; 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) 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:")); _("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")); _("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")); _("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")); _("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")); _("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")); _("unselect specified items"));
color_fprintf_ln(stdout, s->help_color, "* - %s", color_fprintf_ln(stdout, s->cfg.help_color, "* - %s",
_("choose all items")); _("choose all items"));
color_fprintf_ln(stdout, s->help_color, " - %s", color_fprintf_ln(stdout, s->cfg.help_color, " - %s",
_("(empty) finish selecting")); _("(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) 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, "%s", _("Prompt help:"));
color_fprintf_ln(stdout, help_color, "1 - %s", color_fprintf_ln(stdout, help_color, "1 - %s",
_("select a numbered item")); _("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, 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 add_i_state s = { NULL };
struct print_command_item_data data = { "[", "]" }; struct print_command_item_data data = { "[", "]" };
@ -1206,15 +1104,15 @@ int run_add_i(struct repository *r, const struct pathspec *ps,
->util = util; ->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 * When color was asked for, use the prompt color for
* highlighting, otherwise use square brackets. * highlighting, otherwise use square brackets.
*/ */
if (want_color(s.use_color_interactive)) { if (want_color(s.cfg.use_color_interactive)) {
data.color = s.prompt_color; data.color = s.cfg.prompt_color;
data.reset = s.reset_color_interactive; data.reset = s.cfg.reset_color_interactive;
} }
print_file_item_data.color = data.color; print_file_item_data.color = data.color;
print_file_item_data.reset = data.reset; print_file_item_data.reset = data.reset;

View File

@ -1,55 +1,21 @@
#ifndef ADD_INTERACTIVE_H #ifndef ADD_INTERACTIVE_H
#define ADD_INTERACTIVE_H #define ADD_INTERACTIVE_H


#include "color.h" #include "add-patch.h"


struct add_p_opt { struct pathspec;
int context; struct repository;
int interhunkcontext;
};

#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }


struct add_i_state { struct add_i_state {
struct repository *r; struct repository *r;
enum git_colorbool use_color_interactive; struct interactive_config cfg;
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 init_add_i_state(struct add_i_state *s, struct repository *r, 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); 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, int run_add_i(struct repository *r, const struct pathspec *ps,
struct add_p_opt *add_p_opt); struct interactive_options *opts);

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 #endif

View File

@ -2,11 +2,15 @@
#define DISABLE_SIGN_COMPARE_WARNINGS #define DISABLE_SIGN_COMPARE_WARNINGS


#include "git-compat-util.h" #include "git-compat-util.h"
#include "add-interactive.h" #include "add-patch.h"
#include "advice.h" #include "advice.h"
#include "commit.h"
#include "config.h"
#include "diff.h"
#include "editor.h" #include "editor.h"
#include "environment.h" #include "environment.h"
#include "gettext.h" #include "gettext.h"
#include "hex.h"
#include "object-name.h" #include "object-name.h"
#include "pager.h" #include "pager.h"
#include "read-cache-ll.h" #include "read-cache-ll.h"
@ -260,7 +264,10 @@ struct hunk {
}; };


struct add_p_state { struct add_p_state {
struct add_i_state s; struct repository *r;
struct index_state *index;
const char *index_file;
struct interactive_config cfg;
struct strbuf answer, buf; struct strbuf answer, buf;


/* parsed diff */ /* parsed diff */
@ -278,6 +285,122 @@ struct add_p_state {
const char *revision; 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) static void add_p_state_clear(struct add_p_state *s)
{ {
size_t i; size_t i;
@ -289,7 +412,7 @@ static void add_p_state_clear(struct add_p_state *s)
for (i = 0; i < s->file_diff_nr; i++) for (i = 0; i < s->file_diff_nr; i++)
free(s->file_diff[i].hunk); free(s->file_diff[i].hunk);
free(s->file_diff); free(s->file_diff);
clear_add_i_state(&s->s); interactive_config_clear(&s->cfg);
} }


__attribute__((format (printf, 2, 3))) __attribute__((format (printf, 2, 3)))
@ -298,9 +421,9 @@ static void err(struct add_p_state *s, const char *fmt, ...)
va_list args; va_list args;


va_start(args, fmt); va_start(args, fmt);
fputs(s->s.error_color, stdout); fputs(s->cfg.error_color, stdout);
vprintf(fmt, args); vprintf(fmt, args);
puts(s->s.reset_color_interactive); puts(s->cfg.reset_color_interactive);
va_end(args); va_end(args);
} }


@ -318,7 +441,7 @@ static void setup_child_process(struct add_p_state *s,


cp->git_cmd = 1; cp->git_cmd = 1;
strvec_pushf(&cp->env, strvec_pushf(&cp->env,
INDEX_ENVIRONMENT "=%s", s->s.r->index_file); INDEX_ENVIRONMENT "=%s", s->index_file);
} }


static int parse_range(const char **p, static int parse_range(const char **p,
@ -423,12 +546,12 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
int res; int res;


strvec_pushv(&args, s->mode->diff_cmd); strvec_pushv(&args, s->mode->diff_cmd);
if (s->s.context != -1) if (s->cfg.context != -1)
strvec_pushf(&args, "--unified=%i", s->s.context); strvec_pushf(&args, "--unified=%i", s->cfg.context);
if (s->s.interhunkcontext != -1) if (s->cfg.interhunkcontext != -1)
strvec_pushf(&args, "--inter-hunk-context=%i", s->s.interhunkcontext); strvec_pushf(&args, "--inter-hunk-context=%i", s->cfg.interhunkcontext);
if (s->s.interactive_diff_algorithm) if (s->cfg.interactive_diff_algorithm)
strvec_pushf(&args, "--diff-algorithm=%s", s->s.interactive_diff_algorithm); strvec_pushf(&args, "--diff-algorithm=%s", s->cfg.interactive_diff_algorithm);
if (s->revision) { if (s->revision) {
struct object_id oid; struct object_id oid;
strvec_push(&args, strvec_push(&args,
@ -457,9 +580,9 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
} }
strbuf_complete_line(plain); strbuf_complete_line(plain);


if (want_color_fd(1, s->s.use_color_diff)) { if (want_color_fd(1, s->cfg.use_color_diff)) {
struct child_process colored_cp = CHILD_PROCESS_INIT; struct child_process colored_cp = CHILD_PROCESS_INIT;
const char *diff_filter = s->s.interactive_diff_filter; const char *diff_filter = s->cfg.interactive_diff_filter;


setup_child_process(s, &colored_cp, NULL); setup_child_process(s, &colored_cp, NULL);
xsnprintf((char *)args.v[color_arg_index], 8, "--color"); xsnprintf((char *)args.v[color_arg_index], 8, "--color");
@ -692,7 +815,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
hunk->colored_end - hunk->colored_start); hunk->colored_end - hunk->colored_start);
return; return;
} else { } else {
strbuf_addstr(out, s->s.fraginfo_color); strbuf_addstr(out, s->cfg.fraginfo_color);
p = s->colored.buf + header->colored_extra_start; p = s->colored.buf + header->colored_extra_start;
len = header->colored_extra_end len = header->colored_extra_end
- header->colored_extra_start; - header->colored_extra_start;
@ -714,7 +837,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
if (len) if (len)
strbuf_add(out, p, len); strbuf_add(out, p, len);
else if (colored) else if (colored)
strbuf_addf(out, "%s\n", s->s.reset_color_diff); strbuf_addf(out, "%s\n", s->cfg.reset_color_diff);
else else
strbuf_addch(out, '\n'); strbuf_addch(out, '\n');
} }
@ -1103,12 +1226,12 @@ static void recolor_hunk(struct add_p_state *s, struct hunk *hunk)


strbuf_addstr(&s->colored, strbuf_addstr(&s->colored,
plain[current] == '-' ? plain[current] == '-' ?
s->s.file_old_color : s->cfg.file_old_color :
plain[current] == '+' ? plain[current] == '+' ?
s->s.file_new_color : s->cfg.file_new_color :
s->s.context_color); s->cfg.context_color);
strbuf_add(&s->colored, plain + current, eol - current); strbuf_add(&s->colored, plain + current, eol - current);
strbuf_addstr(&s->colored, s->s.reset_color_diff); strbuf_addstr(&s->colored, s->cfg.reset_color_diff);
if (next > eol) if (next > eol)
strbuf_add(&s->colored, plain + eol, next - eol); strbuf_add(&s->colored, plain + eol, next - eol);
current = next; current = next;
@ -1237,7 +1360,7 @@ static int run_apply_check(struct add_p_state *s,


static int read_single_character(struct add_p_state *s) static int read_single_character(struct add_p_state *s)
{ {
if (s->s.use_single_key) { if (s->cfg.use_single_key) {
int res = read_key_without_echo(&s->answer); int res = read_key_without_echo(&s->answer);
printf("%s\n", res == EOF ? "" : s->answer.buf); printf("%s\n", res == EOF ? "" : s->answer.buf);
return res; return res;
@ -1251,7 +1374,7 @@ static int read_single_character(struct add_p_state *s)
static int prompt_yesno(struct add_p_state *s, const char *prompt) static int prompt_yesno(struct add_p_state *s, const char *prompt)
{ {
for (;;) { for (;;) {
color_fprintf(stdout, s->s.prompt_color, "%s", _(prompt)); color_fprintf(stdout, s->cfg.prompt_color, "%s", _(prompt));
fflush(stdout); fflush(stdout);
if (read_single_character(s) == EOF) if (read_single_character(s) == EOF)
return -1; return -1;
@ -1532,15 +1655,15 @@ static int patch_update_file(struct add_p_state *s,
else else
prompt_mode_type = PROMPT_HUNK; prompt_mode_type = PROMPT_HUNK;


printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->s.prompt_color, printf("%s(%"PRIuMAX"/%"PRIuMAX") ", s->cfg.prompt_color,
(uintmax_t)hunk_index + 1, (uintmax_t)hunk_index + 1,
(uintmax_t)(file_diff->hunk_nr (uintmax_t)(file_diff->hunk_nr
? file_diff->hunk_nr ? file_diff->hunk_nr
: 1)); : 1));
printf(_(s->mode->prompt_mode[prompt_mode_type]), printf(_(s->mode->prompt_mode[prompt_mode_type]),
s->buf.buf); s->buf.buf);
if (*s->s.reset_color_interactive) if (*s->cfg.reset_color_interactive)
fputs(s->s.reset_color_interactive, stdout); fputs(s->cfg.reset_color_interactive, stdout);
fflush(stdout); fflush(stdout);
if (read_single_character(s) == EOF) if (read_single_character(s) == EOF)
break; break;
@ -1697,7 +1820,7 @@ soft_increment:
err(s, _("Sorry, cannot split this hunk")); err(s, _("Sorry, cannot split this hunk"));
} else if (!split_hunk(s, file_diff, } else if (!split_hunk(s, file_diff,
hunk - file_diff->hunk)) { hunk - file_diff->hunk)) {
color_fprintf_ln(stdout, s->s.header_color, color_fprintf_ln(stdout, s->cfg.header_color,
_("Split into %d hunks."), _("Split into %d hunks."),
(int)splittable_into); (int)splittable_into);
rendered_hunk_index = -1; rendered_hunk_index = -1;
@ -1715,7 +1838,7 @@ soft_increment:
} else if (s->answer.buf[0] == '?') { } else if (s->answer.buf[0] == '?') {
const char *p = _(help_patch_remainder), *eol = p; const char *p = _(help_patch_remainder), *eol = p;


color_fprintf(stdout, s->s.help_color, "%s", color_fprintf(stdout, s->cfg.help_color, "%s",
_(s->mode->help_patch_text)); _(s->mode->help_patch_text));


/* /*
@ -1733,7 +1856,7 @@ soft_increment:
if (*p != '?' && !strchr(s->buf.buf, *p)) if (*p != '?' && !strchr(s->buf.buf, *p))
continue; continue;


color_fprintf_ln(stdout, s->s.help_color, color_fprintf_ln(stdout, s->cfg.help_color,
"%.*s", (int)(eol - p), p); "%.*s", (int)(eol - p), p);
} }
} else { } else {
@ -1753,7 +1876,7 @@ soft_increment:
strbuf_reset(&s->buf); strbuf_reset(&s->buf);
reassemble_patch(s, file_diff, 0, &s->buf); reassemble_patch(s, file_diff, 0, &s->buf);


discard_index(s->s.r->index); discard_index(s->index);
if (s->mode->apply_for_checkout) if (s->mode->apply_for_checkout)
apply_for_checkout(s, &s->buf, apply_for_checkout(s, &s->buf,
s->mode->is_reverse); s->mode->is_reverse);
@ -1764,9 +1887,11 @@ soft_increment:
NULL, 0, NULL, 0)) NULL, 0, NULL, 0))
error(_("'git apply' failed")); error(_("'git apply' failed"));
} }
if (repo_read_index(s->s.r) >= 0) if (read_index_from(s->index, s->index_file, s->r->gitdir) >= 0 &&
repo_refresh_and_write_index(s->s.r, REFRESH_QUIET, 0, s->index == s->r->index) {
repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,
1, NULL, NULL, NULL); 1, NULL, NULL, NULL);
}
} }


putchar('\n'); putchar('\n');
@ -1774,15 +1899,21 @@ soft_increment:
} }


int run_add_p(struct repository *r, enum add_p_mode 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) const struct pathspec *ps)
{ {
struct add_p_state s = { struct add_p_state s = {
{ r }, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT .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; size_t i, binary_count = 0;


init_add_i_state(&s.s, r, o); interactive_config_init(&s.cfg, r, opts);


if (mode == ADD_P_STASH) if (mode == ADD_P_STASH)
s.mode = &patch_mode_stash; s.mode = &patch_mode_stash;
@ -1833,3 +1964,99 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
add_p_state_clear(&s); add_p_state_clear(&s);
return 0; 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;
}

64
add-patch.h Normal file
View File

@ -0,0 +1,64 @@
#ifndef ADD_PATCH_H
#define ADD_PATCH_H

#include "color.h"

struct index_state;
struct pathspec;
struct repository;

struct interactive_options {
int context;
int interhunkcontext;
};

#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,
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 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

View File

@ -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_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_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_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_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_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); int cmd_init_db(int argc, const char **argv, const char *prefix, struct repository *repo);

View File

@ -31,7 +31,7 @@ static const char * const builtin_add_usage[] = {
NULL NULL
}; };
static int patch_interactive, add_interactive, edit_interactive; 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 take_worktree_changes;
static int add_renormalize; static int add_renormalize;
static int pathspec_file_nul; 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, int interactive_add(struct repository *repo,
const char **argv, const char **argv,
const char *prefix, const char *prefix,
int patch, struct add_p_opt *add_p_opt) int patch, struct interactive_options *interactive_opts)
{ {
struct pathspec pathspec; struct pathspec pathspec;
int ret; int ret;
@ -172,9 +172,9 @@ int interactive_add(struct repository *repo,
prefix, argv); prefix, argv);


if (patch) 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 else
ret = !!run_add_i(repo, &pathspec, add_p_opt); ret = !!run_add_i(repo, &pathspec, interactive_opts);


clear_pathspec(&pathspec); clear_pathspec(&pathspec);
return ret; return ret;
@ -256,8 +256,8 @@ static struct option builtin_add_options[] = {
OPT_GROUP(""), OPT_GROUP(""),
OPT_BOOL('i', "interactive", &add_interactive, N_("interactive picking")), OPT_BOOL('i', "interactive", &add_interactive, N_("interactive picking")),
OPT_BOOL('p', "patch", &patch_interactive, N_("select hunks interactively")), OPT_BOOL('p', "patch", &patch_interactive, N_("select hunks interactively")),
OPT_DIFF_UNIFIED(&add_p_opt.context), OPT_DIFF_UNIFIED(&interactive_opts.context),
OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext), OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT_BOOL('e', "edit", &edit_interactive, N_("edit current diff and apply")), OPT_BOOL('e', "edit", &edit_interactive, N_("edit current diff and apply")),
OPT__FORCE(&ignored_too, N_("allow adding otherwise ignored files"), 0), OPT__FORCE(&ignored_too, N_("allow adding otherwise ignored files"), 0),
OPT_BOOL('u', "update", &take_worktree_changes, N_("update tracked files")), OPT_BOOL('u', "update", &take_worktree_changes, N_("update tracked files")),
@ -400,9 +400,9 @@ int cmd_add(int argc,
prepare_repo_settings(repo); prepare_repo_settings(repo);
repo->settings.command_requires_full_index = 0; repo->settings.command_requires_full_index = 0;


if (add_p_opt.context < -1) if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified"); 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"); die(_("'%s' cannot be negative"), "--inter-hunk-context");


if (patch_interactive) 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"); die(_("options '%s' and '%s' cannot be used together"), "--dry-run", "--interactive/--patch");
if (pathspec_from_file) if (pathspec_from_file)
die(_("options '%s' and '%s' cannot be used together"), "--pathspec-from-file", "--interactive/--patch"); 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 { } else {
if (add_p_opt.context != -1) if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--interactive/--patch"); 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"); die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--interactive/--patch");
} }



View File

@ -546,7 +546,7 @@ static int checkout_paths(const struct checkout_opts *opts,


if (opts->patch_mode) { if (opts->patch_mode) {
enum add_p_mode patch_mode; enum add_p_mode patch_mode;
struct add_p_opt add_p_opt = { struct interactive_options interactive_opts = {
.context = opts->patch_context, .context = opts->patch_context,
.interhunkcontext = opts->patch_interhunk_context, .interhunkcontext = opts->patch_interhunk_context,
}; };
@ -575,7 +575,7 @@ static int checkout_paths(const struct checkout_opts *opts,
else else
BUG("either flag must have been set, worktree=%d, index=%d", BUG("either flag must have been set, worktree=%d, index=%d",
opts->checkout_worktree, opts->checkout_index); 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); rev, &opts->pathspec);
} }


@ -902,7 +902,8 @@ static int merge_working_tree(const struct checkout_opts *opts,
0); 0);
init_ui_merge_options(&o, the_repository); init_ui_merge_options(&o, the_repository);
o.verbosity = 0; 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, ret = reset_tree(new_tree,
opts, 1, opts, 1,

View File

@ -123,7 +123,7 @@ static const char *edit_message, *use_message;
static char *fixup_message, *fixup_commit, *squash_message; static char *fixup_message, *fixup_commit, *squash_message;
static const char *fixup_prefix; static const char *fixup_prefix;
static int all, also, interactive, patch_interactive, only, amend, signoff; 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 edit_flag = -1; /* unspecified */
static int quiet, verbose, no_verify, allow_empty, dry_run, renew_authorship; static int quiet, verbose, no_verify, allow_empty, dry_run, renew_authorship;
static int config_commit_verbose = -1; /* unspecified */ 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; const char *ret;
char *path = NULL; char *path = NULL;


if (add_p_opt.context < -1) if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified"); 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"); die(_("'%s' cannot be negative"), "--inter-hunk-context");


if (is_status) 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)); old_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT));
setenv(INDEX_ENVIRONMENT, the_repository->index_file, 1); 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")); die(_("interactive add failed"));


the_repository->index_file = old_repo_index_file; 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); ret = get_lock_file_path(&index_lock);
goto out; goto out;
} else { } else {
if (add_p_opt.context != -1) if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--interactive/--patch"); 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"); 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('i', "include", &also, N_("add specified files to index for commit")),
OPT_BOOL(0, "interactive", &interactive, N_("interactively add files")), OPT_BOOL(0, "interactive", &interactive, N_("interactively add files")),
OPT_BOOL('p', "patch", &patch_interactive, N_("interactively add changes")), OPT_BOOL('p', "patch", &patch_interactive, N_("interactively add changes")),
OPT_DIFF_UNIFIED(&add_p_opt.context), OPT_DIFF_UNIFIED(&interactive_opts.context),
OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext), OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT_BOOL('o', "only", &only, N_("commit only specified files")), 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('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")), OPT_BOOL(0, "dry-run", &dry_run, N_("show what would be committed")),

614
builtin/history.c Normal file
View File

@ -0,0 +1,614 @@
#define USE_THE_REPOSITORY_VARIABLE

#include "builtin.h"
#include "cache-tree.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 "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"

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;
}

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,
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,
};
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);
return fn(argc, argv, prefix, repo);
}

View File

@ -2,7 +2,6 @@
* "git replay" builtin command * "git replay" builtin command
*/ */


#define USE_THE_REPOSITORY_VARIABLE
#define DISABLE_SIGN_COMPARE_WARNINGS #define DISABLE_SIGN_COMPARE_WARNINGS


#include "git-compat-util.h" #include "git-compat-util.h"
@ -15,18 +14,12 @@
#include "object-name.h" #include "object-name.h"
#include "parse-options.h" #include "parse-options.h"
#include "refs.h" #include "refs.h"
#include "replay.h"
#include "revision.h" #include "revision.h"
#include "strmap.h" #include "strmap.h"
#include <oidset.h> #include <oidset.h>
#include <tree.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) static struct commit *peel_committish(struct repository *repo, const char *name)
{ {
struct object *obj; struct object *obj;
@ -39,59 +32,6 @@ static struct commit *peel_committish(struct repository *repo, const char *name)
OBJ_COMMIT); 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 ref_info {
struct commit *onto; struct commit *onto;
struct strset positive_refs; struct strset positive_refs;
@ -240,50 +180,6 @@ static void determine_replay_mode(struct repository *repo,
strset_clear(&rinfo.positive_refs); 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, static int add_ref_to_transaction(struct ref_transaction *transaction,
const char *refname, const char *refname,
const struct object_id *new_oid, const struct object_id *new_oid,
@ -459,8 +355,8 @@ int cmd_replay(int argc,
if (commit->parents->next) if (commit->parents->next)
die(_("replaying merge commits is not supported yet!")); die(_("replaying merge commits is not supported yet!"));


last_commit = pick_regular_commit(repo, commit, replayed_commits, last_commit = replay_pick_regular_commit(repo, commit, replayed_commits,
onto, &merge_opt, &result); onto, &merge_opt, &result);
if (!last_commit) if (!last_commit)
break; break;



View File

@ -346,7 +346,7 @@ int cmd_reset(int argc,
struct object_id oid; struct object_id oid;
struct pathspec pathspec; struct pathspec pathspec;
int intent_to_add = 0; 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[] = { const struct option options[] = {
OPT__QUIET(&quiet, N_("be quiet, only report errors")), OPT__QUIET(&quiet, N_("be quiet, only report errors")),
OPT_BOOL(0, "no-refresh", &no_refresh, OPT_BOOL(0, "no-refresh", &no_refresh,
@ -371,8 +371,8 @@ int cmd_reset(int argc,
PARSE_OPT_OPTARG, PARSE_OPT_OPTARG,
option_parse_recurse_submodules_worktree_updater), option_parse_recurse_submodules_worktree_updater),
OPT_BOOL('p', "patch", &patch_mode, N_("select hunks interactively")), OPT_BOOL('p', "patch", &patch_mode, N_("select hunks interactively")),
OPT_DIFF_UNIFIED(&add_p_opt.context), OPT_DIFF_UNIFIED(&interactive_opts.context),
OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext), OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT_BOOL('N', "intent-to-add", &intent_to_add, OPT_BOOL('N', "intent-to-add", &intent_to_add,
N_("record only the fact that removed paths will be added later")), N_("record only the fact that removed paths will be added later")),
OPT_PATHSPEC_FROM_FILE(&pathspec_from_file), OPT_PATHSPEC_FROM_FILE(&pathspec_from_file),
@ -423,9 +423,9 @@ int cmd_reset(int argc,
oidcpy(&oid, &tree->object.oid); oidcpy(&oid, &tree->object.oid);
} }


if (add_p_opt.context < -1) if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified"); 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"); die(_("'%s' cannot be negative"), "--inter-hunk-context");


prepare_repo_settings(the_repository); 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}"); die(_("options '%s' and '%s' cannot be used together"), "--patch", "--{hard,mixed,soft}");
trace2_cmd_mode("patch-interactive"); trace2_cmd_mode("patch-interactive");
update_ref_status = !!run_add_p(the_repository, ADD_P_RESET, update_ref_status = !!run_add_p(the_repository, ADD_P_RESET,
&add_p_opt, rev, &pathspec); &interactive_opts, rev, &pathspec);
goto cleanup; goto cleanup;
} else { } else {
if (add_p_opt.context != -1) if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--patch"); 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"); die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--patch");
} }



View File

@ -1306,7 +1306,7 @@ done:


static int stash_patch(struct stash_info *info, const struct pathspec *ps, static int stash_patch(struct stash_info *info, const struct pathspec *ps,
struct strbuf *out_patch, int quiet, struct strbuf *out_patch, int quiet,
struct add_p_opt *add_p_opt) struct interactive_options *interactive_opts)
{ {
int ret = 0; int ret = 0;
struct child_process cp_read_tree = CHILD_PROCESS_INIT; 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)); old_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT));
setenv(INDEX_ENVIRONMENT, the_repository->index_file, 1); 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; the_repository->index_file = old_repo_index_file;
if (old_index_env && *old_index_env) if (old_index_env && *old_index_env)
@ -1427,7 +1427,8 @@ done:
} }


static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_buf, 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 only_staged, struct stash_info *info, struct strbuf *patch,
int quiet) int quiet)
{ {
@ -1509,7 +1510,7 @@ static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_b
untracked_commit_option = 1; untracked_commit_option = 1;
} }
if (patch_mode) { 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 (ret < 0) {
if (!quiet) if (!quiet)
fprintf_ln(stderr, _("Cannot save the current " 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, 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 include_untracked, int only_staged)
{ {
int ret = 0; 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) if (stash_msg)
strbuf_addstr(&stash_msg_buf, stash_msg); strbuf_addstr(&stash_msg_buf, stash_msg);
if (do_create_stash(ps, &stash_msg_buf, include_untracked, patch_mode, 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; ret = -1;
goto done; goto done;
} }
@ -1841,7 +1843,7 @@ static int push_stash(int argc, const char **argv, const char *prefix,
const char *stash_msg = NULL; const char *stash_msg = NULL;
char *pathspec_from_file = NULL; char *pathspec_from_file = NULL;
struct pathspec ps; 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[] = { struct option options[] = {
OPT_BOOL('k', "keep-index", &keep_index, OPT_BOOL('k', "keep-index", &keep_index,
N_("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")), N_("stash staged changes only")),
OPT_BOOL('p', "patch", &patch_mode, OPT_BOOL('p', "patch", &patch_mode,
N_("stash in patch mode")), N_("stash in patch mode")),
OPT_DIFF_UNIFIED(&add_p_opt.context), OPT_DIFF_UNIFIED(&interactive_opts.context),
OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext), OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT__QUIET(&quiet, N_("quiet mode")), OPT__QUIET(&quiet, N_("quiet mode")),
OPT_BOOL('u', "include-untracked", &include_untracked, OPT_BOOL('u', "include-untracked", &include_untracked,
N_("include untracked files in stash")), 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 (!patch_mode) {
if (add_p_opt.context != -1) if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--patch"); 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"); 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"); 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"); die(_("'%s' cannot be negative"), "--inter-hunk-context");


ret = do_push_stash(&ps, stash_msg, quiet, keep_index, patch_mode, 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); clear_pathspec(&ps);
free(pathspec_from_file); 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; const char *stash_msg = NULL;
struct pathspec ps; struct pathspec ps;
struct strbuf stash_msg_buf = STRBUF_INIT; 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[] = { struct option options[] = {
OPT_BOOL('k', "keep-index", &keep_index, OPT_BOOL('k', "keep-index", &keep_index,
N_("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")), N_("stash staged changes only")),
OPT_BOOL('p', "patch", &patch_mode, OPT_BOOL('p', "patch", &patch_mode,
N_("stash in patch mode")), N_("stash in patch mode")),
OPT_DIFF_UNIFIED(&add_p_opt.context), OPT_DIFF_UNIFIED(&interactive_opts.context),
OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext), OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT__QUIET(&quiet, N_("quiet mode")), OPT__QUIET(&quiet, N_("quiet mode")),
OPT_BOOL('u', "include-untracked", &include_untracked, OPT_BOOL('u', "include-untracked", &include_untracked,
N_("include untracked files in stash")), 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)); memset(&ps, 0, sizeof(ps));


if (add_p_opt.context < -1) if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified"); 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"); die(_("'%s' cannot be negative"), "--inter-hunk-context");


if (!patch_mode) { if (!patch_mode) {
if (add_p_opt.context != -1) if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--patch"); 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"); die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--patch");
} }


ret = do_push_stash(&ps, stash_msg, quiet, keep_index, 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); only_staged);


strbuf_release(&stash_msg_buf); strbuf_release(&stash_msg_buf);

View File

@ -699,11 +699,11 @@ static int write_index_as_tree_internal(struct object_id *oid,
return 0; 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; struct object_id o;
int was_valid, ret; int was_valid, ret;


struct index_state *index_state = repo->index;
was_valid = index_state->cache_tree && was_valid = index_state->cache_tree &&
cache_tree_fully_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); 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 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; int entries, was_valid;

View File

@ -47,7 +47,8 @@ int cache_tree_verify(struct repository *, struct index_state *);
#define WRITE_TREE_UNMERGED_INDEX (-2) #define WRITE_TREE_UNMERGED_INDEX (-2)
#define WRITE_TREE_PREFIX_ERROR (-3) #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); 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 *); void prime_cache_tree(struct repository *, struct index_state *, struct tree *);



View File

@ -115,6 +115,7 @@ git-grep mainporcelain info
git-gui mainporcelain git-gui mainporcelain
git-hash-object plumbingmanipulators git-hash-object plumbingmanipulators
git-help ancillaryinterrogators complete git-help ancillaryinterrogators complete
git-history mainporcelain history
git-hook purehelpers git-hook purehelpers
git-http-backend synchingrepositories git-http-backend synchingrepositories
git-http-fetch synchelpers git-http-fetch synchelpers

View File

@ -258,7 +258,7 @@ int for_each_commit_graft(each_commit_graft_fn, void *);
int interactive_add(struct repository *repo, int interactive_add(struct repository *repo,
const char **argv, const char **argv,
const char *prefix, 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 {
struct commit_extra_header *next; struct commit_extra_header *next;

1
git.c
View File

@ -586,6 +586,7 @@ static struct cmd_struct commands[] = {
{ "grep", cmd_grep, RUN_SETUP_GENTLY }, { "grep", cmd_grep, RUN_SETUP_GENTLY },
{ "hash-object", cmd_hash_object }, { "hash-object", cmd_hash_object },
{ "help", cmd_help }, { "help", cmd_help },
{ "history", cmd_history, RUN_SETUP },
{ "hook", cmd_hook, RUN_SETUP }, { "hook", cmd_hook, RUN_SETUP },
{ "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT }, { "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },
{ "init", cmd_init_db }, { "init", cmd_init_db },

View File

@ -464,6 +464,7 @@ libgit_sources = [
'reftable/writer.c', 'reftable/writer.c',
'remote.c', 'remote.c',
'replace-object.c', 'replace-object.c',
'replay.c',
'repo-settings.c', 'repo-settings.c',
'repository.c', 'repository.c',
'rerere.c', 'rerere.c',
@ -603,6 +604,7 @@ builtin_sources = [
'builtin/grep.c', 'builtin/grep.c',
'builtin/hash-object.c', 'builtin/hash-object.c',
'builtin/help.c', 'builtin/help.c',
'builtin/history.c',
'builtin/hook.c', 'builtin/hook.c',
'builtin/index-pack.c', 'builtin/index-pack.c',
'builtin/init-db.c', 'builtin/init-db.c',

118
replay.c Normal file
View File

@ -0,0 +1,118 @@
#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(repo, 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;

if (repo_parse_commit(repo, pickme))
return NULL;

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);
}

18
replay.h Normal file
View File

@ -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

View File

@ -385,6 +385,9 @@ integration_tests = [
't3436-rebase-more-options.sh', 't3436-rebase-more-options.sh',
't3437-rebase-fixup-options.sh', 't3437-rebase-fixup-options.sh',
't3438-rebase-broken-files.sh', 't3438-rebase-broken-files.sh',
't3450-history.sh',
't3451-history-reword.sh',
't3452-history-split.sh',
't3500-cherry.sh', 't3500-cherry.sh',
't3501-revert-cherry-pick.sh', 't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh', 't3502-cherry-pick-merge.sh',

17
t/t3450-history.sh Executable file
View File

@ -0,0 +1,17 @@
#!/bin/sh

test_description='tests for git-history command'

. ./test-lib.sh

test_expect_success 'does nothing without any arguments' '
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 "unknown subcommand: .garbage." err
'

test_done

202
t/t3451-history-reword.sh Executable file
View File

@ -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

432
t/t3452-history-split.sh Executable file
View File

@ -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

View File

@ -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) static void wt_status_collect_changes_worktree(struct wt_status *s)
{ {
struct rev_info rev; struct rev_info rev;

View File

@ -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_prepare(struct repository *r, struct wt_status *s);
void wt_status_print(struct wt_status *s); void wt_status_print(struct wt_status *s);
void wt_status_collect(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. * Frees the buffers allocated by wt_status_collect.
*/ */