Merge pull request #2150 from dscho/add-p-gfw

Offer a built-in version of `git add -i` and `git add -p`
This commit is contained in:
Johannes Schindelin
2019-05-08 21:30:53 +02:00
committed by GitHub
28 changed files with 3449 additions and 91 deletions

View File

@@ -5,3 +5,8 @@ add.ignore-errors (deprecated)::
option of linkgit:git-add[1]. `add.ignore-errors` is deprecated,
as it does not follow the usual naming convention for configuration
variables.
add.interactive.useBuiltin::
[EXPERIMENTAL] Set to `true` to use the experimental built-in
implementation of the interactive version of linkgit:git-add[1]
instead of the Perl script version. Is `false` by default.

View File

@@ -757,6 +757,7 @@ TEST_BUILTINS_OBJS += test-online-cpus.o
TEST_BUILTINS_OBJS += test-parse-options.o
TEST_BUILTINS_OBJS += test-path-utils.o
TEST_BUILTINS_OBJS += test-pkt-line.o
TEST_BUILTINS_OBJS += test-prefix-map.o
TEST_BUILTINS_OBJS += test-prio-queue.o
TEST_BUILTINS_OBJS += test-reach.o
TEST_BUILTINS_OBJS += test-read-cache.o
@@ -852,6 +853,8 @@ LIB_H := $(shell git ls-files '*.h' ':!t/' ':!Documentation/' 2>/dev/null || \
-name '*.h' -print)
LIB_OBJS += abspath.o
LIB_OBJS += add-interactive.o
LIB_OBJS += add-patch.o
LIB_OBJS += advice.o
LIB_OBJS += alias.o
LIB_OBJS += alloc.o
@@ -970,6 +973,7 @@ LIB_OBJS += patch-ids.o
LIB_OBJS += path.o
LIB_OBJS += pathspec.o
LIB_OBJS += pkt-line.o
LIB_OBJS += prefix-map.o
LIB_OBJS += preload-index.o
LIB_OBJS += pretty.o
LIB_OBJS += prio-queue.o

1070
add-interactive.c Normal file

File diff suppressed because it is too large Load Diff

51
add-interactive.h Normal file
View File

@@ -0,0 +1,51 @@
#ifndef ADD_INTERACTIVE_H
#define ADD_INTERACTIVE_H
#include "color.h"
struct add_i_state {
struct repository *r;
int use_color;
char header_color[COLOR_MAXLEN];
char help_color[COLOR_MAXLEN];
char prompt_color[COLOR_MAXLEN];
char error_color[COLOR_MAXLEN];
char reset_color[COLOR_MAXLEN];
char fraginfo_color[COLOR_MAXLEN];
char context_color[COLOR_MAXLEN];
char file_old_color[COLOR_MAXLEN];
char file_new_color[COLOR_MAXLEN];
int use_single_key;
char *interactive_diff_filter, *interactive_diff_algorithm;
};
int init_add_i_state(struct repository *r, struct add_i_state *s);
enum color_add_i {
COLOR_HEADER = 0,
COLOR_HELP,
COLOR_PROMPT,
COLOR_ERROR,
COLOR_RESET,
};
const char *get_add_i_color(enum color_add_i ix);
const char *get_interactive_diff_filter(void);
const char *get_interactive_diff_algorithm(void);
int get_interactive_use_single_key(void);
struct repository;
struct pathspec;
int run_add_i(struct repository *r, const struct pathspec *ps);
enum add_p_mode {
ADD_P_STAGE,
ADD_P_STASH,
ADD_P_RESET,
ADD_P_CHECKOUT,
};
int run_add_p(struct repository *r, enum add_p_mode mode,
const char *revision, const struct pathspec *ps);
#endif

1562
add-patch.c Normal file

File diff suppressed because it is too large Load Diff

10
apply.c
View File

@@ -2676,6 +2676,16 @@ static int find_pos(struct apply_state *state,
unsigned long backwards, forwards, current;
int backwards_lno, forwards_lno, current_lno;
/*
* When running with --allow-overlap, it is possible that a hunk is
* seen that pretends to start at the beginning (but no longer does),
* and that *still* needs to match the end. So trust `match_end` more
* than `match_beginning`.
*/
if (state->allow_overlap && match_beginning && match_end &&
img->nr - preimage->nr != 0)
match_beginning = 0;
/*
* If match_beginning or match_end is specified, there is no
* point starting from a wrong line that will never match and

View File

@@ -20,12 +20,14 @@
#include "bulk-checkin.h"
#include "argv-array.h"
#include "submodule.h"
#include "add-interactive.h"
static const char * const builtin_add_usage[] = {
N_("git add [<options>] [--] <pathspec>..."),
NULL
};
static int patch_interactive, add_interactive, edit_interactive;
static const char *patch_interactive;
static int add_interactive, edit_interactive;
static int take_worktree_changes;
static int add_renormalize;
@@ -180,11 +182,38 @@ static void refresh(int verbose, const struct pathspec *pathspec)
free(seen);
}
static int add_config(const char *var, const char *value, void *cb);
int run_add_interactive(const char *revision, const char *patch_mode,
const struct pathspec *pathspec)
{
int status, i;
struct argv_array argv = ARGV_ARRAY_INIT;
int use_builtin_add_i =
git_env_bool("GIT_TEST_ADD_I_USE_BUILTIN", -1);
if (use_builtin_add_i < 0)
git_config_get_bool("add.interactive.usebuiltin",
&use_builtin_add_i);
if (use_builtin_add_i == 1) {
enum add_p_mode mode;
if (!patch_mode)
return !!run_add_i(the_repository, pathspec);
if (!strcmp(patch_mode, "--patch"))
mode = ADD_P_STAGE;
else if (!strcmp(patch_mode, "--patch=stash"))
mode = ADD_P_STASH;
else if (!strcmp(patch_mode, "--patch=reset"))
mode = ADD_P_RESET;
else if (!strcmp(patch_mode, "--patch=checkout"))
mode = ADD_P_CHECKOUT;
else
die("'%s' not supported", patch_mode);
return !!run_add_p(the_repository, mode, revision, pathspec);
}
argv_array_push(&argv, "add--interactive");
if (patch_mode)
@@ -201,9 +230,11 @@ int run_add_interactive(const char *revision, const char *patch_mode,
return status;
}
int interactive_add(int argc, const char **argv, const char *prefix, int patch)
int interactive_add(int argc, const char **argv, const char *prefix,
const char *patch_mode)
{
struct pathspec pathspec;
char buffer[64];
parse_pathspec(&pathspec, 0,
PATHSPEC_PREFER_FULL |
@@ -211,9 +242,13 @@ int interactive_add(int argc, const char **argv, const char *prefix, int patch)
PATHSPEC_PREFIX_ORIGIN,
prefix, argv);
return run_add_interactive(NULL,
patch ? "--patch" : NULL,
&pathspec);
if (patch_mode) {
xsnprintf(buffer, sizeof(buffer), "--patch%s%s",
*patch_mode ? "=" : "", patch_mode);
patch_mode = buffer;
}
return run_add_interactive(NULL, patch_mode, &pathspec);
}
static int edit_patch(int argc, const char **argv, const char *prefix)
@@ -291,7 +326,9 @@ static struct option builtin_add_options[] = {
OPT__VERBOSE(&verbose, N_("be verbose")),
OPT_GROUP(""),
OPT_BOOL('i', "interactive", &add_interactive, N_("interactive picking")),
OPT_BOOL('p', "patch", &patch_interactive, N_("select hunks interactively")),
{ OPTION_STRING, 'p', "patch", &patch_interactive, N_("patch-mode"),
N_("select hunks interactively"), PARSE_OPT_OPTARG, NULL,
(intptr_t) "" },
OPT_BOOL('e', "edit", &edit_interactive, N_("edit current diff and apply")),
OPT__FORCE(&ignored_too, N_("allow adding otherwise ignored files"), 0),
OPT_BOOL('u', "update", &take_worktree_changes, N_("update tracked files")),
@@ -319,6 +356,7 @@ static int add_config(const char *var, const char *value, void *cb)
ignore_add_errors = git_config_bool(var, value);
return 0;
}
return git_default_config(var, value, cb);
}

View File

@@ -344,7 +344,7 @@ static const char *prepare_index(int argc, const char **argv, const char *prefix
die(_("index file corrupt"));
if (interactive) {
char *old_index_env = NULL;
char *old_index_env = NULL, *old_repo_index_file;
hold_locked_index(&index_lock, LOCK_DIE_ON_ERROR);
refresh_cache_or_die(refresh_flags);
@@ -352,12 +352,17 @@ static const char *prepare_index(int argc, const char **argv, const char *prefix
if (write_locked_index(&the_index, &index_lock, 0))
die(_("unable to create temporary index"));
old_repo_index_file = the_repository->index_file;
the_repository->index_file =
(char *)get_lock_file_path(&index_lock);
old_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT));
setenv(INDEX_ENVIRONMENT, get_lock_file_path(&index_lock), 1);
setenv(INDEX_ENVIRONMENT, the_repository->index_file, 1);
if (interactive_add(argc, argv, prefix, patch_interactive) != 0)
if (interactive_add(argc, argv, prefix,
patch_interactive ? "" : NULL) != 0)
die(_("interactive add failed"));
the_repository->index_file = old_repo_index_file;
if (old_index_env && *old_index_env)
setenv(INDEX_ENVIRONMENT, old_index_env, 1);
else

View File

@@ -981,9 +981,9 @@ static int stash_patch(struct stash_info *info, struct pathspec ps,
{
int ret = 0;
struct child_process cp_read_tree = CHILD_PROCESS_INIT;
struct child_process cp_add_i = CHILD_PROCESS_INIT;
struct child_process cp_diff_tree = CHILD_PROCESS_INIT;
struct index_state istate = { NULL };
char *old_index_env = NULL, *old_repo_index_file;
remove_path(stash_index_path.buf);
@@ -997,16 +997,19 @@ static int stash_patch(struct stash_info *info, struct pathspec ps,
}
/* Find out what the user wants. */
cp_add_i.git_cmd = 1;
argv_array_pushl(&cp_add_i.args, "add--interactive", "--patch=stash",
"--", NULL);
add_pathspecs(&cp_add_i.args, ps);
argv_array_pushf(&cp_add_i.env_array, "GIT_INDEX_FILE=%s",
stash_index_path.buf);
if (run_command(&cp_add_i)) {
ret = -1;
goto done;
}
old_repo_index_file = the_repository->index_file;
the_repository->index_file = stash_index_path.buf;
old_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT));
setenv(INDEX_ENVIRONMENT, the_repository->index_file, 1);
ret = run_add_interactive(NULL, "--patch=stash", &ps);
the_repository->index_file = old_repo_index_file;
if (old_index_env && *old_index_env)
setenv(INDEX_ENVIRONMENT, old_index_env, 1);
else
unsetenv(INDEX_ENVIRONMENT);
FREE_AND_NULL(old_index_env);
/* State of the working tree. */
if (write_index_as_tree(&info->w_tree, &istate, stash_index_path.buf, 0,
@@ -1016,7 +1019,7 @@ static int stash_patch(struct stash_info *info, struct pathspec ps,
}
cp_diff_tree.git_cmd = 1;
argv_array_pushl(&cp_diff_tree.args, "diff-tree", "-p", "HEAD",
argv_array_pushl(&cp_diff_tree.args, "diff-tree", "-p", "-U1", "HEAD",
oid_to_hex(&info->w_tree), "--", NULL);
if (pipe_command(&cp_diff_tree, NULL, 0, out_patch, 0, NULL, 0)) {
ret = -1;

View File

@@ -20,6 +20,7 @@ then
export GIT_TEST_OE_DELTA_SIZE=5
export GIT_TEST_COMMIT_GRAPH=1
export GIT_TEST_MULTI_PACK_INDEX=1
export GIT_TEST_ADD_I_USE_BUILTIN=1
make test
fi

View File

@@ -284,7 +284,8 @@ extern int delayed_reachability_test(struct shallow_info *si, int c);
extern void prune_shallow(unsigned options);
extern struct trace_key trace_shallow;
extern int interactive_add(int argc, const char **argv, const char *prefix, int patch);
extern int interactive_add(int argc, const char **argv, const char *prefix,
const char *patch_mode);
extern int run_add_interactive(const char *revision, const char *patch_mode,
const struct pathspec *pathspec);

View File

@@ -1,12 +1,11 @@
#ifndef NO_INTTYPES_H
#include <inttypes.h>
#endif
#include "git-compat-util.h"
#include "run-command.h"
#include "cache.h"
#include "compat/terminal.h"
#include "sigchain.h"
#include "strbuf.h"
#include "cache.h"
#include "run-command.h"
#include "string-list.h"
#include "argv-array.h"
#include "hashmap.h"
#if defined(HAVE_DEV_TTY) || defined(GIT_WINDOWS_NATIVE)
@@ -37,7 +36,7 @@ static void restore_term(void)
term_fd = -1;
}
static int disable_echo(void)
static int disable_bits(tcflag_t bits)
{
struct termios t;
@@ -48,7 +47,7 @@ static int disable_echo(void)
old_term = t;
sigchain_push_common(restore_term_on_signal);
t.c_lflag &= ~ECHO;
t.c_lflag &= ~bits;
if (!tcsetattr(term_fd, TCSAFLUSH, &t))
return 0;
@@ -58,17 +57,44 @@ error:
return -1;
}
static int disable_echo(void)
{
return disable_bits(ECHO);
}
static int enable_non_canonical(void)
{
return disable_bits(ICANON | ECHO);
}
#elif defined(GIT_WINDOWS_NATIVE)
#define INPUT_PATH "CONIN$"
#define OUTPUT_PATH "CONOUT$"
#define FORCE_TEXT "t"
static int use_stty = 1;
static struct string_list stty_restore = STRING_LIST_INIT_DUP;
static HANDLE hconin = INVALID_HANDLE_VALUE;
static DWORD cmode;
static void restore_term(void)
{
if (use_stty) {
int i;
struct child_process cp = CHILD_PROCESS_INIT;
if (stty_restore.nr == 0)
return;
argv_array_push(&cp.args, "stty");
for (i = 0; i < stty_restore.nr; i++)
argv_array_push(&cp.args, stty_restore.items[i].string);
run_command(&cp);
string_list_clear(&stty_restore, 0);
return;
}
if (hconin == INVALID_HANDLE_VALUE)
return;
@@ -77,26 +103,48 @@ static void restore_term(void)
hconin = INVALID_HANDLE_VALUE;
}
static int set_echo(int echo)
static int disable_bits(DWORD bits)
{
DWORD new_cmode;
if (use_stty) {
struct child_process cp = CHILD_PROCESS_INIT;
if (hconin == INVALID_HANDLE_VALUE)
hconin = CreateFile("CONIN$", GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL);
argv_array_push(&cp.args, "stty");
if (bits & ENABLE_LINE_INPUT) {
string_list_append(&stty_restore, "icanon");
argv_array_push(&cp.args, "-icanon");
}
if (bits & ENABLE_ECHO_INPUT) {
string_list_append(&stty_restore, "echo");
argv_array_push(&cp.args, "-echo");
}
if (bits & ENABLE_PROCESSED_INPUT) {
string_list_append(&stty_restore, "-ignbrk");
string_list_append(&stty_restore, "intr");
string_list_append(&stty_restore, "^c");
argv_array_push(&cp.args, "ignbrk");
argv_array_push(&cp.args, "intr");
argv_array_push(&cp.args, "");
}
if (run_command(&cp) == 0)
return 0;
/* `stty` could not be executed; access the Console directly */
use_stty = 0;
}
hconin = CreateFile("CONIN$", GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL);
if (hconin == INVALID_HANDLE_VALUE)
return -1;
GetConsoleMode(hconin, &cmode);
new_cmode = cmode | ENABLE_LINE_INPUT;
if (echo)
new_cmode |= ENABLE_ECHO_INPUT;
else
new_cmode &= ~ENABLE_ECHO_INPUT;
sigchain_push_common(restore_term_on_signal);
if (!SetConsoleMode(hconin, new_cmode)) {
if (!SetConsoleMode(hconin, cmode & ~bits)) {
CloseHandle(hconin);
hconin = INVALID_HANDLE_VALUE;
return -1;
@@ -107,18 +155,52 @@ static int set_echo(int echo)
static int disable_echo(void)
{
return set_echo(0);
return disable_bits(ENABLE_ECHO_INPUT);
}
static int enable_non_canonical(void)
{
return disable_bits(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT);
}
/*
* Override `getchar()`, as the default implementation does not use
* `ReadFile()`.
*
* This poses a problem when we want to see whether the standard
* input has more characters, as the default of Git for Windows is to start the
* Bash in a MinTTY, which uses a named pipe to emulate a pty, in which case
* our `poll()` emulation calls `PeekNamedPipe()`, which seems to require
* `ReadFile()` to be called first to work properly (it only reports 0
* available bytes, otherwise).
*
* So let's just override `getchar()` with a version backed by `ReadFile()` and
* go our merry ways from here.
*/
static int mingw_getchar(void)
{
DWORD read = 0;
unsigned char ch;
if (!ReadFile(GetStdHandle(STD_INPUT_HANDLE), &ch, 1, &read, NULL))
return EOF;
if (!read) {
error("Unexpected 0 read");
return EOF;
}
return ch;
}
#define getchar mingw_getchar
static char *shell_prompt(const char *prompt, int echo)
{
const char *read_input[] = {
/* Note: call 'bash' explicitly, as 'read -s' is bash-specific */
"bash", "-c", echo ?
"test \"a$SHELL\" != \"a${SHELL%.exe}\" || exit 127; cat >/dev/tty &&"
" read -r line </dev/tty && echo \"$line\"" :
"test \"a$SHELL\" != \"a${SHELL%.exe}\" || exit 127; cat >/dev/tty &&"
" read -r -s line </dev/tty && echo \"$line\" && echo >/dev/tty",
"cat >/dev/tty && read -r line </dev/tty && echo \"$line\"" :
"cat >/dev/tty && read -r -s line </dev/tty && echo \"$line\" && echo >/dev/tty",
NULL
};
struct child_process child = CHILD_PROCESS_INIT;
@@ -154,10 +236,7 @@ ret:
close(child.out);
code = finish_command(&child);
if (code) {
if (code != 127)
error("failed to execute prompt script (exit code %d)",
code);
error("failed to execute prompt script (exit code %d)", code);
return NULL;
}
@@ -183,8 +262,6 @@ char *git_terminal_prompt(const char *prompt, int echo)
if (result)
return result;
if (echo && set_echo(1))
return NULL;
#endif
input_fh = fopen(INPUT_PATH, "r" FORCE_TEXT);
@@ -221,6 +298,125 @@ char *git_terminal_prompt(const char *prompt, int echo)
return buf.buf;
}
/*
* The `is_known_escape_sequence()` function returns 1 if the passed string
* corresponds to an Escape sequence that the terminal capabilities contains.
*
* To avoid depending on ncurses or other platform-specific libraries, we rely
* on the presence of the `infocmp` executable to do the job for us (failing
* silently if the program is not available or refused to run).
*/
struct escape_sequence_entry {
struct hashmap_entry entry;
char sequence[FLEX_ARRAY];
};
static int sequence_entry_cmp(const void *hashmap_cmp_fn_data,
const struct escape_sequence_entry *e1,
const struct escape_sequence_entry *e2,
const void *keydata)
{
return strcmp(e1->sequence, keydata ? keydata : e2->sequence);
}
static int is_known_escape_sequence(const char *sequence)
{
static struct hashmap sequences;
static int initialized;
if (!initialized) {
struct child_process cp = CHILD_PROCESS_INIT;
struct strbuf buf = STRBUF_INIT;
char *p, *eol;
hashmap_init(&sequences, (hashmap_cmp_fn)sequence_entry_cmp,
NULL, 0);
argv_array_pushl(&cp.args, "infocmp", "-L", "-1", NULL);
if (pipe_command(&cp, NULL, 0, &buf, 0, NULL, 0))
strbuf_setlen(&buf, 0);
for (eol = p = buf.buf; *p; p = eol + 1) {
p = strchr(p, '=');
if (!p)
break;
p++;
eol = strchrnul(p, '\n');
if (starts_with(p, "\\E")) {
char *comma = memchr(p, ',', eol - p);
struct escape_sequence_entry *e;
p[0] = '^';
p[1] = '[';
FLEX_ALLOC_MEM(e, sequence, p, comma - p);
hashmap_entry_init(e, strhash(e->sequence));
hashmap_add(&sequences, e);
}
if (!*eol)
break;
}
initialized = 1;
}
return !!hashmap_get_from_hash(&sequences, strhash(sequence), sequence);
}
int read_key_without_echo(struct strbuf *buf)
{
static int warning_displayed;
int ch;
if (warning_displayed || enable_non_canonical() < 0) {
if (!warning_displayed) {
warning("reading single keystrokes not supported on "
"this platform; reading line instead");
warning_displayed = 1;
}
return strbuf_getline(buf, stdin);
}
strbuf_reset(buf);
ch = getchar();
if (ch == EOF) {
restore_term();
return EOF;
}
strbuf_addch(buf, ch);
if (ch == '\033' /* ESC */) {
/*
* We are most likely looking at an Escape sequence. Let's try
* to read more bytes, waiting at most half a second, assuming
* that the sequence is complete if we did not receive any byte
* within that time.
*
* Start by replacing the Escape byte with ^[ */
strbuf_splice(buf, buf->len - 1, 1, "^[", 2);
/*
* Query the terminal capabilities once about all the Escape
* sequences it knows about, so that we can avoid waiting for
* half a second when we know that the sequence is complete.
*/
while (!is_known_escape_sequence(buf->buf)) {
struct pollfd pfd = { .fd = 0, .events = POLLIN };
if (poll(&pfd, 1, 500) < 1)
break;
ch = getchar();
if (ch == EOF)
return 0;
strbuf_addch(buf, ch);
}
}
restore_term();
return 0;
}
#else
char *git_terminal_prompt(const char *prompt, int echo)
@@ -228,4 +424,23 @@ char *git_terminal_prompt(const char *prompt, int echo)
return getpass(prompt);
}
int read_key_without_echo(struct strbuf *buf)
{
static int warning_displayed;
const char *res;
if (!warning_displayed) {
warning("reading single keystrokes not supported on this "
"platform; reading line instead");
warning_displayed = 1;
}
res = getpass("");
strbuf_reset(buf);
if (!res)
return EOF;
strbuf_addstr(buf, res);
return 0;
}
#endif

View File

@@ -3,4 +3,7 @@
char *git_terminal_prompt(const char *prompt, int echo);
/* Read a single keystroke, without echoing it to the terminal */
int read_key_without_echo(struct strbuf *buf);
#endif /* COMPAT_TERMINAL_H */

37
diff.c
View File

@@ -2489,22 +2489,6 @@ static void pprint_rename(struct strbuf *name, const char *a, const char *b)
}
}
struct diffstat_t {
int nr;
int alloc;
struct diffstat_file {
char *from_name;
char *name;
char *print_name;
const char *comments;
unsigned is_unmerged:1;
unsigned is_binary:1;
unsigned is_renamed:1;
unsigned is_interesting:1;
uintmax_t added, deleted;
} **files;
};
static struct diffstat_file *diffstat_add(struct diffstat_t *diffstat,
const char *name_a,
const char *name_b)
@@ -6005,12 +5989,7 @@ void diff_flush(struct diff_options *options)
dirstat_by_line) {
struct diffstat_t diffstat;
memset(&diffstat, 0, sizeof(struct diffstat_t));
for (i = 0; i < q->nr; i++) {
struct diff_filepair *p = q->queue[i];
if (check_pair_status(p))
diff_flush_stat(p, options, &diffstat);
}
compute_diffstat(options, &diffstat, q);
if (output_format & DIFF_FORMAT_NUMSTAT)
show_numstat(&diffstat, options);
if (output_format & DIFF_FORMAT_DIFFSTAT)
@@ -6310,6 +6289,20 @@ static int is_submodule_ignored(const char *path, struct diff_options *options)
return ignored;
}
void compute_diffstat(struct diff_options *options,
struct diffstat_t *diffstat,
struct diff_queue_struct *q)
{
int i;
memset(diffstat, 0, sizeof(struct diffstat_t));
for (i = 0; i < q->nr; i++) {
struct diff_filepair *p = q->queue[i];
if (check_pair_status(p))
diff_flush_stat(p, options, diffstat);
}
}
void diff_addremove(struct diff_options *options,
int addremove, unsigned mode,
const struct object_id *oid,

19
diff.h
View File

@@ -240,6 +240,22 @@ void diff_emit_submodule_error(struct diff_options *o, const char *err);
void diff_emit_submodule_pipethrough(struct diff_options *o,
const char *line, int len);
struct diffstat_t {
int nr;
int alloc;
struct diffstat_file {
char *from_name;
char *name;
char *print_name;
const char *comments;
unsigned is_unmerged:1;
unsigned is_binary:1;
unsigned is_renamed:1;
unsigned is_interesting:1;
uintmax_t added, deleted;
} **files;
};
enum color_diff {
DIFF_RESET = 0,
DIFF_CONTEXT = 1,
@@ -328,6 +344,9 @@ void diff_change(struct diff_options *,
struct diff_filepair *diff_unmerge(struct diff_options *, const char *path);
void compute_diffstat(struct diff_options *options, struct diffstat_t *diffstat,
struct diff_queue_struct *q);
#define DIFF_SETUP_REVERSE 1
#define DIFF_SETUP_USE_SIZE_CACHE 4

View File

@@ -181,7 +181,9 @@ sub run_cmd_pipe {
} else {
my $fh = undef;
open($fh, '-|', @_) or die;
return <$fh>;
my @out = <$fh>;
close $fh || die "Cannot close @_ ($!)";
return @out;
}
}
@@ -228,7 +230,7 @@ my $status_head = sprintf($status_fmt, __('staged'), __('unstaged'), __('path'))
sub get_empty_tree {
return $empty_tree if defined $empty_tree;
$empty_tree = run_cmd_pipe(qw(git hash-object -t tree /dev/null));
($empty_tree) = run_cmd_pipe(qw(git hash-object -t tree /dev/null));
chomp $empty_tree;
return $empty_tree;
}
@@ -1121,7 +1123,7 @@ aborted and the hunk is left unchanged.
EOF2
close $fh;
chomp(my $editor = run_cmd_pipe(qw(git var GIT_EDITOR)));
chomp(my ($editor) = run_cmd_pipe(qw(git var GIT_EDITOR)));
system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
if ($? != 0) {

View File

@@ -206,13 +206,13 @@ create_stash () {
# find out what the user wants
GIT_INDEX_FILE="$TMP-index" \
git add--interactive --patch=stash -- "$@" &&
git add --patch=stash -- "$@" &&
# state of the working tree
w_tree=$(GIT_INDEX_FILE="$TMP-index" git write-tree) ||
die "$(gettext "Cannot save the current worktree state")"
git diff-tree -p HEAD $w_tree -- >"$TMP-patch" &&
git diff-tree -p -U1 HEAD $w_tree -- >"$TMP-patch" &&
test -s "$TMP-patch" ||
die "$(gettext "No changes selected")"

111
prefix-map.c Normal file
View File

@@ -0,0 +1,111 @@
#include "cache.h"
#include "prefix-map.h"
static int map_cmp(const void *unused_cmp_data,
const void *entry,
const void *entry_or_key,
const void *unused_keydata)
{
const struct prefix_map_entry *a = entry;
const struct prefix_map_entry *b = entry_or_key;
return a->prefix_length != b->prefix_length ||
strncmp(a->name, b->name, a->prefix_length);
}
static void add_prefix_entry(struct hashmap *map, const char *name,
size_t prefix_length, struct prefix_item *item)
{
struct prefix_map_entry *result = xmalloc(sizeof(*result));
result->name = name;
result->prefix_length = prefix_length;
result->item = item;
hashmap_entry_init(result, memhash(name, prefix_length));
hashmap_add(map, result);
}
static void init_prefix_map(struct prefix_map *prefix_map,
int min_prefix_length, int max_prefix_length)
{
hashmap_init(&prefix_map->map, map_cmp, NULL, 0);
prefix_map->min_length = min_prefix_length;
prefix_map->max_length = max_prefix_length;
}
static void add_prefix_item(struct prefix_map *prefix_map,
struct prefix_item *item)
{
struct prefix_map_entry *e = xmalloc(sizeof(*e)), *e2;
int j;
e->item = item;
e->name = e->item->name;
for (j = prefix_map->min_length; j <= prefix_map->max_length; j++) {
if (!isascii(e->name[j])) {
free(e);
break;
}
e->prefix_length = j;
hashmap_entry_init(e, memhash(e->name, j));
e2 = hashmap_get(&prefix_map->map, e, NULL);
if (!e2) {
/* prefix is unique so far */
e->item->prefix_length = j;
hashmap_add(&prefix_map->map, e);
break;
}
if (!e2->item)
continue; /* non-unique prefix */
if (j != e2->item->prefix_length)
BUG("unexpected prefix length: %d != %d",
(int)j, (int)e2->item->prefix_length);
/* skip common prefix */
for (; j < prefix_map->max_length && e->name[j]; j++) {
if (e->item->name[j] != e2->item->name[j])
break;
add_prefix_entry(&prefix_map->map, e->name, j + 1,
NULL);
}
/* e2 no longer refers to a unique prefix */
if (j < prefix_map->max_length && e2->name[j]) {
/* found a new unique prefix for e2's item */
e2->item->prefix_length = j + 1;
add_prefix_entry(&prefix_map->map, e2->name, j + 1,
e2->item);
}
else
e2->item->prefix_length = 0;
e2->item = NULL;
if (j < prefix_map->max_length && e->name[j]) {
/* found a unique prefix for the item */
e->item->prefix_length = j + 1;
add_prefix_entry(&prefix_map->map, e->name, j + 1,
e->item);
} else {
/* item has no (short enough) unique prefix */
e->item->prefix_length = 0;
free(e);
}
break;
}
}
void find_unique_prefixes(struct prefix_item **list, size_t nr,
int min_length, int max_length)
{
int i;
struct prefix_map prefix_map;
init_prefix_map(&prefix_map, min_length, max_length);
for (i = 0; i < nr; i++)
add_prefix_item(&prefix_map, list[i]);
hashmap_free(&prefix_map.map, 1);
}

40
prefix-map.h Normal file
View File

@@ -0,0 +1,40 @@
#ifndef PREFIX_MAP_H
#define PREFIX_MAP_H
#include "hashmap.h"
struct prefix_item {
const char *name;
size_t prefix_length;
};
struct prefix_map_entry {
struct hashmap_entry e;
const char *name;
size_t prefix_length;
/* if item is NULL, the prefix is not unique */
struct prefix_item *item;
};
struct prefix_map {
struct hashmap map;
int min_length, max_length;
};
/*
* Find unique prefixes in a given list of strings.
*
* Typically, the `struct prefix_item` information will be but a field in the
* actual item struct; For this reason, the `list` parameter is specified as a
* list of pointers to the items.
*
* The `min_length`/`max_length` parameters define what length the unique
* prefixes should have.
*
* If no unique prefix could be found for a given item, its `prefix_length`
* will be set to 0.
*/
void find_unique_prefixes(struct prefix_item **list, size_t nr,
int min_length, int max_length);
#endif

View File

@@ -272,3 +272,22 @@ int repo_hold_locked_index(struct repository *repo,
BUG("the repo hasn't been setup");
return hold_lock_file_for_update(lf, repo->index_file, flags);
}
int repo_refresh_and_write_index(struct repository *r,
unsigned int flags, int gentle)
{
struct lock_file lock_file = LOCK_INIT;
int fd;
if (repo_read_index_preload(r, NULL, 0) < 0)
return error(_("could not read index"));
fd = repo_hold_locked_index(r, &lock_file, 0);
if (!gentle && fd < 0)
return error(_("could not lock index for writing"));
refresh_index(r->index, flags, NULL, NULL, NULL);
if (0 <= fd)
repo_update_index_if_able(r, &lock_file);
rollback_lock_file(&lock_file);
return 0;
}

View File

@@ -154,5 +154,12 @@ int repo_read_index_unmerged(struct repository *);
*/
void repo_update_index_if_able(struct repository *, struct lock_file *);
/*
* Refresh the index and write it out. If the index file could not be
* locked, error out, except in gentle mode. The flags will be passed
* through to refresh_index().
*/
int repo_refresh_and_write_index(struct repository *r,
unsigned int flags, int gentle);
#endif /* REPOSITORY_H */

View File

@@ -387,6 +387,10 @@ GIT_TEST_STASH_USE_BUILTIN=<boolean>, when false, disables the
built-in version of git-stash. See 'stash.useBuiltin' in
git-config(1).
GIT_TEST_ADD_I_USE_BUILTIN=<boolean>, when true, enables the
builtin version of git add -i. See 'add.interactive.useBuiltin' in
git-config(1).
GIT_TEST_INDEX_THREADS=<n> enables exercising the multi-threaded loading
of the index for the whole test suite by bypassing the default number of
cache entries and thread minimums. Setting this to 1 will make the

View File

@@ -0,0 +1,58 @@
#include "test-tool.h"
#include "cache.h"
#include "prefix-map.h"
static size_t test_count, failed_count;
static void check(int succeeded, const char *file, size_t line_no,
const char *fmt, ...)
{
va_list ap;
test_count++;
if (succeeded)
return;
va_start(ap, fmt);
fprintf(stderr, "%s:%d: ", file, (int)line_no);
vfprintf(stderr, fmt, ap);
fputc('\n', stderr);
va_end(ap);
failed_count++;
}
#define EXPECT_SIZE_T_EQUALS(expect, actual, hint) \
check(expect == actual, __FILE__, __LINE__, \
"size_t's do not match: %" \
PRIdMAX " != %" PRIdMAX " (%s) (%s)", \
(intmax_t)expect, (intmax_t)actual, #actual, hint)
int cmd__prefix_map(int argc, const char **argv)
{
#define NR 5
struct prefix_item items[NR] = {
{ "unique" },
{ "hell" },
{ "hello" },
{ "wok" },
{ "world" },
};
struct prefix_item *list[NR] = {
items, items + 1, items + 2, items + 3, items + 4
};
find_unique_prefixes(list, NR, 1, 3);
#define EXPECT_PREFIX_LENGTH_EQUALS(expect, index) \
EXPECT_SIZE_T_EQUALS(expect, list[index]->prefix_length, \
list[index]->name)
EXPECT_PREFIX_LENGTH_EQUALS(1, 0);
EXPECT_PREFIX_LENGTH_EQUALS(0, 1);
EXPECT_PREFIX_LENGTH_EQUALS(0, 2);
EXPECT_PREFIX_LENGTH_EQUALS(3, 3);
EXPECT_PREFIX_LENGTH_EQUALS(3, 4);
return !!failed_count;
}

View File

@@ -34,6 +34,7 @@ static struct test_cmd cmds[] = {
{ "parse-options", cmd__parse_options },
{ "path-utils", cmd__path_utils },
{ "pkt-line", cmd__pkt_line },
{ "prefix-map", cmd__prefix_map },
{ "prio-queue", cmd__prio_queue },
{ "reach", cmd__reach },
{ "read-cache", cmd__read_cache },

View File

@@ -31,6 +31,7 @@ int cmd__online_cpus(int argc, const char **argv);
int cmd__parse_options(int argc, const char **argv);
int cmd__path_utils(int argc, const char **argv);
int cmd__pkt_line(int argc, const char **argv);
int cmd__prefix_map(int argc, const char **argv);
int cmd__prio_queue(int argc, const char **argv);
int cmd__reach(int argc, const char **argv);
int cmd__read_cache(int argc, const char **argv);

10
t/t0016-prefix-map.sh Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/sh
test_description='basic tests for prefix map'
. ./test-lib.sh
test_expect_success 'prefix map' '
test-tool prefix-map
'
test_done

View File

@@ -23,6 +23,17 @@ diff_cmp () {
test_cmp "$1.filtered" "$2.filtered"
}
# This function uses a trick to manipulate the interactive add to use color:
# the `want_color()` function special-cases the situation where a pager was
# spawned and Git now wants to output colored text: to detect that situation,
# the environment variable `GIT_PAGER_IN_USE` is set. However, color is
# suppressed despite that environment variable if the `TERM` variable
# indicates a dumb terminal, so we set that variable, too.
force_color () {
env GIT_PAGER_IN_USE=true TERM=vt100 "$@"
}
test_expect_success 'setup (initial)' '
echo content >file &&
git add file &&
@@ -94,7 +105,6 @@ test_expect_success 'revert works (commit)' '
grep "unchanged *+3/-0 file" output
'
test_expect_success 'setup expected' '
cat >expected <<-\EOF
EOF
@@ -263,6 +273,35 @@ test_expect_success FILEMODE 'stage mode and hunk' '
# end of tests disabled when filemode is not usable
test_expect_success 'different prompts for mode change/deleted' '
git reset --hard &&
>file &&
>deleted &&
git add --chmod=+x file deleted &&
echo changed >file &&
rm deleted &&
test_write_lines n n n |
git -c core.filemode=true add -p >actual &&
sed -n "s/^\(Stage .*?\).*/\1/p" actual >actual.filtered &&
cat >expect <<-\EOF &&
Stage deletion [y,n,q,a,d,?]?
Stage mode change [y,n,q,a,d,j,J,g,/,?]?
Stage this hunk [y,n,q,a,d,K,g,/,e,?]?
EOF
test_cmp expect actual.filtered
'
test_expect_success 'correct message when there is nothing to do' '
git reset --hard &&
git add -p 2>err &&
test_i18ngrep "No changes" err &&
printf "\\0123" >binary &&
git add binary &&
printf "\\0abc" >binary &&
git add -p 2>err &&
test_i18ngrep "Only binary files changed" err
'
test_expect_success 'setup again' '
git reset --hard &&
test_chmod +x file &&
@@ -374,6 +413,36 @@ test_expect_success 'split hunk setup' '
test_write_lines 10 15 20 21 22 23 24 30 40 50 60 >test
'
test_expect_success 'goto hunk' '
test_when_finished "git reset" &&
tr _ " " >expect <<-EOF &&
Stage this hunk [y,n,q,a,d,K,g,/,e,?]? + 1: -1,2 +1,3 +15
_ 2: -2,4 +3,8 +21
go to which hunk? @@ -1,2 +1,3 @@
_10
+15
_20
Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]?_
EOF
test_write_lines s y g 1 | git add -p >actual &&
tail -n 7 <actual >actual.trimmed &&
test_cmp expect actual.trimmed
'
test_expect_success 'navigate to hunk via regex' '
test_when_finished "git reset" &&
tr _ " " >expect <<-EOF &&
Stage this hunk [y,n,q,a,d,K,g,/,e,?]? @@ -1,2 +1,3 @@
_10
+15
_20
Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]?_
EOF
test_write_lines s y /1,2 | git add -p >actual &&
tail -n 5 <actual >actual.trimmed &&
test_cmp expect actual.trimmed
'
test_expect_success 'split hunk "add -p (edit)"' '
# Split, say Edit and do nothing. Then:
#
@@ -403,6 +472,28 @@ test_expect_failure 'split hunk "add -p (no, yes, edit)"' '
! grep "^+31" actual
'
test_expect_failure 'edit, adding lines to the first hunk' '
test_write_lines 10 11 20 30 40 50 51 60 >test &&
git reset &&
tr _ " " >patch <<-EOF &&
@@ -1,5 +1,6 @@
_10
+11
+12
_20
+21
+22
_30
EOF
# test sequence is s(plit), e(dit), n(o)
# q n q q is there to make sure we exit at the end.
printf "%s\n" s e n q n q q |
EDITOR=./fake_editor.sh git add -p 2>error &&
test_must_be_empty error &&
git diff --cached >actual &&
grep "^+22" actual
'
test_expect_success 'patch mode ignores unmerged entries' '
git reset --hard &&
test_commit conflict &&
@@ -429,35 +520,45 @@ test_expect_success 'patch mode ignores unmerged entries' '
diff_cmp expected diff
'
test_expect_success TTY 'diffs can be colorized' '
test_expect_success 'diffs can be colorized' '
git reset --hard &&
echo content >test &&
printf y | test_terminal git add -p >output 2>&1 &&
printf y | force_color git add -p >output 2>&1 &&
# We do not want to depend on the exact coloring scheme
# git uses for diffs, so just check that we saw some kind of color.
grep "$(printf "\\033")" output
'
test_expect_success TTY 'diffFilter filters diff' '
test_expect_success 'diffFilter filters diff' '
git reset --hard &&
echo content >test &&
test_config interactive.diffFilter "sed s/^/foo:/" &&
printf y | test_terminal git add -p >output 2>&1 &&
printf y | force_color git add -p >output 2>&1 &&
# avoid depending on the exact coloring or content of the prompts,
# and just make sure we saw our diff prefixed
grep foo:.*content output
'
test_expect_success TTY 'detect bogus diffFilter output' '
test_expect_success 'detect bogus diffFilter output' '
git reset --hard &&
echo content >test &&
test_config interactive.diffFilter "echo too-short" &&
printf y | test_must_fail test_terminal git add -p
printf y | test_must_fail force_color git add -p
'
test_expect_success 'diff.algorithm is passed to `git diff-files`' '
git reset --hard &&
>file &&
git add file &&
echo changed >file &&
test_must_fail git -c diff.algorithm=bogus add -p 2>err &&
test_i18ngrep "error: option diff-algorithm accepts " err
'
test_expect_success 'patch-mode via -i prompts for files' '
@@ -660,4 +761,28 @@ test_expect_success EXPENSIVE 'add -i with a lot of files' '
git reset --hard
'
test_expect_success 'show help from add--helper' '
git reset --hard &&
cat >expect <<-EOF &&
<BOLD>*** Commands ***<RESET>
1: <BOLD;BLUE>s<RESET>tatus 2: <BOLD;BLUE>u<RESET>pdate 3: <BOLD;BLUE>r<RESET>evert 4: <BOLD;BLUE>a<RESET>dd untracked
5: <BOLD;BLUE>p<RESET>atch 6: <BOLD;BLUE>d<RESET>iff 7: <BOLD;BLUE>q<RESET>uit 8: <BOLD;BLUE>h<RESET>elp
<BOLD;BLUE>What now<RESET>> <BOLD;RED>status - show paths with changes<RESET>
<BOLD;RED>update - add working tree state to the staged set of changes<RESET>
<BOLD;RED>revert - revert staged set of changes back to the HEAD version<RESET>
<BOLD;RED>patch - pick hunks and update selectively<RESET>
<BOLD;RED>diff - view diff between HEAD and index<RESET>
<BOLD;RED>add untracked - add contents of untracked files to the staged set of changes<RESET>
<BOLD>*** Commands ***<RESET>
1: <BOLD;BLUE>s<RESET>tatus 2: <BOLD;BLUE>u<RESET>pdate 3: <BOLD;BLUE>r<RESET>evert 4: <BOLD;BLUE>a<RESET>dd untracked
5: <BOLD;BLUE>p<RESET>atch 6: <BOLD;BLUE>d<RESET>iff 7: <BOLD;BLUE>q<RESET>uit 8: <BOLD;BLUE>h<RESET>elp
<BOLD;BLUE>What now<RESET>>$SP
Bye.
EOF
test_write_lines h | force_color git add -i >actual.colored &&
test_decode_color <actual.colored >actual &&
test_i18ncmp expect actual
'
test_done

View File

@@ -89,7 +89,7 @@ test_expect_success 'none of this moved HEAD' '
verify_saved_head
'
test_expect_failure 'stash -p with split hunk' '
test_expect_success 'stash -p with split hunk' '
git reset --hard &&
cat >test <<-\EOF &&
aaa
@@ -106,8 +106,8 @@ test_expect_failure 'stash -p with split hunk' '
ccc
EOF
printf "%s\n" s n y q |
test_might_fail git stash -p 2>error &&
! test_must_be_empty error &&
git stash -p 2>error &&
test_must_be_empty error &&
grep "added line 1" test &&
! grep "added line 2" test
'