Skip to contentSkip to conversation

git add -i: add a rudimentary version in C (supporting only status and help so far)

PATCH Superseded Johannes Schindelin via GitGitGadget 6 participants 124 messages Apr 10, 2019 – Nov 15, 2019 Page 1 of 2

Subsystems (2)
lore
Share & tools
lore
#0 lore raw

cover git add -i: add a rudimentary version in C (supporting only status and help so far)

This is the first leg on the long journey to a fully built-in git add -i (next up: parts 2 [https://github.com/gitgitgadget/git/pull/171], 3
[https://github.com/gitgitgadget/git/pull/172], 4
[https://github.com/gitgitgadget/git/pull/173], 5
[https://github.com/gitgitgadget/git/pull/174], and 6
[https://github.com/gitgitgadget/git/pull/175]).

It reflects the part that was submitted a couple of times (see
https://github.com/gitgitgadget/git/pull/103) during the Outreachy project by Slavica Ðukic that continued the journey based on an initial patch series by Daniel Ferreira.

This part only implements the status and the help part, like Slavica's last iteration did, in the interest of making the review remotely more reviewable. I fear that this attempt of making it a bit more reviewable is pretty futile, as so many things changed. So I will ask the reviewers for
forgiveness: please be kind, and give this sort of a fresh review.

I threw in a couple of major changes on top of that iteration, though:

* The original plan was to add a helper (git add--helper) that takes over more and more responsibility from the Perl script over the course of the conversion.

This plan is no longer in effect, as I encountered a serious problem with
that: the MSYS2 runtime used by the Perl interpreter which Git for Windows employs to run git add -i has a curious bug (that is safely outside the purview of this here patch series) where it fails to read from standard input after it spawned a non-MSYS2 program (such as add--helper) that reads from standard input. So that wouldn't work, but quit the interactive add as soon as even one interactive command had been handled by the helper. To keep my git add -i in a working state, I therefore adopted a different strategy:

Just like git difftool was converted by starting with a built-in that did nothing but handing off to the scripted version, guarded by the (opt-in) difftool.useBuiltin config setting, I start this patch series by a built-in add -i that does nothing else but state that it is not implemented yet, guarded by the (opt-in) add.interactive.useBuiltin config setting.

In contrast to the git difftool situation, it is quite a bit easier here, as we do not even have to rename the script to git-legacy-add--interactive.perl: the add--interactive command is an implementation detail that users are not even supposed to know about. Therefore, we can implement that road fork between the built-in and the scripted version in builtin/add.c, i.e. in the user-facing git add command.

This will also naturally help with the transition to a fully built-in git add -i/git add -p, as we saw with the built-in git rebase how important it is for end users to have an escape hatch (and for that reason, tried our best to provide the same with the built-in git stash).

* The help command was actually not hooked up in git add -i in Slavica's last iteration, but was only available as a special option of the git add--helper command. As that command no longer exists, I kind of had to implement some way to let the built-in git add -i show the help text.

* The main loop of git add -i (i.e. the thing that lets you choose status or help) is now implemented (but only lists status and help, of course), as it makes use of that feature that took the main chunk of the Outreachy
project: the function to determine unique prefixes of a list of strings.

* Speaking of the unique prefixes: the functionality to determine those is now encapsulated in the prefix-map.c file, and I also added a regression test.

* Speaking of the tests: I also implemented support for the environment variable GIT_TEST_ADD_I_USE_BUILTIN: by setting it, the test suite can be forced to use the built-in, or the Perl script, version of git add -i. Needless to say: by the end of this patch series, running the test suite with GIT_TEST_ADD_I_USE_BUILTIN=true will still result in a ton of test failures due to not-yet-implemented commands, but it will also demonstrate what already works.

* Since the main loop starts not only by showing the status, but refreshes the index before that, I added that, and I actually refactored that code into a new function (repo_refresh_and_write_index()), as it will be used a couple of times by the end of the complete conversion of git add -i into a built-in command.

Daniel Ferreira (2):
diff: export diffstat interface
built-in add -i: implement the status command

Johannes Schindelin (6):
Start to implement a built-in version of git add --interactive built-in add -i: refresh the index before running status built-in add -i: color the header in the status command built-in add -i: implement the main loop
built-in add -i: support ? (prompt help)
built-in add -i: implement the help command

Slavica Djukic (3):
Add a function to determine unique prefixes for a list of strings built-in add -i: show unique prefixes of the commands built-in add -i: use color in the main loop

 Documentation/config/add.txt |   5 +
 Makefile                     |   3 +
 add-interactive.c            | 560 +++++++++++++++++++++++++++++++++++
 add-interactive.h            |  10 +
 builtin/add.c                |  16 +-
 diff.c                       |  37 +--
 diff.h                       |  19 ++
 prefix-map.c                 | 111 +++++++
 prefix-map.h                 |  40 +++
 repository.c                 |  19 ++
 repository.h                 |   7 +
 t/README                     |   4 +
 t/helper/test-prefix-map.c   |  58 ++++
 t/helper/test-tool.c         |   1 +
 t/helper/test-tool.h         |   1 +
 t/t0016-prefix-map.sh        |  10 +
 t/t3701-add-interactive.sh   |  24 ++
 17 files changed, 902 insertions(+), 23 deletions(-)
 create mode 100644 add-interactive.c
 create mode 100644 add-interactive.h
 create mode 100644 prefix-map.c
 create mode 100644 prefix-map.h
 create mode 100644 t/helper/test-prefix-map.c
 create mode 100755 t/t0016-prefix-map.sh

base-commit: 8104ec994ea3849a968b4667d072fedd1e688642
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-170%2Fdscho%2Fadd-i-in-c-status-and-help-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-170/dscho/add-i-in-c-status-and-help-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/170

Signature · 1 line
gitgitgadget
#1 lore raw

PATCH 1/11 Start to implement a built-in version of `git add --interactive`

From: Johannes Schindelin <[email protected]>

This is hardly the first conversion of a Git command that is implemented as a script to a built-in. So far, the most successful strategy for such conversions has been to add a built-in helper and call that for more and more functionality from the script, as more and more parts are converted.

With the interactive add, we choose a different strategy. The sole reason for this is that on Windows (where such a conversion has the most benefits in terms of speed and robustness) we face the very specific problem that a system() call in Perl seems to close stdin in the parent process when the spawned process consumes even one character from stdin. And that just does not work for us here, as it would stop the main loop as soon as any interactive command was performed by the helper. Which is almost all of the commands in git add -i.

It is almost as if Perl told us once again that it does not want us to use it on Windows.

Instead, we follow the opposite route where we start with a bare-bones version of the built-in interactive add, guarded by the new add.interactive.useBuiltin config variable, and then add more and more functionality to it, until it is feature complete.

At this point, the built-in version of git add -i only states that it cannot do anything yet ;-)

---
 Documentation/config/add.txt |  5 +++++
 Makefile                     |  1 +
 add-interactive.c            | 13 +++++++++++++
 add-interactive.h            | 10 ++++++++++
 builtin/add.c                | 16 +++++++++++++++-
 t/README                     |  4 ++++
 6 files changed, 48 insertions(+), 1 deletion(-)
 create mode 100644 add-interactive.c
 create mode 100644 add-interactive.h
Documentation/config/add.txt +5 -0
index 4d753f006e..c9f748f81c 100644
@@ -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.
Makefile +1 -0
index c5240942f2..18e656a32f 100644
@@ -848,6 +848,7 @@ LIB_H = $(shell $(FIND) . \
 	-name '*.h' -print)
 
 LIB_OBJS += abspath.o
+LIB_OBJS += add-interactive.o
 LIB_OBJS += advice.o
 LIB_OBJS += alias.o
 LIB_OBJS += alloc.o
add-interactive.c new file +13 -0
new file mode 100644
index 0000000000..540bf185d8
@@ -0,0 +1,13 @@
+#include "cache.h"
+#include "add-interactive.h"
+#include "config.h"
+
+int add_i_config(const char *var, const char *value, void *cb)
+{
+	return git_default_config(var, value, cb);
+}
+
+int run_add_i(struct repository *r, const struct pathspec *ps)
+{
+	die(_("No commands are available in the built-in `git add -i` yet!"));
+}
add-interactive.h new file +10 -0
new file mode 100644
index 0000000000..e6e6e051eb
@@ -0,0 +1,10 @@
+#ifndef ADD_INTERACTIVE_H
+#define ADD_INTERACTIVE_H
+
+int add_i_config(const char *var, const char *value, void *cb);
+
+struct repository;
+struct pathspec;
+int run_add_i(struct repository *r, const struct pathspec *ps);
+
+#endif
builtin/add.c +15 -1
index db2dfa4350..5a32a755c8 100644
@@ -20,6 +20,7 @@
 #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>..."),
@@ -28,6 +29,7 @@ static const char * const builtin_add_usage[] = {
 static int patch_interactive, add_interactive, edit_interactive;
 static int take_worktree_changes;
 static int add_renormalize;
+static int use_builtin_add_i;
 
 struct update_callback_data {
 	int flags;
@@ -186,6 +188,9 @@ int run_add_interactive(const char *revision, const char *patch_mode,
 	int status, i;
 	struct argv_array argv = ARGV_ARRAY_INIT;
 
+	if (use_builtin_add_i && !patch_mode)
+		return !!run_add_i(the_repository, pathspec);
+
 	argv_array_push(&argv, "add--interactive");
 	if (patch_mode)
 		argv_array_push(&argv, patch_mode);
@@ -319,7 +324,12 @@ 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);
+	if (!strcmp(var, "add.interactive.usebuiltin")) {
+		use_builtin_add_i = git_config_bool(var, value);
+		return 0;
+	}
+
+	return add_i_config(var, value, cb);
 }
 
 static const char embedded_advice[] = N_(
@@ -394,8 +404,12 @@ int cmd_add(int argc, const char **argv, const char *prefix)
 	int require_pathspec;
 	char *seen = NULL;
 	struct lock_file lock_file = LOCK_INIT;
+	int use_builtin_add_i_env =
+		git_env_bool("GIT_TEST_ADD_I_USE_BUILTIN", -1);
 
 	git_config(add_config, NULL);
+	if (use_builtin_add_i_env >= 0)
+		use_builtin_add_i = use_builtin_add_i_env;
 
 	argc = parse_options(argc, argv, prefix, builtin_add_options,
 			  builtin_add_usage, PARSE_OPT_KEEP_ARGV0);
t/README +4 -0
index 886bbec5bc..6408a1847e 100644
@@ -383,6 +383,10 @@ GIT_TEST_REBASE_USE_BUILTIN=<boolean>, when false, disables the
 builtin version of git-rebase. See 'rebase.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
Signature · 1 line
gitgitgadget
#2 lore raw

Re: [PATCH 01/11] Start to implement a built-in version of `git add --interactive`

Quoted 79 lines · Johannes Schindelin via GitGitGadget

From: Johannes Schindelin <[email protected]>

This is hardly the first conversion of a Git command that is implemented as a script to a built-in. So far, the most successful strategy for such conversions has been to add a built-in helper and call that for more and more functionality from the script, as more and more parts are converted.

With the interactive add, we choose a different strategy. The sole reason for this is that on Windows (where such a conversion has the most benefits in terms of speed and robustness) we face the very specific problem that a system() call in Perl seems to close stdin in the parent process when the spawned process consumes even one character from stdin. And that just does not work for us here, as it would stop the main loop as soon as any interactive command was performed by the helper. Which is almost all of the commands in git add -i.

It is almost as if Perl told us once again that it does not want us to use it on Windows.

Instead, we follow the opposite route where we start with a bare-bones version of the built-in interactive add, guarded by the new add.interactive.useBuiltin config variable, and then add more and more functionality to it, until it is feature complete.

At this point, the built-in version of git add -i only states that it cannot do anything yet ;-)

---
  Documentation/config/add.txt |  5 +++++
  Makefile                     |  1 +
  add-interactive.c            | 13 +++++++++++++
  add-interactive.h            | 10 ++++++++++
  builtin/add.c                | 16 +++++++++++++++-
  t/README                     |  4 ++++
  6 files changed, 48 insertions(+), 1 deletion(-)

create mode 100644 add-interactive.c
create mode 100644 add-interactive.h

Documentation/config/add.txt +5 -0
index 4d753f006e..c9f748f81c 100644
@@ -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.
Makefile +1 -0
index c5240942f2..18e656a32f 100644
@@ -848,6 +848,7 @@ LIB_H = $(shell $(FIND) . \
  	-name '*.h' -print)
 
  LIB_OBJS += abspath.o
+LIB_OBJS += add-interactive.o
  LIB_OBJS += advice.o
  LIB_OBJS += alias.o
  LIB_OBJS += alloc.o
add-interactive.c new file +8 -0
new file mode 100644
index 0000000000..540bf185d8
@@ -0,0 +1,13 @@
+#include "cache.h"
+#include "add-interactive.h"
+#include "config.h"
+
+int add_i_config(const char *var, const char *value, void *cb)
+{
+	return git_default_config(var, value, cb);
+}

[...]

Quoted 4 lines
builtin/add.c +0 -0
index db2dfa4350..5a32a755c8 100644

[...]

Quoted 1 line

+static int use_builtin_add_i;

[...]

builtin/add.c static int add_config(const char *var, const char *value, void *cb) · line 333

Quoted 12 lines
builtin/add.c +6 -1
@@ -319,7 +324,12 @@ 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);
+	if (!strcmp(var, "add.interactive.usebuiltin")) {
+		use_builtin_add_i = git_config_bool(var, value);
+		return 0;
+	}
+
+	return add_i_config(var, value, cb);
  }

Something about this split between add_config() in this file and add_i_config() in add-interactive.c bothers me. I'm not saying it is wrong, but it bothers me.

Perhaps it is that we moved the call to git_default_config() to add-interactive.c -- so correct behavior of the non-interactive case depends on add-interactive.c to do the right thing.

Also, since we can't control the order of observed the k/v pairs, we can't do "if (use_builtin_add_i) return add_i_config(...);" And that wouldn't allow both versions to pickup a common config setting (in their own static variables).

Currently, neither function looks at any other k/v pairs, so this is a bit of a moot point, but I'm wondering if this should look like this:

    int add_config(...)
    {
        // give add-interactive.c a chance to look at k/v pair, but
        // do not short-cut because we don't know yet whether we
        // will be interactive or not yet.
        (void)add_i_config(...);

        ...ignore_add_errors...
        ...use_builtin_add_i...

        return git_default_config(...);
    }

    int add_i_config(...)
    {
        return 0;
    }

or just inline everything here in add_config() and be done with it.

Jeff

#3 lore raw

Re: [PATCH 01/11] Start to implement a built-in version of `git add --interactive`

Quoted 4 lines · Jeff Hostetler

Currently, neither function looks at any other k/v pairs, so this is a bit of a moot point, but I'm wondering if this should look like this:

Quoted 12 lines
    int add_config(...)
    {
        // give add-interactive.c a chance to look at k/v pair, but
        // do not short-cut because we don't know yet whether we
        // will be interactive or not yet.
        (void)add_i_config(...);

        ...ignore_add_errors...
        ...use_builtin_add_i...

        return git_default_config(...);
    }

Yeah, I agree this split seems a bit more natural. It is worth propagating errors from add_i_config(), though, like:

if (add_i_config(var, value, data))

	return -1;

so that any key-specific errors (e.g., config_error_nonbool) stop the parsing in the usual way.

-Peff

#4 lore raw

Re: [PATCH 01/11] Start to implement a built-in version of `git add --interactive`

Hi Jeff & Jeff,

Quoted 19 lines · Jeff King
Quoted 4 lines · Jeff Hostetler

Currently, neither function looks at any other k/v pairs, so this is a bit of a moot point, but I'm wondering if this should look like this:

Quoted 12 lines
    int add_config(...)
    {
        // give add-interactive.c a chance to look at k/v pair, but
        // do not short-cut because we don't know yet whether we
        // will be interactive or not yet.
        (void)add_i_config(...);

        ...ignore_add_errors...
        ...use_builtin_add_i...

        return git_default_config(...);
    }
Quoted 8 lines

Yeah, I agree this split seems a bit more natural. It is worth propagating errors from add_i_config(), though, like:

if (add_i_config(var, value, data))

	return -1;

so that any key-specific errors (e.g., config_error_nonbool) stop the parsing in the usual way.

The only problem there is that add_i_config() (like all the other git_config() callbacks) does not report whether it consumed the key/value pair or not. I tried to avoid deviating from the standard practice to avoid calling git_default_config() when we already consumed the config setting.

And I also tried pretty hard to *not* bleed any internal state of add-interactive into builtin/add, as I wanted the new code to be as libified as possible (in a nearby thread, somebody wished for a new -p mode that would essentially be a combined git stash -p and git add -p, and with properly libified code such a beast is a lot more feasible).

Any idea how to deal with that?

I guess I could invert the order, where add_config() would be called as a fall-back from add_i_config()...

Or I invent a new convention where add_i_config() returns 1 when it consumed the key/value pair. But that would set a precedent that is inconsistent with the entire existing code base, something I am uncomfortable to do for the sake of add -i...

Ciao,
Dscho

#5 lore raw

Re: [PATCH 01/11] Start to implement a built-in version of `git add --interactive`

Quoted 6 lines · Johannes Schindelin
Quoted 6 lines

Yeah, I agree this split seems a bit more natural. It is worth propagating errors from add_i_config(), though, like:

if (add_i_config(var, value, data))

	return -1;
Quoted 8 lines
Quoted 2 lines

so that any key-specific errors (e.g., config_error_nonbool) stop the parsing in the usual way.

The only problem there is that add_i_config() (like all the other git_config() callbacks) does not report whether it consumed the key/value pair or not. I tried to avoid deviating from the standard practice to avoid calling git_default_config() when we already consumed the config setting.

I don't think it's worth worrying too much about that. We wouldn't match the keys in multiple places anyway (and even if we did, it would arguably be the right thing to give every callback a chance to see them).

The only thing it does is short-circuit the rest of the checks that we know won't match. But that doesn't really change the performance substantially; the worst case is already that we have to hit every possible strcmp().

And most of our config code does not worry about this, and is OK with branching (it just needs to propagate errors, as above). For some more discussion, see 6680a0874f (drop odd return value semantics from userdiff_config, 2012-02-07).

All that said...

Quoted 7 lines

And I also tried pretty hard to *not* bleed any internal state of add-interactive into builtin/add, as I wanted the new code to be as libified as possible (in a nearby thread, somebody wished for a new -p mode that would essentially be a combined git stash -p and git add -p, and with properly libified code such a beast is a lot more feasible).

Any idea how to deal with that?

The most lib-ified thing is to just use the configset code. I.e., wherever you need the config, just load it on demand via git_config_get_int or whatever.

Quoted 4 lines

Or I invent a new convention where add_i_config() returns 1 when it consumed the key/value pair. But that would set a precedent that is inconsistent with the entire existing code base, something I am uncomfortable to do for the sake of add -i...

Yes, don't do that. :) That was the same thing we finally got rid of for userdiff_config().

-Peff

#6 lore raw

Re: [PATCH 01/11] Start to implement a built-in version of `git add --interactive`

Hi Peff,

Quoted 9 lines · Jeff King
Quoted 7 lines · Johannes Schindelin

And I also tried pretty hard to *not* bleed any internal state of add-interactive into builtin/add, as I wanted the new code to be as libified as possible (in a nearby thread, somebody wished for a new -p mode that would essentially be a combined git stash -p and git add -p, and with properly libified code such a beast is a lot more feasible).

Quoted 5 lines
Quoted 1 line

Any idea how to deal with that?

The most lib-ified thing is to just use the configset code. I.e., wherever you need the config, just load it on demand via git_config_get_int or whatever.

True.

And it cost me *quite* a few days to implement the changes. But the result is definitely a lot better, in my opinion.

Quoted 7 lines
Quoted 4 lines

Or I invent a new convention where add_i_config() returns 1 when it consumed the key/value pair. But that would set a precedent that is inconsistent with the entire existing code base, something I am uncomfortable to do for the sake of add -i...

Yes, don't do that. :) That was the same thing we finally got rid of for userdiff_config().

Thanks for stopping me. I did not remember about the userdiff_config() thing.

Ciao,
Dscho

#7 lore raw

PATCH 2/11 diff: export diffstat interface

In reply to Johannes Schindelin via GitGitGadget

From: Daniel Ferreira <[email protected]>

Make the diffstat interface (namely, the diffstat_t struct and compute_diffstat) no longer be internal to diff.c and allow it to be used by other parts of git.

This is helpful for code that may want to easily extract information from files using the diff machinery, while flushing it differently from how the show_* functions used by diff_flush() do it. One example is the builtin implementation of git-add--interactive's status.

---
 diff.c | 37 +++++++++++++++----------------------
 diff.h | 19 +++++++++++++++++++
 2 files changed, 34 insertions(+), 22 deletions(-)
diff.c +15 -22
index 5306c48652..daa5f3a736 100644
@@ -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)
@@ -6001,12 +5985,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)
@@ -6306,6 +6285,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,
diff.h +19 -0
index b512d0477a..ae9bedfab8 100644
@@ -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
 
Signature · 1 line
gitgitgadget
#8 lore raw

PATCH 3/11 built-in add -i: implement the `status` command

In reply to Johannes Schindelin via GitGitGadget

From: Daniel Ferreira <[email protected]>

This implements the status command of git add -i. The data structures introduced in this commit will be extended as needed later.

At this point, we re-implement only part of the list_and_choose() function of the Perl script git-add--interactive.perl and call it list(). It does not yet color anything, or do columns, or allow user input.

Over the course of the next commits, we will introduce a list_and_choose() function that uses list() to display the list of options and let the user choose one or more of the displayed items. This will be used to implement the main loop of the built-in git add -i, at which point the new status command can actually be used.

Note that we pass the list of items as a struct item ** as opposed to a struct item *, to allow for the actual items to contain much more information than merely the name.

---
 add-interactive.c | 265 +++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 264 insertions(+), 1 deletion(-)
add-interactive.c +264 -1
index 540bf185d8..f627a56eeb 100644
@@ -1,13 +1,276 @@
 #include "cache.h"
 #include "add-interactive.h"
 #include "config.h"
+#include "diffcore.h"
+#include "revision.h"
+#include "refs.h"
 
 int add_i_config(const char *var, const char *value, void *cb)
 {
 	return git_default_config(var, value, cb);
 }
 
+struct item {
+	const char *name;
+};
+
+struct list_options {
+	const char *header;
+	void (*print_item)(int i, struct item *item, void *print_item_data);
+	void *print_item_data;
+};
+
+static void list(struct item **list, size_t nr, struct list_options *opts)
+{
+	int i;
+
+	if (!nr)
+		return;
+
+	if (opts->header)
+		printf("%s\n", opts->header);
+
+	for (i = 0; i < nr; i++) {
+		opts->print_item(i, list[i], opts->print_item_data);
+		putchar('\n');
+	}
+}
+
+struct adddel {
+	uintmax_t add, del;
+	unsigned seen:1, binary:1;
+};
+
+struct file_list {
+	struct file_item {
+		struct item item;
+		struct adddel index, worktree;
+	} **file;
+	size_t nr, alloc;
+};
+
+static void add_file_item(struct file_list *list, const char *name)
+{
+	struct file_item *item;
+
+	FLEXPTR_ALLOC_STR(item, item.name, name);
+
+	ALLOC_GROW(list->file, list->nr + 1, list->alloc);
+	list->file[list->nr++] = item;
+}
+
+static void reset_file_list(struct file_list *list)
+{
+	size_t i;
+
+	for (i = 0; i < list->nr; i++)
+		free(list->file[i]);
+	list->nr = 0;
+}
+
+static void release_file_list(struct file_list *list)
+{
+	reset_file_list(list);
+	FREE_AND_NULL(list->file);
+	list->alloc = 0;
+}
+
+static int file_item_cmp(const void *a, const void *b)
+{
+	const struct file_item * const *f1 = a;
+	const struct file_item * const *f2 = b;
+
+	return strcmp((*f1)->item.name, (*f2)->item.name);
+}
+
+struct pathname_entry {
+	struct hashmap_entry ent;
+	size_t index;
+	char pathname[FLEX_ARRAY];
+};
+
+static int pathname_entry_cmp(const void *unused_cmp_data,
+			      const void *entry, const void *entry_or_key,
+			      const void *pathname)
+{
+	const struct pathname_entry *e1 = entry, *e2 = entry_or_key;
+
+	return strcmp(e1->pathname,
+		      pathname ? (const char *)pathname : e2->pathname);
+}
+
+struct collection_status {
+	enum { FROM_WORKTREE = 0, FROM_INDEX = 1 } phase;
+
+	const char *reference;
+
+	struct file_list *list;
+	struct hashmap file_map;
+};
+
+static void collect_changes_cb(struct diff_queue_struct *q,
+			       struct diff_options *options,
+			       void *data)
+{
+	struct collection_status *s = data;
+	struct diffstat_t stat = { 0 };
+	int i;
+
+	if (!q->nr)
+		return;
+
+	compute_diffstat(options, &stat, q);
+
+	for (i = 0; i < stat.nr; i++) {
+		const char *name = stat.files[i]->name;
+		int hash = strhash(name);
+		struct pathname_entry *entry;
+		size_t file_index;
+		struct file_item *file;
+		struct adddel *adddel;
+
+		entry = hashmap_get_from_hash(&s->file_map, hash, name);
+		if (entry)
+			file_index = entry->index;
+		else {
+			FLEX_ALLOC_STR(entry, pathname, name);
+			hashmap_entry_init(entry, hash);
+			entry->index = file_index = s->list->nr;
+			hashmap_add(&s->file_map, entry);
+
+			add_file_item(s->list, name);
+		}
+		file = s->list->file[file_index];
+
+		adddel = s->phase == FROM_INDEX ? &file->index : &file->worktree;
+		adddel->seen = 1;
+		adddel->add = stat.files[i]->added;
+		adddel->del = stat.files[i]->deleted;
+		if (stat.files[i]->is_binary)
+			adddel->binary = 1;
+	}
+}
+
+static int get_modified_files(struct repository *r, struct file_list *list,
+			      const struct pathspec *ps)
+{
+	struct object_id head_oid;
+	int is_initial = !resolve_ref_unsafe("HEAD", RESOLVE_REF_READING,
+					     &head_oid, NULL);
+	struct collection_status s = { FROM_WORKTREE };
+
+	if (repo_read_index_preload(r, ps, 0) < 0)
+		return error(_("could not read index"));
+
+	s.list = list;
+	hashmap_init(&s.file_map, pathname_entry_cmp, NULL, 0);
+
+	for (s.phase = FROM_WORKTREE; s.phase <= FROM_INDEX; s.phase++) {
+		struct rev_info rev;
+		struct setup_revision_opt opt = { 0 };
+
+		opt.def = is_initial ?
+			empty_tree_oid_hex() : oid_to_hex(&head_oid);
+
+		init_revisions(&rev, NULL);
+		setup_revisions(0, NULL, &rev, &opt);
+
+		rev.diffopt.output_format = DIFF_FORMAT_CALLBACK;
+		rev.diffopt.format_callback = collect_changes_cb;
+		rev.diffopt.format_callback_data = &s;
+
+		if (ps)
+			copy_pathspec(&rev.prune_data, ps);
+
+		if (s.phase == FROM_INDEX)
+			run_diff_index(&rev, 1);
+		else {
+			rev.diffopt.flags.ignore_dirty_submodules = 1;
+			run_diff_files(&rev, 0);
+		}
+	}
+	hashmap_free(&s.file_map, 1);
+
+	/* While the diffs are ordered already, we ran *two* diffs... */
+	QSORT(list->file, list->nr, file_item_cmp);
+
+	return 0;
+}
+
+static void populate_wi_changes(struct strbuf *buf,
+				struct adddel *ad, const char *no_changes)
+{
+	if (ad->binary)
+		strbuf_addstr(buf, _("binary"));
+	else if (ad->seen)
+		strbuf_addf(buf, "+%"PRIuMAX"/-%"PRIuMAX,
+			    (uintmax_t)ad->add, (uintmax_t)ad->del);
+	else
+		strbuf_addstr(buf, no_changes);
+}
+
+struct print_file_item_data {
+	const char *modified_fmt;
+	struct strbuf buf, index, worktree;
+};
+
+static void print_file_item(int i, struct item *item,
+			    void *print_file_item_data)
+{
+	struct file_item *c = (struct file_item *)item;
+	struct print_file_item_data *d = print_file_item_data;
+
+	strbuf_reset(&d->index);
+	strbuf_reset(&d->worktree);
+	strbuf_reset(&d->buf);
+
+	populate_wi_changes(&d->worktree, &c->worktree, _("nothing"));
+	populate_wi_changes(&d->index, &c->index, _("unchanged"));
+	strbuf_addf(&d->buf, d->modified_fmt,
+		    d->index.buf, d->worktree.buf, item->name);
+
+	printf(" %2d: %s", i + 1, d->buf.buf);
+}
+
+static int run_status(struct repository *r, const struct pathspec *ps,
+		      struct file_list *files, struct list_options *opts)
+{
+	reset_file_list(files);
+
+	if (get_modified_files(r, files, ps) < 0)
+		return -1;
+
+	if (files->nr)
+		list((struct item **)files->file, files->nr, opts);
+	putchar('\n');
+
+	return 0;
+}
+
 int run_add_i(struct repository *r, const struct pathspec *ps)
 {
-	die(_("No commands are available in the built-in `git add -i` yet!"));
+	struct print_file_item_data print_file_item_data = {
+		"%12s %12s %s", STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
+	};
+	struct list_options opts = {
+		NULL, print_file_item, &print_file_item_data
+	};
+	struct strbuf header = STRBUF_INIT;
+	struct file_list files = { NULL };
+	int res = 0;
+
+	strbuf_addstr(&header, "      ");
+	strbuf_addf(&header, print_file_item_data.modified_fmt,
+		    _("staged"), _("unstaged"), _("path"));
+	opts.header = header.buf;
+
+	res = run_status(r, ps, &files, &opts);
+
+	release_file_list(&files);
+	strbuf_release(&print_file_item_data.buf);
+	strbuf_release(&print_file_item_data.index);
+	strbuf_release(&print_file_item_data.worktree);
+	strbuf_release(&header);
+
+	return res;
 }
Signature · 1 line
gitgitgadget
#9 lore raw

PATCH 4/11 built-in add -i: refresh the index before running `status`

In reply to Johannes Schindelin via GitGitGadget

From: Johannes Schindelin <[email protected]>

This is what the Perl version does, and therefore it is what the built-in version should do, too.

---
 add-interactive.c |  4 +++-
 repository.c      | 19 +++++++++++++++++++
 repository.h      |  7 +++++++
 3 files changed, 29 insertions(+), 1 deletion(-)
add-interactive.c +3 -1
index f627a56eeb..d971b58552 100644
@@ -264,7 +264,9 @@ int run_add_i(struct repository *r, const struct pathspec *ps)
 		    _("staged"), _("unstaged"), _("path"));
 	opts.header = header.buf;
 
-	res = run_status(r, ps, &files, &opts);
+	repo_refresh_and_write_index(r, REFRESH_QUIET, 1);
+	if (run_status(r, ps, &files, &opts) < 0)
+		res = -1;
 
 	release_file_list(&files);
 	strbuf_release(&print_file_item_data.buf);
repository.c +19 -0
index 65e6f8b8fd..c90f310093 100644
@@ -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;
+}
repository.h +7 -0
index 8981649d43..fb49e0e328 100644
@@ -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 */
Signature · 1 line
gitgitgadget
#10 lore raw

PATCH 5/11 built-in add -i: color the header in the `status` command

In reply to Johannes Schindelin via GitGitGadget

From: Johannes Schindelin <[email protected]>

For simplicity, we only implemented the status command without colors. This patch starts adding color, matching what the Perl script git-add--interactive.perl does.

---
 add-interactive.c | 51 ++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 48 insertions(+), 3 deletions(-)
add-interactive.c +48 -3
index d971b58552..79adc58321 100644
@@ -1,13 +1,56 @@
 #include "cache.h"
 #include "add-interactive.h"
 #include "config.h"
+#include "color.h"
+#include "config.h"
 #include "diffcore.h"
 #include "revision.h"
 #include "refs.h"
 
+static int use_color = -1;
+
+enum color_add_i {
+	COLOR_HEADER = 0,
+};
+
+static char list_colors[][COLOR_MAXLEN] = {
+	GIT_COLOR_BOLD,      /* Header */
+};
+
+static const char *get_add_i_color(enum color_add_i ix)
+{
+	if (want_color(use_color))
+		return list_colors[ix];
+	return "";
+}
+
+static int parse_color_slot(const char *slot)
+{
+	if (!strcasecmp(slot, "header"))
+		return COLOR_HEADER;
+
+	return -1;
+}
+
 int add_i_config(const char *var, const char *value, void *cb)
 {
-	return git_default_config(var, value, cb);
+	const char *name;
+
+	if (!strcmp(var, "color.interactive")) {
+		use_color = git_config_colorbool(var, value);
+		return 0;
+	}
+
+	if (skip_prefix(var, "color.interactive.", &name)) {
+		int slot = parse_color_slot(name);
+		if (slot < 0)
+			return 0;
+		if (!value)
+			return config_error_nonbool(var);
+		return color_parse(value, list_colors[slot]);
+	}
+
+	return git_color_default_config(var, value, cb);
 }
 
 struct item {
@@ -27,8 +70,10 @@ static void list(struct item **list, size_t nr, struct list_options *opts)
 	if (!nr)
 		return;
 
-	if (opts->header)
-		printf("%s\n", opts->header);
+	if (opts->header) {
+		const char *header_color = get_add_i_color(COLOR_HEADER);
+		color_fprintf_ln(stdout, header_color, "%s", opts->header);
+	}
 
 	for (i = 0; i < nr; i++) {
 		opts->print_item(i, list[i], opts->print_item_data);
Signature · 1 line
gitgitgadget
#11 lore raw

PATCH 6/11 built-in add -i: implement the main loop

In reply to Johannes Schindelin via GitGitGadget

From: Johannes Schindelin <[email protected]>

The reason why we did not start with the main loop to begin with is that it is the first user of list_and_choose(), which uses the list() function that we conveniently introduced for use by the status command.

Apart from the "and choose" part, there are more differences between the way the status command calls the list_and_choose() function in the Perl version of git add -i compared to the other callers of said function. The most important ones:

- The list is not only shown, but the user is also asked to make a choice, possibly selecting multiple entries.

- The list of items is prefixed with a marker indicating what items have been selected, if multi-selection is allowed.

- Initially, for each item a unique prefix (if there exists any within the given parameters) is determined, and shown in the list, and accepted as a shortcut for the selection.

These features will be implemented later, except the part where the user can choose a command. At this stage, though, the built-in git add -i still only supports the status command, with the remaining commands to follow over the course of the next commits.

In addition, we also modify list() to support displaying the commands in columns, even if there is currently only one.

The Perl script git-add--interactive.perl mixed the purposes of the "list" and the "and choose" part into the same function. In the C version, we will keep them separate instead, calling the list() function from the list_and_choose() function.

Note that we only have a prompt ending in a single ">" at this stage; later commits will add commands that display a double ">>" to indicate that the user is in a different loop than the main one.

---
 add-interactive.c | 122 +++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 120 insertions(+), 2 deletions(-)
add-interactive.c +120 -2
index 79adc58321..c8bd62369e 100644
@@ -58,6 +58,7 @@ struct item {
 };
 
 struct list_options {
+	int columns;
 	const char *header;
 	void (*print_item)(int i, struct item *item, void *print_item_data);
 	void *print_item_data;
@@ -65,7 +66,7 @@ struct list_options {
 
 static void list(struct item **list, size_t nr, struct list_options *opts)
 {
-	int i;
+	int i, last_lf = 0;
 
 	if (!nr)
 		return;
@@ -77,8 +78,90 @@ static void list(struct item **list, size_t nr, struct list_options *opts)
 
 	for (i = 0; i < nr; i++) {
 		opts->print_item(i, list[i], opts->print_item_data);
+
+		if ((opts->columns) && ((i + 1) % (opts->columns))) {
+			putchar('\t');
+			last_lf = 0;
+		}
+		else {
+			putchar('\n');
+			last_lf = 1;
+		}
+	}
+
+	if (!last_lf)
 		putchar('\n');
+}
+struct list_and_choose_options {
+	struct list_options list_opts;
+
+	const char *prompt;
+};
+
+/*
+ * Returns the selected index.
+ */
+static ssize_t list_and_choose(struct item **items, size_t nr,
+			       struct list_and_choose_options *opts)
+{
+	struct strbuf input = STRBUF_INIT;
+	ssize_t res = -1;
+
+	for (;;) {
+		char *p, *endp;
+
+		strbuf_reset(&input);
+
+		list(items, nr, &opts->list_opts);
+
+		printf("%s%s", opts->prompt, "> ");
+		fflush(stdout);
+
+		if (strbuf_getline(&input, stdin) == EOF) {
+			putchar('\n');
+			res = -2;
+			break;
+		}
+		strbuf_trim(&input);
+
+		if (!input.len)
+			break;
+
+		p = input.buf;
+		for (;;) {
+			size_t sep = strcspn(p, " \t\r\n,");
+			ssize_t index = -1;
+
+			if (!sep) {
+				if (!*p)
+					break;
+				p++;
+				continue;
+			}
+
+			if (isdigit(*p)) {
+				index = strtoul(p, &endp, 10) - 1;
+				if (endp != p + sep)
+					index = -1;
+			}
+
+			p[sep] = '\0';
+			if (index < 0 || index >= nr)
+				printf(_("Huh (%s)?\n"), p);
+			else {
+				res = index;
+				break;
+			}
+
+			p += sep + 1;
+		}
+
+		if (res >= 0)
+			break;
 	}
+
+	strbuf_release(&input);
+	return res;
 }
 
 struct adddel {
@@ -292,16 +375,39 @@ static int run_status(struct repository *r, const struct pathspec *ps,
 	return 0;
 }
 
+static void print_command_item(int i, struct item *item,
+			       void *print_command_item_data)
+{
+	printf(" %2d: %s", i + 1, item->name);
+}
+
+struct command_item {
+	struct item item;
+	int (*command)(struct repository *r, const struct pathspec *ps,
+		       struct file_list *files, struct list_options *opts);
+};
+
 int run_add_i(struct repository *r, const struct pathspec *ps)
 {
+	struct list_and_choose_options main_loop_opts = {
+		{ 4, N_("*** Commands ***"), print_command_item, NULL },
+		N_("What now")
+	};
+	struct command_item
+		status = { { "status" }, run_status };
+	struct command_item *commands[] = {
+		&status
+	};
+
 	struct print_file_item_data print_file_item_data = {
 		"%12s %12s %s", STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
 	};
 	struct list_options opts = {
-		NULL, print_file_item, &print_file_item_data
+		0, NULL, print_file_item, &print_file_item_data
 	};
 	struct strbuf header = STRBUF_INIT;
 	struct file_list files = { NULL };
+	ssize_t i;
 	int res = 0;
 
 	strbuf_addstr(&header, "      ");
@@ -313,6 +419,18 @@ int run_add_i(struct repository *r, const struct pathspec *ps)
 	if (run_status(r, ps, &files, &opts) < 0)
 		res = -1;
 
+	for (;;) {
+		i = list_and_choose((struct item **)commands,
+				    ARRAY_SIZE(commands), &main_loop_opts);
+		if (i < -1) {
+			printf(_("Bye.\n"));
+			res = 0;
+			break;
+		}
+		if (i >= 0)
+			res = commands[i]->command(r, ps, &files, &opts);
+	}
+
 	release_file_list(&files);
 	strbuf_release(&print_file_item_data.buf);
 	strbuf_release(&print_file_item_data.index);
Signature · 1 line
gitgitgadget
#12 lore raw

Re: [PATCH 06/11] built-in add -i: implement the main loop

Quoted 101 lines · Johannes Schindelin via GitGitGadget

From: Johannes Schindelin <[email protected]>

The reason why we did not start with the main loop to begin with is that it is the first user of list_and_choose(), which uses the list() function that we conveniently introduced for use by the status command.

Apart from the "and choose" part, there are more differences between the way the status command calls the list_and_choose() function in the Perl version of git add -i compared to the other callers of said function. The most important ones:

- The list is not only shown, but the user is also asked to make a choice, possibly selecting multiple entries.

- The list of items is prefixed with a marker indicating what items have been selected, if multi-selection is allowed.

- Initially, for each item a unique prefix (if there exists any within the given parameters) is determined, and shown in the list, and accepted as a shortcut for the selection.

These features will be implemented later, except the part where the user can choose a command. At this stage, though, the built-in git add -i still only supports the status command, with the remaining commands to follow over the course of the next commits.

In addition, we also modify list() to support displaying the commands in columns, even if there is currently only one.

The Perl script git-add--interactive.perl mixed the purposes of the "list" and the "and choose" part into the same function. In the C version, we will keep them separate instead, calling the list() function from the list_and_choose() function.

Note that we only have a prompt ending in a single ">" at this stage; later commits will add commands that display a double ">>" to indicate that the user is in a different loop than the main one.

---
  add-interactive.c | 122 +++++++++++++++++++++++++++++++++++++++++++++-
  1 file changed, 120 insertions(+), 2 deletions(-)
add-interactive.c +33 -1
index 79adc58321..c8bd62369e 100644
@@ -58,6 +58,7 @@ struct item {
  };
 
  struct list_options {
+	int columns;
  	const char *header;
  	void (*print_item)(int i, struct item *item, void *print_item_data);
  	void *print_item_data;
@@ -65,7 +66,7 @@ struct list_options {
 
  static void list(struct item **list, size_t nr, struct list_options *opts)
  {
-	int i;
+	int i, last_lf = 0;
 
  	if (!nr)
  		return;
@@ -77,8 +78,90 @@ static void list(struct item **list, size_t nr, struct list_options *opts)
 
  	for (i = 0; i < nr; i++) {
  		opts->print_item(i, list[i], opts->print_item_data);
+
+		if ((opts->columns) && ((i + 1) % (opts->columns))) {
+			putchar('\t');
+			last_lf = 0;
+		}
+		else {
+			putchar('\n');
+			last_lf = 1;
+		}
+	}
+
+	if (!last_lf)
  		putchar('\n');
+}
+struct list_and_choose_options {
+	struct list_options list_opts;
+
+	const char *prompt;
+};
+
+/*
+ * Returns the selected index.
+ */
+static ssize_t list_and_choose(struct item **items, size_t nr,
+			       struct list_and_choose_options *opts)
+{
+	struct strbuf input = STRBUF_INIT;
+	ssize_t res = -1;
+
+	for (;;) {
+		char *p, *endp;
+

add-interactive.c static void list(struct item **list, size_t nr, struct list_options *opts) · line 122

Quoted 10 lines
add-interactive.c +10 -0
+		strbuf_reset(&input);
+
+		list(items, nr, &opts->list_opts);
+
+		printf("%s%s", opts->prompt, "> ");
+		fflush(stdout);
+
+		if (strbuf_getline(&input, stdin) == EOF) {
+			putchar('\n');
+			res = -2;

It would be nice to know what -1 and -2 mean if
they get returned to our caller. Maybe a #define
for these??

Quoted 106 lines
add-interactive.c +77 -1
+			break;
+		}
+		strbuf_trim(&input);
+
+		if (!input.len)
+			break;
+
+		p = input.buf;
+		for (;;) {
+			size_t sep = strcspn(p, " \t\r\n,");
+			ssize_t index = -1;
+
+			if (!sep) {
+				if (!*p)
+					break;
+				p++;
+				continue;
+			}
+
+			if (isdigit(*p)) {
+				index = strtoul(p, &endp, 10) - 1;
+				if (endp != p + sep)
+					index = -1;
+			}
+
+			p[sep] = '\0';
+			if (index < 0 || index >= nr)
+				printf(_("Huh (%s)?\n"), p);
+			else {
+				res = index;
+				break;
+			}
+
+			p += sep + 1;
+		}
+
+		if (res >= 0)
+			break;
  	}
+
+	strbuf_release(&input);
+	return res;
  }
 
  struct adddel {
@@ -292,16 +375,39 @@ static int run_status(struct repository *r, const struct pathspec *ps,
  	return 0;
  }
 
+static void print_command_item(int i, struct item *item,
+			       void *print_command_item_data)
+{
+	printf(" %2d: %s", i + 1, item->name);
+}
+
+struct command_item {
+	struct item item;
+	int (*command)(struct repository *r, const struct pathspec *ps,
+		       struct file_list *files, struct list_options *opts);
+};
+
  int run_add_i(struct repository *r, const struct pathspec *ps)
  {
+	struct list_and_choose_options main_loop_opts = {
+		{ 4, N_("*** Commands ***"), print_command_item, NULL },
+		N_("What now")
+	};
+	struct command_item
+		status = { { "status" }, run_status };
+	struct command_item *commands[] = {
+		&status
+	};
+
  	struct print_file_item_data print_file_item_data = {
  		"%12s %12s %s", STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
  	};
  	struct list_options opts = {
-		NULL, print_file_item, &print_file_item_data
+		0, NULL, print_file_item, &print_file_item_data
  	};
  	struct strbuf header = STRBUF_INIT;
  	struct file_list files = { NULL };
+	ssize_t i;
  	int res = 0;
 
  	strbuf_addstr(&header, "      ");
@@ -313,6 +419,18 @@ int run_add_i(struct repository *r, const struct pathspec *ps)
  	if (run_status(r, ps, &files, &opts) < 0)
  		res = -1;
 
+	for (;;) {
+		i = list_and_choose((struct item **)commands,
+				    ARRAY_SIZE(commands), &main_loop_opts);
+		if (i < -1) {
+			printf(_("Bye.\n"));
+			res = 0;
+			break;
+		}
+		if (i >= 0)
+			res = commands[i]->command(r, ps, &files, &opts);
+	}
+
  	release_file_list(&files);
  	strbuf_release(&print_file_item_data.buf);
  	strbuf_release(&print_file_item_data.index);
#13 lore raw

Re: [PATCH 06/11] built-in add -i: implement the main loop

Hi Jeff,

Quoted 22 lines · Jeff Hostetler
Quoted 20 lines · Johannes Schindelin via GitGitGadget

[...]

add-interactive.c +19 -0
+
+/*
+ * Returns the selected index.
+ */
+static ssize_t list_and_choose(struct item **items, size_t nr,
+			       struct list_and_choose_options *opts)
+{
+	struct strbuf input = STRBUF_INIT;
+	ssize_t res = -1;
+
+	for (;;) {
+		char *p, *endp;
+
+		strbuf_reset(&input);
+
+		list(items, nr, &opts->list_opts);
+
+		printf("%s%s", opts->prompt, "> ");
+		fflush(stdout);
Quoted 8 lines

add-interactive.c static void list(struct item **list, size_t nr, struct list_options *opts) · line 122

Quoted 4 lines
add-interactive.c +4 -0
+
+		if (strbuf_getline(&input, stdin) == EOF) {
+			putchar('\n');
+			res = -2;

It would be nice to know what -1 and -2 mean if
they get returned to our caller. Maybe a #define
for these??

Makes a total lot of sense. I changed that.

Thanks!
Dscho

#14 lore raw

PATCH 7/11 Add a function to determine unique prefixes for a list of strings

In reply to Johannes Schindelin via GitGitGadget

From: Slavica Djukic <[email protected]>

In the git add -i command, we show unique prefixes of the commands and files, to give an indication what prefix would select them.

Naturally, the C implementation looks a lot different than the Perl
implementation: in Perl, a trie is much easier implemented, while we already have a pretty neat hashmap implementation in C that we use for the purpose of storing (not necessarily unique) prefixes.

The idea: for each item that we add, we generate prefixes starting with the first letter, then the first two letters, then three, etc, until we find a prefix that is unique (or until the prefix length would be longer than we want). If we encounter a previously-unique prefix on the way, we adjust that item's prefix to make it unique again (or we mark it as having no unique prefix if we failed to find one). These partial prefixes are stored in a hash map (for quick lookup times).

To make sure that this function works as expected, we add a test using a special-purpose test helper that was added for that purpose.

Note: We expect the list of prefix items to be passed in as a list of pointers rather than as regular list to avoid having to copy information (the actual items will most likely contain more information than just the name and the length of the unique prefix, but passing in struct prefix_item * would not allow for that).

---
 Makefile                   |   2 +
 prefix-map.c               | 111 +++++++++++++++++++++++++++++++++++++
 prefix-map.h               |  40 +++++++++++++
 t/helper/test-prefix-map.c |  58 +++++++++++++++++++
 t/helper/test-tool.c       |   1 +
 t/helper/test-tool.h       |   1 +
 t/t0016-prefix-map.sh      |  10 ++++
 7 files changed, 223 insertions(+)
 create mode 100644 prefix-map.c
 create mode 100644 prefix-map.h
 create mode 100644 t/helper/test-prefix-map.c
 create mode 100755 t/t0016-prefix-map.sh
Makefile +2 -0
index 18e656a32f..8299b3f17d 100644
@@ -754,6 +754,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
@@ -967,6 +968,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
prefix-map.c new file +111 -0

111 lines not on this page · raw message

prefix-map.h new file +40 -0
new file mode 100644
index 0000000000..ce3b8a4a32
@@ -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
t/helper/test-prefix-map.c new file +58 -0

58 lines not on this page · raw message

t/helper/test-tool.c +1 -0
index 99db7409b8..d6a92a8699 100644
@@ -32,6 +32,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 },
t/helper/test-tool.h +1 -0
index 25abed1cf2..33a089ee4e 100644
@@ -29,6 +29,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);
t/t0016-prefix-map.sh new file +10 -0
new file mode 100755
index 0000000000..187fa92aec
@@ -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
Signature · 1 line
gitgitgadget
#15 lore raw

Re: [PATCH 07/11] Add a function to determine unique prefixes for a list of strings

Quoted 104 lines · Slavica Djukic via GitGitGadget

From: Slavica Djukic <[email protected]>

In the git add -i command, we show unique prefixes of the commands and files, to give an indication what prefix would select them.

Naturally, the C implementation looks a lot different than the Perl
implementation: in Perl, a trie is much easier implemented, while we already have a pretty neat hashmap implementation in C that we use for the purpose of storing (not necessarily unique) prefixes.

The idea: for each item that we add, we generate prefixes starting with the first letter, then the first two letters, then three, etc, until we find a prefix that is unique (or until the prefix length would be longer than we want). If we encounter a previously-unique prefix on the way, we adjust that item's prefix to make it unique again (or we mark it as having no unique prefix if we failed to find one). These partial prefixes are stored in a hash map (for quick lookup times).

To make sure that this function works as expected, we add a test using a special-purpose test helper that was added for that purpose.

Note: We expect the list of prefix items to be passed in as a list of pointers rather than as regular list to avoid having to copy information (the actual items will most likely contain more information than just the name and the length of the unique prefix, but passing in struct prefix_item * would not allow for that).

---
  Makefile                   |   2 +
  prefix-map.c               | 111 +++++++++++++++++++++++++++++++++++++
  prefix-map.h               |  40 +++++++++++++
  t/helper/test-prefix-map.c |  58 +++++++++++++++++++
  t/helper/test-tool.c       |   1 +
  t/helper/test-tool.h       |   1 +
  t/t0016-prefix-map.sh      |  10 ++++
  7 files changed, 223 insertions(+)

create mode 100644 prefix-map.c
create mode 100644 prefix-map.h
create mode 100644 t/helper/test-prefix-map.c
create mode 100755 t/t0016-prefix-map.sh

Makefile +2 -0
index 18e656a32f..8299b3f17d 100644
@@ -754,6 +754,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
@@ -967,6 +968,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
prefix-map.c new file +35 -0
new file mode 100644
index 0000000000..3c5ae4ae0a
@@ -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,

prefix-map.c line 45

Quoted 10 lines
prefix-map.c new file +10 -0
+			    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])) {

This feels odd, if I understand the intent.

First, why "isascii()" rather than just non-zero?

But mainly, can we walk off the end of the array and read potentially uninitialized memory? Shouldn't we have something at the top of the function like:

    len = strlen(item->name);
    if (len < prefix_map->min_length)
        return;

(And maybe avoid the xmalloc() too?)

And maybe do " j <= min(len, max_length) " in the loop? But I see you're modifying "j" down in the body of the loop, so I'll wait on suggesting that.

Quoted 10 lines
prefix-map.c new file +10 -0
+			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;

prefix-map.c line 65

Quoted 10 lines
prefix-map.c new file +10 -0
+			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);

IIUC, this assurance comes directly from map_cmp(), right? We could strengthen this to

     (j != e2->item->prefix_length || strncmp(...))

if we wanted to, right?

prefix-map.c line 70

Quoted 5 lines
prefix-map.c new file +5 -0
+
+		/* skip common prefix */
+		for (; j < prefix_map->max_length && e->name[j]; j++) {
+			if (e->item->name[j] != e2->item->name[j])
+				break;

Same comment here about walking off of the defined end of both arrays.

I'm going to stop here. I'm getting confused.

Quoted 192 lines
prefix-map.c new file +41 -0
+			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);
+}
prefix-map.h new file +40 -0
new file mode 100644
index 0000000000..ce3b8a4a32
@@ -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
t/helper/test-prefix-map.c new file +58 -0
new file mode 100644
index 0000000000..3f1c90eaf0
@@ -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;
+}
t/helper/test-tool.c +1 -0
index 99db7409b8..d6a92a8699 100644
@@ -32,6 +32,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 },
t/helper/test-tool.h +1 -0
index 25abed1cf2..33a089ee4e 100644
@@ -29,6 +29,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);
t/t0016-prefix-map.sh new file +10 -0
new file mode 100755
index 0000000000..187fa92aec
@@ -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
#16 lore raw

Re: [PATCH 07/11] Add a function to determine unique prefixes for a list of strings

Hi Jeff,

Quoted 85 lines · Jeff Hostetler
Quoted 71 lines · Slavica Djukic via GitGitGadget

From: Slavica Djukic <[email protected]>

In the git add -i command, we show unique prefixes of the commands and files, to give an indication what prefix would select them.

Naturally, the C implementation looks a lot different than the Perl
implementation: in Perl, a trie is much easier implemented, while we already have a pretty neat hashmap implementation in C that we use for the purpose of storing (not necessarily unique) prefixes.

The idea: for each item that we add, we generate prefixes starting with the first letter, then the first two letters, then three, etc, until we find a prefix that is unique (or until the prefix length would be longer than we want). If we encounter a previously-unique prefix on the way, we adjust that item's prefix to make it unique again (or we mark it as having no unique prefix if we failed to find one). These partial prefixes are stored in a hash map (for quick lookup times).

To make sure that this function works as expected, we add a test using a special-purpose test helper that was added for that purpose.

Note: We expect the list of prefix items to be passed in as a list of pointers rather than as regular list to avoid having to copy information (the actual items will most likely contain more information than just the name and the length of the unique prefix, but passing in struct prefix_item * would not allow for that).

---

prefix-map.c new file +35 -0
new file mode 100644
index 0000000000..3c5ae4ae0a
@@ -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,

prefix-map.c line 45

Quoted 10 lines
prefix-map.c new file +10 -0
+			    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])) {

This feels odd, if I understand the intent.

Quoted 1 line

First, why "isascii()" rather than just non-zero?

That's to imitate git-add--interactive.perl's

	if (ord($letters[0]) > 127 ||
	    ($soft_limit && $j + 1 > $soft_limit))

See https://github.com/git/git/blob/v2.21.0/git-add--interactive.perl#L410
for more complete context.

I think the main benefit here is that we avoid running into the trap of using incomplete UTF-8 multi-byte sequences in prefixes.

I guess we could throw in an extra safety on the C side by excluding control characters, too. But that would be a deviation from Perl, and I actually do not even feel strongly about excluding, say, a HT (horizontal tab) from the prefixes.

Quoted 7 lines

But mainly, can we walk off the end of the array and read potentially uninitialized memory? Shouldn't we have something at the top of the function like:

    len = strlen(item->name);
    if (len < prefix_map->min_length)
        return;

Ooops, you're right. But I would not use strlen() here, we can easily just add && e->name[j]` to the loop condition.

Quoted 1 line

(And maybe avoid the xmalloc() too?)

Hmm. At first, I thought: no, we use *e *both* for lookup and for adding a new item once we did not find any existing for the current prefix length.

But it does indeed become a lot clearer when I separate those. It's not even performance or memory critical a code path.

Quoted 27 lines

And maybe do " j <= min(len, max_length) " in the loop? But I see you're modifying "j" down in the body of the loop, so I'll wait on suggesting that.

Quoted 10 lines
prefix-map.c new file +10 -0
+			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;

prefix-map.c line 65

Quoted 10 lines
prefix-map.c new file +10 -0
+			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);

IIUC, this assurance comes directly from map_cmp(), right? We could strengthen this to

Quoted 2 lines
     (j != e2->item->prefix_length || strncmp(...))

if we wanted to, right?

Right, I'll actually go for memcmp() here, but the idea is the same.

Quoted 7 lines

prefix-map.c line 70

Quoted 5 lines
prefix-map.c new file +5 -0
+
+		/* skip common prefix */
+		for (; j < prefix_map->max_length && e->name[j]; j++) {
+			if (e->item->name[j] != e2->item->name[j])
+				break;

Same comment here about walking off of the defined end of both arrays.

Actually, no, not here, as I already test for e->name[j] in the loop condition. If we reach the end of e2->item->name, the inner condition will break out of the loop.

Quoted 1 line

I'm going to stop here. I'm getting confused.

Oh no ;-)

Thank you for your helpful comments!

Ciao,
Dscho

#17 lore raw

PATCH 8/11 built-in add -i: show unique prefixes of the commands

In reply to Johannes Schindelin via GitGitGadget

From: Slavica Djukic <[email protected]>

Just like in the Perl script git-add--interactive.perl, for each command a unique prefix is determined (if there exists any within the given parameters), and shown in the list, and accepted as a shortcut for the command.

We use the prefix map implementation that we just added in the previous commit for that purpose.

---
 add-interactive.c | 70 ++++++++++++++++++++++++++++++++++++++---------
 1 file changed, 57 insertions(+), 13 deletions(-)
add-interactive.c +57 -13
index c8bd62369e..62ce446dd9 100644
@@ -6,6 +6,7 @@
 #include "diffcore.h"
 #include "revision.h"
 #include "refs.h"
+#include "prefix-map.h"
 
 static int use_color = -1;
 
@@ -53,18 +54,33 @@ int add_i_config(const char *var, const char *value, void *cb)
 	return git_color_default_config(var, value, cb);
 }
 
-struct item {
-	const char *name;
-};
+static ssize_t find_unique(const char *string,
+			   struct prefix_item **list, size_t nr)
+{
+	ssize_t found = -1, i;
+
+	for (i = 0; i < nr; i++) {
+		struct prefix_item *item = list[i];
+		if (!starts_with(item->name, string))
+			continue;
+		if (found >= 0)
+			return -1;
+		found = i;
+	}
+
+	return found;
+}
 
 struct list_options {
 	int columns;
 	const char *header;
-	void (*print_item)(int i, struct item *item, void *print_item_data);
+	void (*print_item)(int i, struct prefix_item *item,
+			   void *print_item_data);
 	void *print_item_data;
 };
 
-static void list(struct item **list, size_t nr, struct list_options *opts)
+static void list(struct prefix_item **list, size_t nr,
+		 struct list_options *opts)
 {
 	int i, last_lf = 0;
 
@@ -101,12 +117,14 @@ struct list_and_choose_options {
 /*
  * Returns the selected index.
  */
-static ssize_t list_and_choose(struct item **items, size_t nr,
+static ssize_t list_and_choose(struct prefix_item **items, size_t nr,
 			       struct list_and_choose_options *opts)
 {
 	struct strbuf input = STRBUF_INIT;
 	ssize_t res = -1;
 
+	find_unique_prefixes(items, nr, 1, 4);
+
 	for (;;) {
 		char *p, *endp;
 
@@ -146,6 +164,9 @@ static ssize_t list_and_choose(struct item **items, size_t nr,
 			}
 
 			p[sep] = '\0';
+			if (index < 0)
+				index = find_unique(p, items, nr);
+
 			if (index < 0 || index >= nr)
 				printf(_("Huh (%s)?\n"), p);
 			else {
@@ -171,7 +192,7 @@ struct adddel {
 
 struct file_list {
 	struct file_item {
-		struct item item;
+		struct prefix_item item;
 		struct adddel index, worktree;
 	} **file;
 	size_t nr, alloc;
@@ -337,12 +358,29 @@ static void populate_wi_changes(struct strbuf *buf,
 		strbuf_addstr(buf, no_changes);
 }
 
+/* filters out prefixes which have special meaning to list_and_choose() */
+static int is_valid_prefix(const char *prefix, size_t prefix_len)
+{
+	return prefix_len && prefix &&
+		/*
+		 * We expect `prefix` to be NUL terminated, therefore this
+		 * `strcspn()` call is okay, even if it might do much more
+		 * work than strictly necessary.
+		 */
+		strcspn(prefix, " \t\r\n,") >= prefix_len &&	/* separators */
+		*prefix != '-' &&				/* deselection */
+		!isdigit(*prefix) &&				/* selection */
+		(prefix_len != 1 ||
+		 (*prefix != '*' &&				/* "all" wildcard */
+		  *prefix != '?'));				/* prompt help */
+}
+
 struct print_file_item_data {
 	const char *modified_fmt;
 	struct strbuf buf, index, worktree;
 };
 
-static void print_file_item(int i, struct item *item,
+static void print_file_item(int i, struct prefix_item *item,
 			    void *print_file_item_data)
 {
 	struct file_item *c = (struct file_item *)item;
@@ -369,20 +407,26 @@ static int run_status(struct repository *r, const struct pathspec *ps,
 		return -1;
 
 	if (files->nr)
-		list((struct item **)files->file, files->nr, opts);
+		list((struct prefix_item **)files->file, files->nr, opts);
 	putchar('\n');
 
 	return 0;
 }
 
-static void print_command_item(int i, struct item *item,
+static void print_command_item(int i, struct prefix_item *item,
 			       void *print_command_item_data)
 {
-	printf(" %2d: %s", i + 1, item->name);
+	if (!item->prefix_length ||
+	    !is_valid_prefix(item->name, item->prefix_length))
+		printf(" %2d: %s", i + 1, item->name);
+	else
+		printf(" %3d: [%.*s]%s", i + 1,
+		       (int)item->prefix_length, item->name,
+		       item->name + item->prefix_length);
 }
 
 struct command_item {
-	struct item item;
+	struct prefix_item item;
 	int (*command)(struct repository *r, const struct pathspec *ps,
 		       struct file_list *files, struct list_options *opts);
 };
@@ -420,7 +464,7 @@ int run_add_i(struct repository *r, const struct pathspec *ps)
 		res = -1;
 
 	for (;;) {
-		i = list_and_choose((struct item **)commands,
+		i = list_and_choose((struct prefix_item **)commands,
 				    ARRAY_SIZE(commands), &main_loop_opts);
 		if (i < -1) {
 			printf(_("Bye.\n"));
Signature · 1 line
gitgitgadget
#18 lore raw

PATCH 9/11 built-in add -i: support `?` (prompt help)

In reply to Johannes Schindelin via GitGitGadget

From: Johannes Schindelin <[email protected]>

With this change, we print out the same colored help text that the Perl-based git add -i prints in the main loop when question mark is entered.

---
 add-interactive.c | 24 +++++++++++++++++++++++-
 1 file changed, 23 insertions(+), 1 deletion(-)
add-interactive.c +23 -1
index 62ce446dd9..03d0770013 100644
@@ -12,10 +12,12 @@ static int use_color = -1;
 
 enum color_add_i {
 	COLOR_HEADER = 0,
+	COLOR_HELP,
 };
 
 static char list_colors[][COLOR_MAXLEN] = {
 	GIT_COLOR_BOLD,      /* Header */
+	GIT_COLOR_BOLD_RED,  /* Help */
 };
 
 static const char *get_add_i_color(enum color_add_i ix)
@@ -29,6 +31,8 @@ static int parse_color_slot(const char *slot)
 {
 	if (!strcasecmp(slot, "header"))
 		return COLOR_HEADER;
+	if (!strcasecmp(slot, "help"))
+		return COLOR_HELP;
 
 	return -1;
 }
@@ -112,6 +116,7 @@ struct list_and_choose_options {
 	struct list_options list_opts;
 
 	const char *prompt;
+	void (*print_help)(void);
 };
 
 /*
@@ -145,6 +150,11 @@ static ssize_t list_and_choose(struct prefix_item **items, size_t nr,
 		if (!input.len)
 			break;
 
+		if (!strcmp(input.buf, "?")) {
+			opts->print_help();
+			continue;
+		}
+
 		p = input.buf;
 		for (;;) {
 			size_t sep = strcspn(p, " \t\r\n,");
@@ -431,11 +441,23 @@ struct command_item {
 		       struct file_list *files, struct list_options *opts);
 };
 
+static void command_prompt_help(void)
+{
+	const char *help_color = get_add_i_color(COLOR_HELP);
+	color_fprintf_ln(stdout, help_color, "%s", _("Prompt help:"));
+	color_fprintf_ln(stdout, help_color, "1          - %s",
+			 _("select a numbered item"));
+	color_fprintf_ln(stdout, help_color, "foo        - %s",
+			 _("select item based on unique prefix"));
+	color_fprintf_ln(stdout, help_color, "           - %s",
+			 _("(empty) select nothing"));
+}
+
 int run_add_i(struct repository *r, const struct pathspec *ps)
 {
 	struct list_and_choose_options main_loop_opts = {
 		{ 4, N_("*** Commands ***"), print_command_item, NULL },
-		N_("What now")
+		N_("What now"), command_prompt_help
 	};
 	struct command_item
 		status = { { "status" }, run_status };
Signature · 1 line
gitgitgadget
#19 lore raw

PATCH 10/11 built-in add -i: use color in the main loop

In reply to Johannes Schindelin via GitGitGadget

From: Slavica Djukic <[email protected]>

The error messages as well as the unique prefixes are colored in git add -i by default; We need to do the same in the built-in version.

---
 add-interactive.c | 40 +++++++++++++++++++++++++++++++++++-----
 1 file changed, 35 insertions(+), 5 deletions(-)
add-interactive.c +35 -5
index 03d0770013..a1550d9b9f 100644
@@ -13,11 +13,17 @@ static int use_color = -1;
 enum color_add_i {
 	COLOR_HEADER = 0,
 	COLOR_HELP,
+	COLOR_PROMPT,
+	COLOR_ERROR,
+	COLOR_RESET,
 };
 
 static char list_colors[][COLOR_MAXLEN] = {
 	GIT_COLOR_BOLD,      /* Header */
 	GIT_COLOR_BOLD_RED,  /* Help */
+	GIT_COLOR_BOLD_BLUE, /* Prompt */
+	GIT_COLOR_BOLD_RED,  /* Error */
+	GIT_COLOR_RESET,     /* Reset */
 };
 
 static const char *get_add_i_color(enum color_add_i ix)
@@ -33,6 +39,12 @@ static int parse_color_slot(const char *slot)
 		return COLOR_HEADER;
 	if (!strcasecmp(slot, "help"))
 		return COLOR_HELP;
+	if (!strcasecmp(slot, "prompt"))
+		return COLOR_PROMPT;
+	if (!strcasecmp(slot, "error"))
+		return COLOR_ERROR;
+	if (!strcasecmp(slot, "reset"))
+		return COLOR_RESET;
 
 	return -1;
 }
@@ -125,6 +137,8 @@ struct list_and_choose_options {
 static ssize_t list_and_choose(struct prefix_item **items, size_t nr,
 			       struct list_and_choose_options *opts)
 {
+	const char *prompt_color = get_add_i_color(COLOR_PROMPT);
+	const char *error_color = get_add_i_color(COLOR_ERROR);
 	struct strbuf input = STRBUF_INIT;
 	ssize_t res = -1;
 
@@ -137,7 +151,8 @@ static ssize_t list_and_choose(struct prefix_item **items, size_t nr,
 
 		list(items, nr, &opts->list_opts);
 
-		printf("%s%s", opts->prompt, "> ");
+		color_fprintf(stdout, prompt_color, "%s", opts->prompt);
+		fputs("> ", stdout);
 		fflush(stdout);
 
 		if (strbuf_getline(&input, stdin) == EOF) {
@@ -178,7 +193,8 @@ static ssize_t list_and_choose(struct prefix_item **items, size_t nr,
 				index = find_unique(p, items, nr);
 
 			if (index < 0 || index >= nr)
-				printf(_("Huh (%s)?\n"), p);
+				color_fprintf_ln(stdout, error_color,
+						 _("Huh (%s)?"), p);
 			else {
 				res = index;
 				break;
@@ -423,15 +439,21 @@ static int run_status(struct repository *r, const struct pathspec *ps,
 	return 0;
 }
 
+struct print_command_item_data {
+	const char *color, *reset;
+};
+
 static void print_command_item(int i, struct prefix_item *item,
 			       void *print_command_item_data)
 {
+	struct print_command_item_data *d = print_command_item_data;
+
 	if (!item->prefix_length ||
 	    !is_valid_prefix(item->name, item->prefix_length))
 		printf(" %2d: %s", i + 1, item->name);
 	else
-		printf(" %3d: [%.*s]%s", i + 1,
-		       (int)item->prefix_length, item->name,
+		printf(" %2d: %s%.*s%s%s", i + 1,
+		       d->color, (int)item->prefix_length, item->name, d->reset,
 		       item->name + item->prefix_length);
 }
 
@@ -455,8 +477,16 @@ static void command_prompt_help(void)
 
 int run_add_i(struct repository *r, const struct pathspec *ps)
 {
+	struct print_command_item_data data = {
+		/*
+		 * When color was asked for, use the prompt color for
+		 * highlighting, otherwise use square brackets.
+		 */
+		want_color(use_color) ? get_add_i_color(COLOR_PROMPT) : "[",
+		want_color(use_color) ? get_add_i_color(COLOR_RESET) : "]"
+	};
 	struct list_and_choose_options main_loop_opts = {
-		{ 4, N_("*** Commands ***"), print_command_item, NULL },
+		{ 4, N_("*** Commands ***"), print_command_item, &data },
 		N_("What now"), command_prompt_help
 	};
 	struct command_item
Signature · 1 line
gitgitgadget
#20 lore raw

PATCH 11/11 built-in add -i: implement the `help` command

In reply to Johannes Schindelin via GitGitGadget

From: Johannes Schindelin <[email protected]>

This imitates the code to show the help text from the Perl script git-add--interactive.perl in the built-in version.

To make sure that it renders exactly like the Perl version of git add -i, we also add a test case for that to t3701-add-interactive.sh.

---
 add-interactive.c          | 27 +++++++++++++++++++++++++--
 t/t3701-add-interactive.sh | 24 ++++++++++++++++++++++++
 2 files changed, 49 insertions(+), 2 deletions(-)
add-interactive.c +25 -2
index a1550d9b9f..a6baf57e20 100644
@@ -439,6 +439,27 @@ static int run_status(struct repository *r, const struct pathspec *ps,
 	return 0;
 }
 
+static int run_help(struct repository *r, const struct pathspec *ps,
+		    struct file_list *files, struct list_options *opts)
+{
+	const char *help_color = get_add_i_color(COLOR_HELP);
+
+	color_fprintf_ln(stdout, help_color, "status        - %s",
+			 _("show paths with changes"));
+	color_fprintf_ln(stdout, help_color, "update        - %s",
+			 _("add working tree state to the staged set of changes"));
+	color_fprintf_ln(stdout, help_color, "revert        - %s",
+			 _("revert staged set of changes back to the HEAD version"));
+	color_fprintf_ln(stdout, help_color, "patch         - %s",
+			 _("pick hunks and update selectively"));
+	color_fprintf_ln(stdout, help_color, "diff          - %s",
+			 _("view diff between HEAD and index"));
+	color_fprintf_ln(stdout, help_color, "add untracked - %s",
+			 _("add contents of untracked files to the staged set of changes"));
+
+	return 0;
+}
+
 struct print_command_item_data {
 	const char *color, *reset;
 };
@@ -490,9 +511,11 @@ int run_add_i(struct repository *r, const struct pathspec *ps)
 		N_("What now"), command_prompt_help
 	};
 	struct command_item
-		status = { { "status" }, run_status };
+		status = { { "status" }, run_status },
+		help = { { "help" }, run_help };
 	struct command_item *commands[] = {
-		&status
+		&status,
+		&help
 	};
 
 	struct print_file_item_data print_file_item_data = {
t/t3701-add-interactive.sh +24 -0
index 65dfbc033a..91aaef2932 100755
@@ -639,4 +639,28 @@ test_expect_success 'add -p patch editing works with pathological context lines'
 	test_cmp expected-2 actual
 '
 
+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 | GIT_PAGER_IN_USE=true TERM=vt100 git add -i >actual.colored &&
+	test_decode_color <actual.colored >actual &&
+	test_i18ncmp expect actual
+'
+
 test_done
Signature · 1 line
gitgitgadget
#21 lore raw

v2 cover git add -i: add a rudimentary version in C (supporting only status and help so far)

In reply to Johannes Schindelin via GitGitGadget

This is the first leg on the long journey to a fully built-in git add -i (next up: parts 2 [https://github.com/gitgitgadget/git/pull/171], 3
[https://github.com/gitgitgadget/git/pull/172], 4
[https://github.com/gitgitgadget/git/pull/173], 5
[https://github.com/gitgitgadget/git/pull/174], and 6
[https://github.com/gitgitgadget/git/pull/175]). Note: the latter PRs are not necessarily up to date, and will be re-targeted to the appropriate branches in https://github.com/gitster/git as soon as Junio picks them up.

This here patch series reflects the part that was submitted a couple of times (see https://github.com/gitgitgadget/git/pull/103) during the Outreachy project by Slavica Ðukic that continued the journey based on an initial patch series by Daniel Ferreira.

It only implements the status and the help part, in the interest of making the review remotely more reviewable.

As I am a heavy user of git add -p myself and use a patched version for weeks already (it is so nice to not suffer over one second startup until the MSYS2 Perl finally shows me anything, instead it feels instantaneous), I integrated these patch series into Git for Windows' master already, as an opt-in feature guarded by the config variable add.interactive.useBuiltin (and Git for Windows' installer is prepared to detect this version and offer the option in the graphical user interface).

I had planned on submitting this before v2.22.0-rc0, but there was such a backlog of builds from a big pushout that I had to wait ;-)

Changes since v1:

* The config machinery was reworked completely, to not use a callback to git_config(), but instead to query the config via the repo_config_get_*() functions. This also prevents a future "Huh???" moment: the internal add --interactive API accepts a parameter of type struct repository *r, but the previous configuration did not use that to query the config (and could in the future be a repository other than the_repository).

* As a consequence, the color sequences are no longer stored in file-local variables, but passed around via a struct.

* Instead of using the magical constant -2 to quit the main loop, it is now defined as LIST_AND_CHOOSE_QUIT (and likewise, LIST_AND_CHOOSE_ERROR is defined as -1 and used where appropriate).

* Improved the add_prefix_item() function by avoiding buffer overruns, not reusing the struct that is used for lookup also for adding the new item, and by strengthening the bug check.

Daniel Ferreira (2):
diff: export diffstat interface
built-in add -i: implement the status command

Johannes Schindelin (6):
Start to implement a built-in version of git add --interactive built-in add -i: refresh the index before running status built-in add -i: color the header in the status command built-in add -i: implement the main loop
built-in add -i: support ? (prompt help)
built-in add -i: implement the help command

Slavica Djukic (3):
Add a function to determine unique prefixes for a list of strings built-in add -i: show unique prefixes of the commands built-in add -i: use color in the main loop

 Documentation/config/add.txt |   5 +
 Makefile                     |   3 +
 add-interactive.c            | 558 +++++++++++++++++++++++++++++++++++
 add-interactive.h            |   8 +
 builtin/add.c                |  10 +
 diff.c                       |  37 +--
 diff.h                       |  19 ++
 prefix-map.c                 | 109 +++++++
 prefix-map.h                 |  40 +++
 repository.c                 |  19 ++
 repository.h                 |   7 +
 t/README                     |   4 +
 t/helper/test-prefix-map.c   |  58 ++++
 t/helper/test-tool.c         |   1 +
 t/helper/test-tool.h         |   1 +
 t/t0016-prefix-map.sh        |  10 +
 t/t3701-add-interactive.sh   |  24 ++
 17 files changed, 891 insertions(+), 22 deletions(-)
 create mode 100644 add-interactive.c
 create mode 100644 add-interactive.h
 create mode 100644 prefix-map.c
 create mode 100644 prefix-map.h
 create mode 100644 t/helper/test-prefix-map.c
 create mode 100755 t/t0016-prefix-map.sh

base-commit: 8104ec994ea3849a968b4667d072fedd1e688642
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-170%2Fdscho%2Fadd-i-in-c-status-and-help-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-170/dscho/add-i-in-c-status-and-help-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/170

Range-diff vs v1:

  1:  12978dc248 !  1:  ed53346b92 Start to implement a built-in version of `git add --interactive`
     @@ -62,12 +62,6 @@
      @@
      +#include "cache.h"
      +#include "add-interactive.h"
     -+#include "config.h"
     -+
     -+int add_i_config(const char *var, const char *value, void *cb)
     -+{
     -+	return git_default_config(var, value, cb);
     -+}
      +
      +int run_add_i(struct repository *r, const struct pathspec *ps)
      +{
     @@ -82,8 +76,6 @@
      +#ifndef ADD_INTERACTIVE_H
      +#define ADD_INTERACTIVE_H
      +
     -+int add_i_config(const char *var, const char *value, void *cb);
     -+
      +struct repository;
      +struct pathspec;
      +int run_add_i(struct repository *r, const struct pathspec *ps);
     @@ -102,50 +94,28 @@
       static const char * const builtin_add_usage[] = {
       	N_("git add [<options>] [--] <pathspec>..."),
      @@
     - static int patch_interactive, add_interactive, edit_interactive;
     - static int take_worktree_changes;
     - static int add_renormalize;
     -+static int use_builtin_add_i;
     - 
     - struct update_callback_data {
     - 	int flags;
     -@@
     + {
       	int status, i;
       	struct argv_array argv = ARGV_ARRAY_INIT;
     - 
     -+	if (use_builtin_add_i && !patch_mode)
     -+		return !!run_add_i(the_repository, pathspec);
     ++	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 && !patch_mode)
     ++		return !!run_add_i(the_repository, pathspec);
     + 
       	argv_array_push(&argv, "add--interactive");
       	if (patch_mode)
     - 		argv_array_push(&argv, patch_mode);
      @@
       		ignore_add_errors = git_config_bool(var, value);
       		return 0;
       	}
     --	return git_default_config(var, value, cb);
     -+	if (!strcmp(var, "add.interactive.usebuiltin")) {
     -+		use_builtin_add_i = git_config_bool(var, value);
     -+		return 0;
     -+	}
      +
     -+	return add_i_config(var, value, cb);
     + 	return git_default_config(var, value, cb);
       }
     - static const char embedded_advice[] = N_(
     -@@
     - 	int require_pathspec;
     - 	char *seen = NULL;
     - 	struct lock_file lock_file = LOCK_INIT;
     -+	int use_builtin_add_i_env =
     -+		git_env_bool("GIT_TEST_ADD_I_USE_BUILTIN", -1);
     - 
     - 	git_config(add_config, NULL);
     -+	if (use_builtin_add_i_env >= 0)
     -+		use_builtin_add_i = use_builtin_add_i_env;
     - 
     - 	argc = parse_options(argc, argv, prefix, builtin_add_options,
     - 			  builtin_add_usage, PARSE_OPT_KEEP_ARGV0);
      
       diff --git a/t/README b/t/README
       --- a/t/README
  2:  06ba1ae344 =  2:  bc99009fbf diff: export diffstat interface
  3:  be9ab904d1 !  3:  5e23c0756b built-in add -i: implement the `status` command
     @@ -30,16 +30,10 @@
      @@
       #include "cache.h"
       #include "add-interactive.h"
     - #include "config.h"
      +#include "diffcore.h"
      +#include "revision.h"
      +#include "refs.h"
     - 
     - int add_i_config(const char *var, const char *value, void *cb)
     - {
     - 	return git_default_config(var, value, cb);
     - }
     - 
     ++
      +struct item {
      +	const char *name;
      +};
     @@ -276,7 +270,7 @@
      +
      +	return 0;
      +}
     -+
     + 
       int run_add_i(struct repository *r, const struct pathspec *ps)
       {
      -	die(_("No commands are available in the built-in `git add -i` yet!"));
  4:  a512e14609 =  4:  8cafc6ae8d built-in add -i: refresh the index before running `status`
  5:  f7c8df058c <  -:  ---------- built-in add -i: color the header in the `status` command
  -:  ---------- >  5:  83d92a9762 built-in add -i: color the header in the `status` command
  6:  93b3151b6c !  6:  3eec219124 built-in add -i: implement the main loop
     @@ -53,8 +53,8 @@
       	void (*print_item)(int i, struct item *item, void *print_item_data);
       	void *print_item_data;
      @@
     - 
     - static void list(struct item **list, size_t nr, struct list_options *opts)
     + static void list(struct item **list, size_t nr,
     + 		 struct add_i_state *s, struct list_options *opts)
       {
      -	int i;
      +	int i, last_lf = 0;
     @@ -85,28 +85,35 @@
      +	const char *prompt;
      +};
      +
     ++#define LIST_AND_CHOOSE_ERROR (-1)
     ++#define LIST_AND_CHOOSE_QUIT  (-2)
     ++
      +/*
      + * Returns the selected index.
     ++ *
     ++ * If an error occurred, returns `LIST_AND_CHOOSE_ERROR`. Upon EOF,
     ++ * `LIST_AND_CHOOSE_QUIT` is returned.
      + */
      +static ssize_t list_and_choose(struct item **items, size_t nr,
     ++			       struct add_i_state *s,
      +			       struct list_and_choose_options *opts)
      +{
      +	struct strbuf input = STRBUF_INIT;
     -+	ssize_t res = -1;
     ++	ssize_t res = LIST_AND_CHOOSE_ERROR;
      +
      +	for (;;) {
      +		char *p, *endp;
      +
      +		strbuf_reset(&input);
      +
     -+		list(items, nr, &opts->list_opts);
     ++		list(items, nr, s, &opts->list_opts);
      +
      +		printf("%s%s", opts->prompt, "> ");
      +		fflush(stdout);
      +
      +		if (strbuf_getline(&input, stdin) == EOF) {
      +			putchar('\n');
     -+			res = -2;
     ++			res = LIST_AND_CHOOSE_QUIT;
      +			break;
      +		}
      +		strbuf_trim(&input);
     @@ -143,7 +150,7 @@
      +			p += sep + 1;
      +		}
      +
     -+		if (res >= 0)
     ++		if (res != LIST_AND_CHOOSE_ERROR)
      +			break;
       	}
      +
     @@ -164,12 +171,13 @@
      +
      +struct command_item {
      +	struct item item;
     -+	int (*command)(struct repository *r, const struct pathspec *ps,
     ++	int (*command)(struct add_i_state *s, const struct pathspec *ps,
      +		       struct file_list *files, struct list_options *opts);
      +};
      +
       int run_add_i(struct repository *r, const struct pathspec *ps)
       {
     + 	struct add_i_state s = { NULL };
      +	struct list_and_choose_options main_loop_opts = {
      +		{ 4, N_("*** Commands ***"), print_command_item, NULL },
      +		N_("What now")
     @@ -192,21 +200,21 @@
      +	ssize_t i;
       	int res = 0;
       
     - 	strbuf_addstr(&header, "      ");
     + 	if (init_add_i_state(r, &s))
      @@
     - 	if (run_status(r, ps, &files, &opts) < 0)
     + 	if (run_status(&s, ps, &files, &opts) < 0)
       		res = -1;
       
      +	for (;;) {
      +		i = list_and_choose((struct item **)commands,
     -+				    ARRAY_SIZE(commands), &main_loop_opts);
     -+		if (i < -1) {
     ++				    ARRAY_SIZE(commands), &s, &main_loop_opts);
     ++		if (i == LIST_AND_CHOOSE_QUIT) {
      +			printf(_("Bye.\n"));
      +			res = 0;
      +			break;
      +		}
     -+		if (i >= 0)
     -+			res = commands[i]->command(r, ps, &files, &opts);
     ++		if (i != LIST_AND_CHOOSE_ERROR)
     ++			res = commands[i]->command(&s, ps, &files, &opts);
      +	}
      +
       	release_file_list(&files);
  7:  db1ede3636 !  7:  e02a52c3ac Add a function to determine unique prefixes for a list of strings
     @@ -92,40 +92,40 @@
      +static void add_prefix_item(struct prefix_map *prefix_map,
      +			    struct prefix_item *item)
      +{
     -+	struct prefix_map_entry *e = xmalloc(sizeof(*e)), *e2;
     ++	struct prefix_map_entry e = { { NULL } }, *e2;
      +	int j;
      +
     -+	e->item = item;
     -+	e->name = e->item->name;
     ++	e.item = item;
     ++	e.name = item->name;
      +
     -+	for (j = prefix_map->min_length; j <= prefix_map->max_length; j++) {
     -+		if (!isascii(e->name[j])) {
     -+			free(e);
     ++	for (j = prefix_map->min_length;
     ++	     j <= prefix_map->max_length && e.name[j]; j++) {
     ++		/* Avoid breaking UTF-8 multi-byte sequences */
     ++		if (!isascii(e.name[j]))
      +			break;
     -+		}
      +
     -+		e->prefix_length = j;
     -+		hashmap_entry_init(e, memhash(e->name, j));
     -+		e2 = hashmap_get(&prefix_map->map, e, NULL);
     ++		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);
     ++			/* prefix is unique at this stage */
     ++			item->prefix_length = j;
     ++			add_prefix_entry(&prefix_map->map, e.name, j, item);
      +			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);
     ++		if (j != e2->item->prefix_length || memcmp(e.name, e2->name, j))
     ++			BUG("unexpected prefix length: %d != %d (%s != %s)",
     ++			    j, (int)e2->item->prefix_length, e.name, e2->name);
      +
      +		/* skip common prefix */
     -+		for (; j < prefix_map->max_length && e->name[j]; j++) {
     -+			if (e->item->name[j] != e2->item->name[j])
     ++		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,
     ++			add_prefix_entry(&prefix_map->map, e.name, j + 1,
      +					 NULL);
      +		}
      +
     @@ -140,16 +140,14 @@
      +			e2->item->prefix_length = 0;
      +		e2->item = NULL;
      +
     -+		if (j < prefix_map->max_length && e->name[j]) {
     ++		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 {
     ++			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);
     -+		}
     ++			e.item->prefix_length = 0;
      +
      +		break;
      +	}
  8:  56acc31e96 !  8:  ced9b6aced built-in add -i: show unique prefixes of the commands
     @@ -22,10 +22,10 @@
       #include "refs.h"
      +#include "prefix-map.h"
       
     - static int use_color = -1;
     - 
     + struct add_i_state {
     + 	struct repository *r;
      @@
     - 	return git_color_default_config(var, value, cb);
     + 	return 0;
       }
       
      -struct item {
     @@ -57,22 +57,22 @@
       	void *print_item_data;
       };
       
     --static void list(struct item **list, size_t nr, struct list_options *opts)
     +-static void list(struct item **list, size_t nr,
      +static void list(struct prefix_item **list, size_t nr,
     -+		 struct list_options *opts)
     + 		 struct add_i_state *s, struct list_options *opts)
       {
       	int i, last_lf = 0;
     - 
      @@
     - /*
     -  * Returns the selected index.
     +  * If an error occurred, returns `LIST_AND_CHOOSE_ERROR`. Upon EOF,
     +  * `LIST_AND_CHOOSE_QUIT` is returned.
        */
      -static ssize_t list_and_choose(struct item **items, size_t nr,
      +static ssize_t list_and_choose(struct prefix_item **items, size_t nr,
     + 			       struct add_i_state *s,
       			       struct list_and_choose_options *opts)
       {
       	struct strbuf input = STRBUF_INIT;
     - 	ssize_t res = -1;
     + 	ssize_t res = LIST_AND_CHOOSE_ERROR;
       
      +	find_unique_prefixes(items, nr, 1, 4);
      +
     @@ -133,8 +133,8 @@
       		return -1;
       
       	if (files->nr)
     --		list((struct item **)files->file, files->nr, opts);
     -+		list((struct prefix_item **)files->file, files->nr, opts);
     +-		list((struct item **)files->file, files->nr, s, opts);
     ++		list((struct prefix_item **)files->file, files->nr, s, opts);
       	putchar('\n');
       
       	return 0;
     @@ -157,7 +157,7 @@
       struct command_item {
      -	struct item item;
      +	struct prefix_item item;
     - 	int (*command)(struct repository *r, const struct pathspec *ps,
     + 	int (*command)(struct add_i_state *s, const struct pathspec *ps,
       		       struct file_list *files, struct list_options *opts);
       };
      @@
     @@ -166,6 +166,6 @@
       	for (;;) {
      -		i = list_and_choose((struct item **)commands,
      +		i = list_and_choose((struct prefix_item **)commands,
     - 				    ARRAY_SIZE(commands), &main_loop_opts);
     - 		if (i < -1) {
     + 				    ARRAY_SIZE(commands), &s, &main_loop_opts);
     + 		if (i == LIST_AND_CHOOSE_QUIT) {
       			printf(_("Bye.\n"));
  9:  ddd0ee51f6 !  9:  7378af60ad built-in add -i: support `?` (prompt help)
     @@ -12,41 +12,35 @@
       --- a/add-interactive.c
       +++ b/add-interactive.c
      @@
     - 
     - enum color_add_i {
     - 	COLOR_HEADER = 0,
     -+	COLOR_HELP,
     - };
     - 
     - static char list_colors[][COLOR_MAXLEN] = {
     - 	GIT_COLOR_BOLD,      /* Header */
     -+	GIT_COLOR_BOLD_RED,  /* Help */
     + 	struct repository *r;
     + 	int use_color;
     + 	char header_color[COLOR_MAXLEN];
     ++	char help_color[COLOR_MAXLEN];
       };
       
     - static const char *get_add_i_color(enum color_add_i ix)
     + static void init_color(struct repository *r, struct add_i_state *s,
      @@
     - {
     - 	if (!strcasecmp(slot, "header"))
     - 		return COLOR_HEADER;
     -+	if (!strcasecmp(slot, "help"))
     -+		return COLOR_HELP;
     + 	s->use_color = want_color(s->use_color);
     + 
     + 	init_color(r, s, "header", s->header_color, GIT_COLOR_BOLD);
     ++	init_color(r, s, "help", s->help_color, GIT_COLOR_BOLD_RED);
       
     - 	return -1;
     + 	return 0;
       }
      @@
       	struct list_options list_opts;
       
       	const char *prompt;
     -+	void (*print_help)(void);
     ++	void (*print_help)(struct add_i_state *s);
       };
       
     - /*
     + #define LIST_AND_CHOOSE_ERROR (-1)
      @@
       		if (!input.len)
       			break;
       
      +		if (!strcmp(input.buf, "?")) {
     -+			opts->print_help();
     ++			opts->print_help(s);
      +			continue;
      +		}
      +
     @@ -57,9 +51,9 @@
       		       struct file_list *files, struct list_options *opts);
       };
       
     -+static void command_prompt_help(void)
     ++static void command_prompt_help(struct add_i_state *s)
      +{
     -+	const char *help_color = get_add_i_color(COLOR_HELP);
     ++	const char *help_color = s->help_color;
      +	color_fprintf_ln(stdout, help_color, "%s", _("Prompt help:"));
      +	color_fprintf_ln(stdout, help_color, "1          - %s",
      +			 _("select a numbered item"));
     @@ -71,6 +65,7 @@
      +
       int run_add_i(struct repository *r, const struct pathspec *ps)
       {
     + 	struct add_i_state s = { NULL };
       	struct list_and_choose_options main_loop_opts = {
       		{ 4, N_("*** Commands ***"), print_command_item, NULL },
      -		N_("What now")
 10:  3e1a88d9c0 ! 10:  36edef85e6 built-in add -i: use color in the main loop
     @@ -12,51 +12,31 @@
       --- a/add-interactive.c
       +++ b/add-interactive.c
      @@
     - enum color_add_i {
     - 	COLOR_HEADER = 0,
     - 	COLOR_HELP,
     -+	COLOR_PROMPT,
     -+	COLOR_ERROR,
     -+	COLOR_RESET,
     + 	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];
       };
       
     - static char list_colors[][COLOR_MAXLEN] = {
     - 	GIT_COLOR_BOLD,      /* Header */
     - 	GIT_COLOR_BOLD_RED,  /* Help */
     -+	GIT_COLOR_BOLD_BLUE, /* Prompt */
     -+	GIT_COLOR_BOLD_RED,  /* Error */
     -+	GIT_COLOR_RESET,     /* Reset */
     - };
     - 
     - static const char *get_add_i_color(enum color_add_i ix)
     + static void init_color(struct repository *r, struct add_i_state *s,
      @@
     - 		return COLOR_HEADER;
     - 	if (!strcasecmp(slot, "help"))
     - 		return COLOR_HELP;
     -+	if (!strcasecmp(slot, "prompt"))
     -+		return COLOR_PROMPT;
     -+	if (!strcasecmp(slot, "error"))
     -+		return COLOR_ERROR;
     -+	if (!strcasecmp(slot, "reset"))
     -+		return COLOR_RESET;
       
     - 	return -1;
     - }
     -@@
     - static ssize_t list_and_choose(struct prefix_item **items, size_t nr,
     - 			       struct list_and_choose_options *opts)
     - {
     -+	const char *prompt_color = get_add_i_color(COLOR_PROMPT);
     -+	const char *error_color = get_add_i_color(COLOR_ERROR);
     - 	struct strbuf input = STRBUF_INIT;
     - 	ssize_t res = -1;
     + 	init_color(r, s, "header", s->header_color, GIT_COLOR_BOLD);
     + 	init_color(r, s, "help", s->help_color, GIT_COLOR_BOLD_RED);
     ++	init_color(r, s, "prompt", s->prompt_color, GIT_COLOR_BOLD_BLUE);
     ++	init_color(r, s, "error", s->error_color, GIT_COLOR_BOLD_RED);
     ++	init_color(r, s, "reset", s->reset_color, GIT_COLOR_RESET);
       
     + 	return 0;
     + }
      @@
       
     - 		list(items, nr, &opts->list_opts);
     + 		list(items, nr, s, &opts->list_opts);
       
      -		printf("%s%s", opts->prompt, "> ");
     -+		color_fprintf(stdout, prompt_color, "%s", opts->prompt);
     ++		color_fprintf(stdout, s->prompt_color, "%s", opts->prompt);
      +		fputs("> ", stdout);
       		fflush(stdout);
       
     @@ -66,7 +46,7 @@
       
       			if (index < 0 || index >= nr)
      -				printf(_("Huh (%s)?\n"), p);
     -+				color_fprintf_ln(stdout, error_color,
     ++				color_fprintf_ln(stdout, s->error_color,
      +						 _("Huh (%s)?"), p);
       			else {
       				res = index;
     @@ -96,20 +76,32 @@
       }
       
      @@
     - 
       int run_add_i(struct repository *r, const struct pathspec *ps)
       {
     -+	struct print_command_item_data data = {
     -+		/*
     -+		 * When color was asked for, use the prompt color for
     -+		 * highlighting, otherwise use square brackets.
     -+		 */
     -+		want_color(use_color) ? get_add_i_color(COLOR_PROMPT) : "[",
     -+		want_color(use_color) ? get_add_i_color(COLOR_RESET) : "]"
     -+	};
     + 	struct add_i_state s = { NULL };
     ++	struct print_command_item_data data;
       	struct list_and_choose_options main_loop_opts = {
      -		{ 4, N_("*** Commands ***"), print_command_item, NULL },
      +		{ 4, N_("*** Commands ***"), print_command_item, &data },
       		N_("What now"), command_prompt_help
       	};
       	struct command_item
     +@@
     + 	if (init_add_i_state(r, &s))
     + 		return error("could not parse `add -i` config");
     + 
     ++	/*
     ++	 * When color was asked for, use the prompt color for
     ++	 * highlighting, otherwise use square brackets.
     ++	 */
     ++	if (s.use_color) {
     ++		data.color = s.prompt_color;
     ++		data.reset = s.reset_color;
     ++	} else {
     ++		data.color = "[";
     ++		data.reset = "]";
     ++	}
     ++
     + 	strbuf_addstr(&header, "      ");
     + 	strbuf_addf(&header, print_file_item_data.modified_fmt,
     + 		    _("staged"), _("unstaged"), _("path"));
 11:  481e3316d5 ! 11:  266dbf2a6b built-in add -i: implement the `help` command
     @@ -18,10 +18,10 @@
       	return 0;
       }
       
     -+static int run_help(struct repository *r, const struct pathspec *ps,
     ++static int run_help(struct add_i_state *s, const struct pathspec *ps,
      +		    struct file_list *files, struct list_options *opts)
      +{
     -+	const char *help_color = get_add_i_color(COLOR_HELP);
     ++	const char *help_color = s->help_color;
      +
      +	color_fprintf_ln(stdout, help_color, "status        - %s",
      +			 _("show paths with changes"));
Signature · 1 line
gitgitgadget
#22 lore raw

PATCH v2 1/11 Start to implement a built-in version of `git add --interactive`

From: Johannes Schindelin <[email protected]>

This is hardly the first conversion of a Git command that is implemented as a script to a built-in. So far, the most successful strategy for such conversions has been to add a built-in helper and call that for more and more functionality from the script, as more and more parts are converted.

With the interactive add, we choose a different strategy. The sole reason for this is that on Windows (where such a conversion has the most benefits in terms of speed and robustness) we face the very specific problem that a system() call in Perl seems to close stdin in the parent process when the spawned process consumes even one character from stdin. And that just does not work for us here, as it would stop the main loop as soon as any interactive command was performed by the helper. Which is almost all of the commands in git add -i.

It is almost as if Perl told us once again that it does not want us to use it on Windows.

Instead, we follow the opposite route where we start with a bare-bones version of the built-in interactive add, guarded by the new add.interactive.useBuiltin config variable, and then add more and more functionality to it, until it is feature complete.

At this point, the built-in version of git add -i only states that it cannot do anything yet ;-)

---
 Documentation/config/add.txt |  5 +++++
 Makefile                     |  1 +
 add-interactive.c            |  7 +++++++
 add-interactive.h            |  8 ++++++++
 builtin/add.c                | 10 ++++++++++
 t/README                     |  4 ++++
 6 files changed, 35 insertions(+)
 create mode 100644 add-interactive.c
 create mode 100644 add-interactive.h
Documentation/config/add.txt +5 -0
index 4d753f006e..c9f748f81c 100644
@@ -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.
Makefile +1 -0
index c5240942f2..18e656a32f 100644
@@ -848,6 +848,7 @@ LIB_H = $(shell $(FIND) . \
 	-name '*.h' -print)
 
 LIB_OBJS += abspath.o
+LIB_OBJS += add-interactive.o
 LIB_OBJS += advice.o
 LIB_OBJS += alias.o
 LIB_OBJS += alloc.o
add-interactive.c new file +7 -0
new file mode 100644
index 0000000000..482e458dc6
@@ -0,0 +1,7 @@
+#include "cache.h"
+#include "add-interactive.h"
+
+int run_add_i(struct repository *r, const struct pathspec *ps)
+{
+	die(_("No commands are available in the built-in `git add -i` yet!"));
+}
add-interactive.h new file +8 -0
new file mode 100644
index 0000000000..7043b8741d
@@ -0,0 +1,8 @@
+#ifndef ADD_INTERACTIVE_H
+#define ADD_INTERACTIVE_H
+
+struct repository;
+struct pathspec;
+int run_add_i(struct repository *r, const struct pathspec *ps);
+
+#endif
builtin/add.c +10 -0
index db2dfa4350..e19d66e894 100644
@@ -20,6 +20,7 @@
 #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>..."),
@@ -185,6 +186,14 @@ int run_add_interactive(const char *revision, const char *patch_mode,
 {
 	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 && !patch_mode)
+		return !!run_add_i(the_repository, pathspec);
 
 	argv_array_push(&argv, "add--interactive");
 	if (patch_mode)
@@ -319,6 +328,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);
 }
 
t/README +4 -0
index 886bbec5bc..6408a1847e 100644
@@ -383,6 +383,10 @@ GIT_TEST_REBASE_USE_BUILTIN=<boolean>, when false, disables the
 builtin version of git-rebase. See 'rebase.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
Signature · 1 line
gitgitgadget
#23 lore raw

PATCH v2 2/11 diff: export diffstat interface

In reply to Johannes Schindelin via GitGitGadget

From: Daniel Ferreira <[email protected]>

Make the diffstat interface (namely, the diffstat_t struct and compute_diffstat) no longer be internal to diff.c and allow it to be used by other parts of git.

This is helpful for code that may want to easily extract information from files using the diff machinery, while flushing it differently from how the show_* functions used by diff_flush() do it. One example is the builtin implementation of git-add--interactive's status.

---
 diff.c | 37 +++++++++++++++----------------------
 diff.h | 19 +++++++++++++++++++
 2 files changed, 34 insertions(+), 22 deletions(-)
diff.c +15 -22
index 5306c48652..daa5f3a736 100644
@@ -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)
@@ -6001,12 +5985,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)
@@ -6306,6 +6285,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,
diff.h +19 -0
index b512d0477a..ae9bedfab8 100644
@@ -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
 
Signature · 1 line
gitgitgadget
#24 lore raw

PATCH v2 3/11 built-in add -i: implement the `status` command

In reply to Johannes Schindelin via GitGitGadget

From: Daniel Ferreira <[email protected]>

This implements the status command of git add -i. The data structures introduced in this commit will be extended as needed later.

At this point, we re-implement only part of the list_and_choose() function of the Perl script git-add--interactive.perl and call it list(). It does not yet color anything, or do columns, or allow user input.

Over the course of the next commits, we will introduce a list_and_choose() function that uses list() to display the list of options and let the user choose one or more of the displayed items. This will be used to implement the main loop of the built-in git add -i, at which point the new status command can actually be used.

Note that we pass the list of items as a struct item ** as opposed to a struct item *, to allow for the actual items to contain much more information than merely the name.

---
 add-interactive.c | 265 +++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 264 insertions(+), 1 deletion(-)
add-interactive.c +264 -1
index 482e458dc6..59b28011f7 100644
@@ -1,7 +1,270 @@
 #include "cache.h"
 #include "add-interactive.h"
+#include "diffcore.h"
+#include "revision.h"
+#include "refs.h"
+
+struct item {
+	const char *name;
+};
+
+struct list_options {
+	const char *header;
+	void (*print_item)(int i, struct item *item, void *print_item_data);
+	void *print_item_data;
+};
+
+static void list(struct item **list, size_t nr, struct list_options *opts)
+{
+	int i;
+
+	if (!nr)
+		return;
+
+	if (opts->header)
+		printf("%s\n", opts->header);
+
+	for (i = 0; i < nr; i++) {
+		opts->print_item(i, list[i], opts->print_item_data);
+		putchar('\n');
+	}
+}
+
+struct adddel {
+	uintmax_t add, del;
+	unsigned seen:1, binary:1;
+};
+
+struct file_list {
+	struct file_item {
+		struct item item;
+		struct adddel index, worktree;
+	} **file;
+	size_t nr, alloc;
+};
+
+static void add_file_item(struct file_list *list, const char *name)
+{
+	struct file_item *item;
+
+	FLEXPTR_ALLOC_STR(item, item.name, name);
+
+	ALLOC_GROW(list->file, list->nr + 1, list->alloc);
+	list->file[list->nr++] = item;
+}
+
+static void reset_file_list(struct file_list *list)
+{
+	size_t i;
+
+	for (i = 0; i < list->nr; i++)
+		free(list->file[i]);
+	list->nr = 0;
+}
+
+static void release_file_list(struct file_list *list)
+{
+	reset_file_list(list);
+	FREE_AND_NULL(list->file);
+	list->alloc = 0;
+}
+
+static int file_item_cmp(const void *a, const void *b)
+{
+	const struct file_item * const *f1 = a;
+	const struct file_item * const *f2 = b;
+
+	return strcmp((*f1)->item.name, (*f2)->item.name);
+}
+
+struct pathname_entry {
+	struct hashmap_entry ent;
+	size_t index;
+	char pathname[FLEX_ARRAY];
+};
+
+static int pathname_entry_cmp(const void *unused_cmp_data,
+			      const void *entry, const void *entry_or_key,
+			      const void *pathname)
+{
+	const struct pathname_entry *e1 = entry, *e2 = entry_or_key;
+
+	return strcmp(e1->pathname,
+		      pathname ? (const char *)pathname : e2->pathname);
+}
+
+struct collection_status {
+	enum { FROM_WORKTREE = 0, FROM_INDEX = 1 } phase;
+
+	const char *reference;
+
+	struct file_list *list;
+	struct hashmap file_map;
+};
+
+static void collect_changes_cb(struct diff_queue_struct *q,
+			       struct diff_options *options,
+			       void *data)
+{
+	struct collection_status *s = data;
+	struct diffstat_t stat = { 0 };
+	int i;
+
+	if (!q->nr)
+		return;
+
+	compute_diffstat(options, &stat, q);
+
+	for (i = 0; i < stat.nr; i++) {
+		const char *name = stat.files[i]->name;
+		int hash = strhash(name);
+		struct pathname_entry *entry;
+		size_t file_index;
+		struct file_item *file;
+		struct adddel *adddel;
+
+		entry = hashmap_get_from_hash(&s->file_map, hash, name);
+		if (entry)
+			file_index = entry->index;
+		else {
+			FLEX_ALLOC_STR(entry, pathname, name);
+			hashmap_entry_init(entry, hash);
+			entry->index = file_index = s->list->nr;
+			hashmap_add(&s->file_map, entry);
+
+			add_file_item(s->list, name);
+		}
+		file = s->list->file[file_index];
+
+		adddel = s->phase == FROM_INDEX ? &file->index : &file->worktree;
+		adddel->seen = 1;
+		adddel->add = stat.files[i]->added;
+		adddel->del = stat.files[i]->deleted;
+		if (stat.files[i]->is_binary)
+			adddel->binary = 1;
+	}
+}
+
+static int get_modified_files(struct repository *r, struct file_list *list,
+			      const struct pathspec *ps)
+{
+	struct object_id head_oid;
+	int is_initial = !resolve_ref_unsafe("HEAD", RESOLVE_REF_READING,
+					     &head_oid, NULL);
+	struct collection_status s = { FROM_WORKTREE };
+
+	if (repo_read_index_preload(r, ps, 0) < 0)
+		return error(_("could not read index"));
+
+	s.list = list;
+	hashmap_init(&s.file_map, pathname_entry_cmp, NULL, 0);
+
+	for (s.phase = FROM_WORKTREE; s.phase <= FROM_INDEX; s.phase++) {
+		struct rev_info rev;
+		struct setup_revision_opt opt = { 0 };
+
+		opt.def = is_initial ?
+			empty_tree_oid_hex() : oid_to_hex(&head_oid);
+
+		init_revisions(&rev, NULL);
+		setup_revisions(0, NULL, &rev, &opt);
+
+		rev.diffopt.output_format = DIFF_FORMAT_CALLBACK;
+		rev.diffopt.format_callback = collect_changes_cb;
+		rev.diffopt.format_callback_data = &s;
+
+		if (ps)
+			copy_pathspec(&rev.prune_data, ps);
+
+		if (s.phase == FROM_INDEX)
+			run_diff_index(&rev, 1);
+		else {
+			rev.diffopt.flags.ignore_dirty_submodules = 1;
+			run_diff_files(&rev, 0);
+		}
+	}
+	hashmap_free(&s.file_map, 1);
+
+	/* While the diffs are ordered already, we ran *two* diffs... */
+	QSORT(list->file, list->nr, file_item_cmp);
+
+	return 0;
+}
+
+static void populate_wi_changes(struct strbuf *buf,
+				struct adddel *ad, const char *no_changes)
+{
+	if (ad->binary)
+		strbuf_addstr(buf, _("binary"));
+	else if (ad->seen)
+		strbuf_addf(buf, "+%"PRIuMAX"/-%"PRIuMAX,
+			    (uintmax_t)ad->add, (uintmax_t)ad->del);
+	else
+		strbuf_addstr(buf, no_changes);
+}
+
+struct print_file_item_data {
+	const char *modified_fmt;
+	struct strbuf buf, index, worktree;
+};
+
+static void print_file_item(int i, struct item *item,
+			    void *print_file_item_data)
+{
+	struct file_item *c = (struct file_item *)item;
+	struct print_file_item_data *d = print_file_item_data;
+
+	strbuf_reset(&d->index);
+	strbuf_reset(&d->worktree);
+	strbuf_reset(&d->buf);
+
+	populate_wi_changes(&d->worktree, &c->worktree, _("nothing"));
+	populate_wi_changes(&d->index, &c->index, _("unchanged"));
+	strbuf_addf(&d->buf, d->modified_fmt,
+		    d->index.buf, d->worktree.buf, item->name);
+
+	printf(" %2d: %s", i + 1, d->buf.buf);
+}
+
+static int run_status(struct repository *r, const struct pathspec *ps,
+		      struct file_list *files, struct list_options *opts)
+{
+	reset_file_list(files);
+
+	if (get_modified_files(r, files, ps) < 0)
+		return -1;
+
+	if (files->nr)
+		list((struct item **)files->file, files->nr, opts);
+	putchar('\n');
+
+	return 0;
+}
 
 int run_add_i(struct repository *r, const struct pathspec *ps)
 {
-	die(_("No commands are available in the built-in `git add -i` yet!"));
+	struct print_file_item_data print_file_item_data = {
+		"%12s %12s %s", STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
+	};
+	struct list_options opts = {
+		NULL, print_file_item, &print_file_item_data
+	};
+	struct strbuf header = STRBUF_INIT;
+	struct file_list files = { NULL };
+	int res = 0;
+
+	strbuf_addstr(&header, "      ");
+	strbuf_addf(&header, print_file_item_data.modified_fmt,
+		    _("staged"), _("unstaged"), _("path"));
+	opts.header = header.buf;
+
+	res = run_status(r, ps, &files, &opts);
+
+	release_file_list(&files);
+	strbuf_release(&print_file_item_data.buf);
+	strbuf_release(&print_file_item_data.index);
+	strbuf_release(&print_file_item_data.worktree);
+	strbuf_release(&header);
+
+	return res;
 }
Signature · 1 line
gitgitgadget
#25 lore raw

PATCH v2 4/11 built-in add -i: refresh the index before running `status`

In reply to Johannes Schindelin via GitGitGadget

From: Johannes Schindelin <[email protected]>

This is what the Perl version does, and therefore it is what the built-in version should do, too.

---
 add-interactive.c |  4 +++-
 repository.c      | 19 +++++++++++++++++++
 repository.h      |  7 +++++++
 3 files changed, 29 insertions(+), 1 deletion(-)
add-interactive.c +3 -1
index 59b28011f7..2dbf29dee2 100644
@@ -258,7 +258,9 @@ int run_add_i(struct repository *r, const struct pathspec *ps)
 		    _("staged"), _("unstaged"), _("path"));
 	opts.header = header.buf;
 
-	res = run_status(r, ps, &files, &opts);
+	repo_refresh_and_write_index(r, REFRESH_QUIET, 1);
+	if (run_status(r, ps, &files, &opts) < 0)
+		res = -1;
 
 	release_file_list(&files);
 	strbuf_release(&print_file_item_data.buf);
repository.c +19 -0
index 65e6f8b8fd..c90f310093 100644
@@ -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;
+}
repository.h +7 -0
index 8981649d43..fb49e0e328 100644
@@ -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 */
Signature · 1 line
gitgitgadget
#26 lore raw

PATCH v2 5/11 built-in add -i: color the header in the `status` command

In reply to Johannes Schindelin via GitGitGadget

From: Johannes Schindelin <[email protected]>

For simplicity, we only implemented the status command without colors. This patch starts adding color, matching what the Perl script git-add--interactive.perl does.

---
 add-interactive.c | 60 ++++++++++++++++++++++++++++++++++++++++++-----
 1 file changed, 54 insertions(+), 6 deletions(-)
add-interactive.c +54 -6
index 2dbf29dee2..6c2fca12c1 100644
@@ -1,9 +1,51 @@
 #include "cache.h"
 #include "add-interactive.h"
+#include "color.h"
+#include "config.h"
 #include "diffcore.h"
 #include "revision.h"
 #include "refs.h"
 
+struct add_i_state {
+	struct repository *r;
+	int use_color;
+	char header_color[COLOR_MAXLEN];
+};
+
+static void init_color(struct repository *r, struct add_i_state *s,
+		       const char *slot_name, char *dst,
+		       const char *default_color)
+{
+	char *key = xstrfmt("color.interactive.%s", slot_name);
+	const char *value;
+
+	if (!s->use_color)
+		dst[0] = '\0';
+	else if (repo_config_get_value(r, key, &value) ||
+		 color_parse(value, dst))
+		strlcpy(dst, default_color, COLOR_MAXLEN);
+
+	free(key);
+}
+
+static int init_add_i_state(struct repository *r, struct add_i_state *s)
+{
+	const char *value;
+
+	s->r = r;
+
+	if (repo_config_get_value(r, "color.interactive", &value))
+		s->use_color = -1;
+	else
+		s->use_color =
+			git_config_colorbool("color.interactive", value);
+	s->use_color = want_color(s->use_color);
+
+	init_color(r, s, "header", s->header_color, GIT_COLOR_BOLD);
+
+	return 0;
+}
+
 struct item {
 	const char *name;
 };
@@ -14,7 +56,8 @@ struct list_options {
 	void *print_item_data;
 };
 
-static void list(struct item **list, size_t nr, struct list_options *opts)
+static void list(struct item **list, size_t nr,
+		 struct add_i_state *s, struct list_options *opts)
 {
 	int i;
 
@@ -22,7 +65,8 @@ static void list(struct item **list, size_t nr, struct list_options *opts)
 		return;
 
 	if (opts->header)
-		printf("%s\n", opts->header);
+		color_fprintf_ln(stdout, s->header_color,
+				 "%s", opts->header);
 
 	for (i = 0; i < nr; i++) {
 		opts->print_item(i, list[i], opts->print_item_data);
@@ -226,16 +270,16 @@ static void print_file_item(int i, struct item *item,
 	printf(" %2d: %s", i + 1, d->buf.buf);
 }
 
-static int run_status(struct repository *r, const struct pathspec *ps,
+static int run_status(struct add_i_state *s, const struct pathspec *ps,
 		      struct file_list *files, struct list_options *opts)
 {
 	reset_file_list(files);
 
-	if (get_modified_files(r, files, ps) < 0)
+	if (get_modified_files(s->r, files, ps) < 0)
 		return -1;
 
 	if (files->nr)
-		list((struct item **)files->file, files->nr, opts);
+		list((struct item **)files->file, files->nr, s, opts);
 	putchar('\n');
 
 	return 0;
@@ -243,6 +287,7 @@ static int run_status(struct repository *r, const struct pathspec *ps,
 
 int run_add_i(struct repository *r, const struct pathspec *ps)
 {
+	struct add_i_state s = { NULL };
 	struct print_file_item_data print_file_item_data = {
 		"%12s %12s %s", STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
 	};
@@ -253,13 +298,16 @@ int run_add_i(struct repository *r, const struct pathspec *ps)
 	struct file_list files = { NULL };
 	int res = 0;
 
+	if (init_add_i_state(r, &s))
+		return error("could not parse `add -i` config");
+
 	strbuf_addstr(&header, "      ");
 	strbuf_addf(&header, print_file_item_data.modified_fmt,
 		    _("staged"), _("unstaged"), _("path"));
 	opts.header = header.buf;
 
 	repo_refresh_and_write_index(r, REFRESH_QUIET, 1);
-	if (run_status(r, ps, &files, &opts) < 0)
+	if (run_status(&s, ps, &files, &opts) < 0)
 		res = -1;
 
 	release_file_list(&files);
Signature · 1 line
gitgitgadget
#27 lore raw

PATCH v2 6/11 built-in add -i: implement the main loop

In reply to Johannes Schindelin via GitGitGadget

From: Johannes Schindelin <[email protected]>

The reason why we did not start with the main loop to begin with is that it is the first user of list_and_choose(), which uses the list() function that we conveniently introduced for use by the status command.

Apart from the "and choose" part, there are more differences between the way the status command calls the list_and_choose() function in the Perl version of git add -i compared to the other callers of said function. The most important ones:

- The list is not only shown, but the user is also asked to make a choice, possibly selecting multiple entries.

- The list of items is prefixed with a marker indicating what items have been selected, if multi-selection is allowed.

- Initially, for each item a unique prefix (if there exists any within the given parameters) is determined, and shown in the list, and accepted as a shortcut for the selection.

These features will be implemented later, except the part where the user can choose a command. At this stage, though, the built-in git add -i still only supports the status command, with the remaining commands to follow over the course of the next commits.

In addition, we also modify list() to support displaying the commands in columns, even if there is currently only one.

The Perl script git-add--interactive.perl mixed the purposes of the "list" and the "and choose" part into the same function. In the C version, we will keep them separate instead, calling the list() function from the list_and_choose() function.

Note that we only have a prompt ending in a single ">" at this stage; later commits will add commands that display a double ">>" to indicate that the user is in a different loop than the main one.

---
 add-interactive.c | 129 +++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 127 insertions(+), 2 deletions(-)
add-interactive.c +127 -2
index 6c2fca12c1..0ea4f3edb8 100644
@@ -51,6 +51,7 @@ struct item {
 };
 
 struct list_options {
+	int columns;
 	const char *header;
 	void (*print_item)(int i, struct item *item, void *print_item_data);
 	void *print_item_data;
@@ -59,7 +60,7 @@ struct list_options {
 static void list(struct item **list, size_t nr,
 		 struct add_i_state *s, struct list_options *opts)
 {
-	int i;
+	int i, last_lf = 0;
 
 	if (!nr)
 		return;
@@ -70,8 +71,97 @@ static void list(struct item **list, size_t nr,
 
 	for (i = 0; i < nr; i++) {
 		opts->print_item(i, list[i], opts->print_item_data);
+
+		if ((opts->columns) && ((i + 1) % (opts->columns))) {
+			putchar('\t');
+			last_lf = 0;
+		}
+		else {
+			putchar('\n');
+			last_lf = 1;
+		}
+	}
+
+	if (!last_lf)
 		putchar('\n');
+}
+struct list_and_choose_options {
+	struct list_options list_opts;
+
+	const char *prompt;
+};
+
+#define LIST_AND_CHOOSE_ERROR (-1)
+#define LIST_AND_CHOOSE_QUIT  (-2)
+
+/*
+ * Returns the selected index.
+ *
+ * If an error occurred, returns `LIST_AND_CHOOSE_ERROR`. Upon EOF,
+ * `LIST_AND_CHOOSE_QUIT` is returned.
+ */
+static ssize_t list_and_choose(struct item **items, size_t nr,
+			       struct add_i_state *s,
+			       struct list_and_choose_options *opts)
+{
+	struct strbuf input = STRBUF_INIT;
+	ssize_t res = LIST_AND_CHOOSE_ERROR;
+
+	for (;;) {
+		char *p, *endp;
+
+		strbuf_reset(&input);
+
+		list(items, nr, s, &opts->list_opts);
+
+		printf("%s%s", opts->prompt, "> ");
+		fflush(stdout);
+
+		if (strbuf_getline(&input, stdin) == EOF) {
+			putchar('\n');
+			res = LIST_AND_CHOOSE_QUIT;
+			break;
+		}
+		strbuf_trim(&input);
+
+		if (!input.len)
+			break;
+
+		p = input.buf;
+		for (;;) {
+			size_t sep = strcspn(p, " \t\r\n,");
+			ssize_t index = -1;
+
+			if (!sep) {
+				if (!*p)
+					break;
+				p++;
+				continue;
+			}
+
+			if (isdigit(*p)) {
+				index = strtoul(p, &endp, 10) - 1;
+				if (endp != p + sep)
+					index = -1;
+			}
+
+			p[sep] = '\0';
+			if (index < 0 || index >= nr)
+				printf(_("Huh (%s)?\n"), p);
+			else {
+				res = index;
+				break;
+			}
+
+			p += sep + 1;
+		}
+
+		if (res != LIST_AND_CHOOSE_ERROR)
+			break;
 	}
+
+	strbuf_release(&input);
+	return res;
 }
 
 struct adddel {
@@ -285,17 +375,40 @@ static int run_status(struct add_i_state *s, const struct pathspec *ps,
 	return 0;
 }
 
+static void print_command_item(int i, struct item *item,
+			       void *print_command_item_data)
+{
+	printf(" %2d: %s", i + 1, item->name);
+}
+
+struct command_item {
+	struct item item;
+	int (*command)(struct add_i_state *s, const struct pathspec *ps,
+		       struct file_list *files, struct list_options *opts);
+};
+
 int run_add_i(struct repository *r, const struct pathspec *ps)
 {
 	struct add_i_state s = { NULL };
+	struct list_and_choose_options main_loop_opts = {
+		{ 4, N_("*** Commands ***"), print_command_item, NULL },
+		N_("What now")
+	};
+	struct command_item
+		status = { { "status" }, run_status };
+	struct command_item *commands[] = {
+		&status
+	};
+
 	struct print_file_item_data print_file_item_data = {
 		"%12s %12s %s", STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
 	};
 	struct list_options opts = {
-		NULL, print_file_item, &print_file_item_data
+		0, NULL, print_file_item, &print_file_item_data
 	};
 	struct strbuf header = STRBUF_INIT;
 	struct file_list files = { NULL };
+	ssize_t i;
 	int res = 0;
 
 	if (init_add_i_state(r, &s))
@@ -310,6 +423,18 @@ int run_add_i(struct repository *r, const struct pathspec *ps)
 	if (run_status(&s, ps, &files, &opts) < 0)
 		res = -1;
 
+	for (;;) {
+		i = list_and_choose((struct item **)commands,
+				    ARRAY_SIZE(commands), &s, &main_loop_opts);
+		if (i == LIST_AND_CHOOSE_QUIT) {
+			printf(_("Bye.\n"));
+			res = 0;
+			break;
+		}
+		if (i != LIST_AND_CHOOSE_ERROR)
+			res = commands[i]->command(&s, ps, &files, &opts);
+	}
+
 	release_file_list(&files);
 	strbuf_release(&print_file_item_data.buf);
 	strbuf_release(&print_file_item_data.index);
Signature · 1 line
gitgitgadget
#28 lore raw

PATCH v2 7/11 Add a function to determine unique prefixes for a list of strings

In reply to Johannes Schindelin via GitGitGadget

From: Slavica Djukic <[email protected]>

In the git add -i command, we show unique prefixes of the commands and files, to give an indication what prefix would select them.

Naturally, the C implementation looks a lot different than the Perl
implementation: in Perl, a trie is much easier implemented, while we already have a pretty neat hashmap implementation in C that we use for the purpose of storing (not necessarily unique) prefixes.

The idea: for each item that we add, we generate prefixes starting with the first letter, then the first two letters, then three, etc, until we find a prefix that is unique (or until the prefix length would be longer than we want). If we encounter a previously-unique prefix on the way, we adjust that item's prefix to make it unique again (or we mark it as having no unique prefix if we failed to find one). These partial prefixes are stored in a hash map (for quick lookup times).

To make sure that this function works as expected, we add a test using a special-purpose test helper that was added for that purpose.

Note: We expect the list of prefix items to be passed in as a list of pointers rather than as regular list to avoid having to copy information (the actual items will most likely contain more information than just the name and the length of the unique prefix, but passing in struct prefix_item * would not allow for that).

---
 Makefile                   |   2 +
 prefix-map.c               | 109 +++++++++++++++++++++++++++++++++++++
 prefix-map.h               |  40 ++++++++++++++
 t/helper/test-prefix-map.c |  58 ++++++++++++++++++++
 t/helper/test-tool.c       |   1 +
 t/helper/test-tool.h       |   1 +
 t/t0016-prefix-map.sh      |  10 ++++
 7 files changed, 221 insertions(+)
 create mode 100644 prefix-map.c
 create mode 100644 prefix-map.h
 create mode 100644 t/helper/test-prefix-map.c
 create mode 100755 t/t0016-prefix-map.sh
Makefile +2 -0
index 18e656a32f..8299b3f17d 100644
@@ -754,6 +754,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
@@ -967,6 +968,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
prefix-map.c new file +109 -0

109 lines not on this page · raw message

prefix-map.h new file +40 -0
new file mode 100644
index 0000000000..ce3b8a4a32
@@ -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
t/helper/test-prefix-map.c new file +58 -0

58 lines not on this page · raw message

t/helper/test-tool.c +1 -0
index 99db7409b8..d6a92a8699 100644
@@ -32,6 +32,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 },
t/helper/test-tool.h +1 -0
index 25abed1cf2..33a089ee4e 100644
@@ -29,6 +29,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);
t/t0016-prefix-map.sh new file +10 -0
new file mode 100755
index 0000000000..187fa92aec
@@ -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
Signature · 1 line
gitgitgadget
#29 lore raw

PATCH v2 8/11 built-in add -i: show unique prefixes of the commands

In reply to Johannes Schindelin via GitGitGadget

From: Slavica Djukic <[email protected]>

Just like in the Perl script git-add--interactive.perl, for each command a unique prefix is determined (if there exists any within the given parameters), and shown in the list, and accepted as a shortcut for the command.

We use the prefix map implementation that we just added in the previous commit for that purpose.

---
 add-interactive.c | 69 ++++++++++++++++++++++++++++++++++++++---------
 1 file changed, 56 insertions(+), 13 deletions(-)
add-interactive.c +56 -13
index 0ea4f3edb8..7ff87bae1b 100644
@@ -5,6 +5,7 @@
 #include "diffcore.h"
 #include "revision.h"
 #include "refs.h"
+#include "prefix-map.h"
 
 struct add_i_state {
 	struct repository *r;
@@ -46,18 +47,32 @@ static int init_add_i_state(struct repository *r, struct add_i_state *s)
 	return 0;
 }
 
-struct item {
-	const char *name;
-};
+static ssize_t find_unique(const char *string,
+			   struct prefix_item **list, size_t nr)
+{
+	ssize_t found = -1, i;
+
+	for (i = 0; i < nr; i++) {
+		struct prefix_item *item = list[i];
+		if (!starts_with(item->name, string))
+			continue;
+		if (found >= 0)
+			return -1;
+		found = i;
+	}
+
+	return found;
+}
 
 struct list_options {
 	int columns;
 	const char *header;
-	void (*print_item)(int i, struct item *item, void *print_item_data);
+	void (*print_item)(int i, struct prefix_item *item,
+			   void *print_item_data);
 	void *print_item_data;
 };
 
-static void list(struct item **list, size_t nr,
+static void list(struct prefix_item **list, size_t nr,
 		 struct add_i_state *s, struct list_options *opts)
 {
 	int i, last_lf = 0;
@@ -100,13 +115,15 @@ struct list_and_choose_options {
  * If an error occurred, returns `LIST_AND_CHOOSE_ERROR`. Upon EOF,
  * `LIST_AND_CHOOSE_QUIT` is returned.
  */
-static ssize_t list_and_choose(struct item **items, size_t nr,
+static ssize_t list_and_choose(struct prefix_item **items, size_t nr,
 			       struct add_i_state *s,
 			       struct list_and_choose_options *opts)
 {
 	struct strbuf input = STRBUF_INIT;
 	ssize_t res = LIST_AND_CHOOSE_ERROR;
 
+	find_unique_prefixes(items, nr, 1, 4);
+
 	for (;;) {
 		char *p, *endp;
 
@@ -146,6 +163,9 @@ static ssize_t list_and_choose(struct item **items, size_t nr,
 			}
 
 			p[sep] = '\0';
+			if (index < 0)
+				index = find_unique(p, items, nr);
+
 			if (index < 0 || index >= nr)
 				printf(_("Huh (%s)?\n"), p);
 			else {
@@ -171,7 +191,7 @@ struct adddel {
 
 struct file_list {
 	struct file_item {
-		struct item item;
+		struct prefix_item item;
 		struct adddel index, worktree;
 	} **file;
 	size_t nr, alloc;
@@ -337,12 +357,29 @@ static void populate_wi_changes(struct strbuf *buf,
 		strbuf_addstr(buf, no_changes);
 }
 
+/* filters out prefixes which have special meaning to list_and_choose() */
+static int is_valid_prefix(const char *prefix, size_t prefix_len)
+{
+	return prefix_len && prefix &&
+		/*
+		 * We expect `prefix` to be NUL terminated, therefore this
+		 * `strcspn()` call is okay, even if it might do much more
+		 * work than strictly necessary.
+		 */
+		strcspn(prefix, " \t\r\n,") >= prefix_len &&	/* separators */
+		*prefix != '-' &&				/* deselection */
+		!isdigit(*prefix) &&				/* selection */
+		(prefix_len != 1 ||
+		 (*prefix != '*' &&				/* "all" wildcard */
+		  *prefix != '?'));				/* prompt help */
+}
+
 struct print_file_item_data {
 	const char *modified_fmt;
 	struct strbuf buf, index, worktree;
 };
 
-static void print_file_item(int i, struct item *item,
+static void print_file_item(int i, struct prefix_item *item,
 			    void *print_file_item_data)
 {
 	struct file_item *c = (struct file_item *)item;
@@ -369,20 +406,26 @@ static int run_status(struct add_i_state *s, const struct pathspec *ps,
 		return -1;
 
 	if (files->nr)
-		list((struct item **)files->file, files->nr, s, opts);
+		list((struct prefix_item **)files->file, files->nr, s, opts);
 	putchar('\n');
 
 	return 0;
 }
 
-static void print_command_item(int i, struct item *item,
+static void print_command_item(int i, struct prefix_item *item,
 			       void *print_command_item_data)
 {
-	printf(" %2d: %s", i + 1, item->name);
+	if (!item->prefix_length ||
+	    !is_valid_prefix(item->name, item->prefix_length))
+		printf(" %2d: %s", i + 1, item->name);
+	else
+		printf(" %3d: [%.*s]%s", i + 1,
+		       (int)item->prefix_length, item->name,
+		       item->name + item->prefix_length);
 }
 
 struct command_item {
-	struct item item;
+	struct prefix_item item;
 	int (*command)(struct add_i_state *s, const struct pathspec *ps,
 		       struct file_list *files, struct list_options *opts);
 };
@@ -424,7 +467,7 @@ int run_add_i(struct repository *r, const struct pathspec *ps)
 		res = -1;
 
 	for (;;) {
-		i = list_and_choose((struct item **)commands,
+		i = list_and_choose((struct prefix_item **)commands,
 				    ARRAY_SIZE(commands), &s, &main_loop_opts);
 		if (i == LIST_AND_CHOOSE_QUIT) {
 			printf(_("Bye.\n"));
Signature · 1 line
gitgitgadget
#30 lore raw

PATCH v2 9/11 built-in add -i: support `?` (prompt help)

In reply to Johannes Schindelin via GitGitGadget

From: Johannes Schindelin <[email protected]>

With this change, we print out the same colored help text that the Perl-based git add -i prints in the main loop when question mark is entered.

---
 add-interactive.c | 22 +++++++++++++++++++++-
 1 file changed, 21 insertions(+), 1 deletion(-)
add-interactive.c +21 -1
index 7ff87bae1b..1435c12be9 100644
@@ -11,6 +11,7 @@ struct add_i_state {
 	struct repository *r;
 	int use_color;
 	char header_color[COLOR_MAXLEN];
+	char help_color[COLOR_MAXLEN];
 };
 
 static void init_color(struct repository *r, struct add_i_state *s,
@@ -43,6 +44,7 @@ static int init_add_i_state(struct repository *r, struct add_i_state *s)
 	s->use_color = want_color(s->use_color);
 
 	init_color(r, s, "header", s->header_color, GIT_COLOR_BOLD);
+	init_color(r, s, "help", s->help_color, GIT_COLOR_BOLD_RED);
 
 	return 0;
 }
@@ -104,6 +106,7 @@ struct list_and_choose_options {
 	struct list_options list_opts;
 
 	const char *prompt;
+	void (*print_help)(struct add_i_state *s);
 };
 
 #define LIST_AND_CHOOSE_ERROR (-1)
@@ -144,6 +147,11 @@ static ssize_t list_and_choose(struct prefix_item **items, size_t nr,
 		if (!input.len)
 			break;
 
+		if (!strcmp(input.buf, "?")) {
+			opts->print_help(s);
+			continue;
+		}
+
 		p = input.buf;
 		for (;;) {
 			size_t sep = strcspn(p, " \t\r\n,");
@@ -430,12 +438,24 @@ struct command_item {
 		       struct file_list *files, struct list_options *opts);
 };
 
+static void command_prompt_help(struct add_i_state *s)
+{
+	const char *help_color = s->help_color;
+	color_fprintf_ln(stdout, help_color, "%s", _("Prompt help:"));
+	color_fprintf_ln(stdout, help_color, "1          - %s",
+			 _("select a numbered item"));
+	color_fprintf_ln(stdout, help_color, "foo        - %s",
+			 _("select item based on unique prefix"));
+	color_fprintf_ln(stdout, help_color, "           - %s",
+			 _("(empty) select nothing"));
+}
+
 int run_add_i(struct repository *r, const struct pathspec *ps)
 {
 	struct add_i_state s = { NULL };
 	struct list_and_choose_options main_loop_opts = {
 		{ 4, N_("*** Commands ***"), print_command_item, NULL },
-		N_("What now")
+		N_("What now"), command_prompt_help
 	};
 	struct command_item
 		status = { { "status" }, run_status };
Signature · 1 line
gitgitgadget
#31 lore raw

PATCH v2 10/11 built-in add -i: use color in the main loop

In reply to Johannes Schindelin via GitGitGadget

From: Slavica Djukic <[email protected]>

The error messages as well as the unique prefixes are colored in git add -i by default; We need to do the same in the built-in version.

---
 add-interactive.c | 37 ++++++++++++++++++++++++++++++++-----
 1 file changed, 32 insertions(+), 5 deletions(-)
add-interactive.c +32 -5
index 1435c12be9..538658dfa7 100644
@@ -12,6 +12,9 @@ struct add_i_state {
 	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];
 };
 
 static void init_color(struct repository *r, struct add_i_state *s,
@@ -45,6 +48,9 @@ static int init_add_i_state(struct repository *r, struct add_i_state *s)
 
 	init_color(r, s, "header", s->header_color, GIT_COLOR_BOLD);
 	init_color(r, s, "help", s->help_color, GIT_COLOR_BOLD_RED);
+	init_color(r, s, "prompt", s->prompt_color, GIT_COLOR_BOLD_BLUE);
+	init_color(r, s, "error", s->error_color, GIT_COLOR_BOLD_RED);
+	init_color(r, s, "reset", s->reset_color, GIT_COLOR_RESET);
 
 	return 0;
 }
@@ -134,7 +140,8 @@ static ssize_t list_and_choose(struct prefix_item **items, size_t nr,
 
 		list(items, nr, s, &opts->list_opts);
 
-		printf("%s%s", opts->prompt, "> ");
+		color_fprintf(stdout, s->prompt_color, "%s", opts->prompt);
+		fputs("> ", stdout);
 		fflush(stdout);
 
 		if (strbuf_getline(&input, stdin) == EOF) {
@@ -175,7 +182,8 @@ static ssize_t list_and_choose(struct prefix_item **items, size_t nr,
 				index = find_unique(p, items, nr);
 
 			if (index < 0 || index >= nr)
-				printf(_("Huh (%s)?\n"), p);
+				color_fprintf_ln(stdout, s->error_color,
+						 _("Huh (%s)?"), p);
 			else {
 				res = index;
 				break;
@@ -420,15 +428,21 @@ static int run_status(struct add_i_state *s, const struct pathspec *ps,
 	return 0;
 }
 
+struct print_command_item_data {
+	const char *color, *reset;
+};
+
 static void print_command_item(int i, struct prefix_item *item,
 			       void *print_command_item_data)
 {
+	struct print_command_item_data *d = print_command_item_data;
+
 	if (!item->prefix_length ||
 	    !is_valid_prefix(item->name, item->prefix_length))
 		printf(" %2d: %s", i + 1, item->name);
 	else
-		printf(" %3d: [%.*s]%s", i + 1,
-		       (int)item->prefix_length, item->name,
+		printf(" %2d: %s%.*s%s%s", i + 1,
+		       d->color, (int)item->prefix_length, item->name, d->reset,
 		       item->name + item->prefix_length);
 }
 
@@ -453,8 +467,9 @@ static void command_prompt_help(struct add_i_state *s)
 int run_add_i(struct repository *r, const struct pathspec *ps)
 {
 	struct add_i_state s = { NULL };
+	struct print_command_item_data data;
 	struct list_and_choose_options main_loop_opts = {
-		{ 4, N_("*** Commands ***"), print_command_item, NULL },
+		{ 4, N_("*** Commands ***"), print_command_item, &data },
 		N_("What now"), command_prompt_help
 	};
 	struct command_item
@@ -477,6 +492,18 @@ int run_add_i(struct repository *r, const struct pathspec *ps)
 	if (init_add_i_state(r, &s))
 		return error("could not parse `add -i` config");
 
+	/*
+	 * When color was asked for, use the prompt color for
+	 * highlighting, otherwise use square brackets.
+	 */
+	if (s.use_color) {
+		data.color = s.prompt_color;
+		data.reset = s.reset_color;
+	} else {
+		data.color = "[";
+		data.reset = "]";
+	}
+
 	strbuf_addstr(&header, "      ");
 	strbuf_addf(&header, print_file_item_data.modified_fmt,
 		    _("staged"), _("unstaged"), _("path"));
Signature · 1 line
gitgitgadget
#32 lore raw

PATCH v2 11/11 built-in add -i: implement the `help` command

In reply to Johannes Schindelin via GitGitGadget

From: Johannes Schindelin <[email protected]>

This imitates the code to show the help text from the Perl script git-add--interactive.perl in the built-in version.

To make sure that it renders exactly like the Perl version of git add -i, we also add a test case for that to t3701-add-interactive.sh.

---
 add-interactive.c          | 27 +++++++++++++++++++++++++--
 t/t3701-add-interactive.sh | 24 ++++++++++++++++++++++++
 2 files changed, 49 insertions(+), 2 deletions(-)
add-interactive.c +25 -2
index 538658dfa7..c431c72e3f 100644
@@ -428,6 +428,27 @@ static int run_status(struct add_i_state *s, const struct pathspec *ps,
 	return 0;
 }
 
+static int run_help(struct add_i_state *s, const struct pathspec *ps,
+		    struct file_list *files, struct list_options *opts)
+{
+	const char *help_color = s->help_color;
+
+	color_fprintf_ln(stdout, help_color, "status        - %s",
+			 _("show paths with changes"));
+	color_fprintf_ln(stdout, help_color, "update        - %s",
+			 _("add working tree state to the staged set of changes"));
+	color_fprintf_ln(stdout, help_color, "revert        - %s",
+			 _("revert staged set of changes back to the HEAD version"));
+	color_fprintf_ln(stdout, help_color, "patch         - %s",
+			 _("pick hunks and update selectively"));
+	color_fprintf_ln(stdout, help_color, "diff          - %s",
+			 _("view diff between HEAD and index"));
+	color_fprintf_ln(stdout, help_color, "add untracked - %s",
+			 _("add contents of untracked files to the staged set of changes"));
+
+	return 0;
+}
+
 struct print_command_item_data {
 	const char *color, *reset;
 };
@@ -473,9 +494,11 @@ int run_add_i(struct repository *r, const struct pathspec *ps)
 		N_("What now"), command_prompt_help
 	};
 	struct command_item
-		status = { { "status" }, run_status };
+		status = { { "status" }, run_status },
+		help = { { "help" }, run_help };
 	struct command_item *commands[] = {
-		&status
+		&status,
+		&help
 	};
 
 	struct print_file_item_data print_file_item_data = {
t/t3701-add-interactive.sh +24 -0
index 65dfbc033a..91aaef2932 100755
@@ -639,4 +639,28 @@ test_expect_success 'add -p patch editing works with pathological context lines'
 	test_cmp expected-2 actual
 '
 
+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 | GIT_PAGER_IN_USE=true TERM=vt100 git add -i >actual.colored &&
+	test_decode_color <actual.colored >actual &&
+	test_i18ncmp expect actual
+'
+
 test_done
Signature · 1 line
gitgitgadget
#33 lore raw

v3 cover git add -i: add a rudimentary version in C (supporting only status and help so far)

In reply to Johannes Schindelin via GitGitGadget

This is the first leg on the long journey to a fully built-in git add -i (next up: parts 2 [https://github.com/gitgitgadget/git/pull/171], 3
[https://github.com/gitgitgadget/git/pull/172], 4
[https://github.com/gitgitgadget/git/pull/173], 5
[https://github.com/gitgitgadget/git/pull/174], and 6
[https://github.com/gitgitgadget/git/pull/175]). Note: the latter PRs are not necessarily up to date, and will be re-targeted to the appropriate branches in https://github.com/gitster/git as soon as Junio picks them up.

This here patch series reflects the part that was submitted a couple of times (see https://github.com/gitgitgadget/git/pull/103) during the Outreachy project by Slavica Ðukic that continued the journey based on an initial patch series by Daniel Ferreira.

It only implements the status and the help part, in the interest of making the review remotely more reviewable.

As I am a heavy user of git add -p myself and use a patched version for weeks already (it is so nice to not suffer over one second startup until the MSYS2 Perl finally shows me anything, instead it feels instantaneous), I integrated these patch series into Git for Windows' master already, as an opt-in feature guarded by the config variable add.interactive.useBuiltin (and Git for Windows' installer is prepared to detect this version and offer the option in the graphical user interface).

I had planned on submitting this before v2.22.0-rc0, but there was such a backlog of builds from a big pushout that I had to wait ;-)

Changes since v2:

* Rebased to master to avoid merge conflicts.
* Renumbered the prefix-map test to avoid conflicts with two patch series that are currently in-flight in pu.

Changes since v1:

* The config machinery was reworked completely, to not use a callback to git_config(), but instead to query the config via the repo_config_get_*() functions. This also prevents a future "Huh???" moment: the internal add --interactive API accepts a parameter of type struct repository *r, but the previous configuration did not use that to query the config (and could in the future be a repository other than the_repository).

* As a consequence, the color sequences are no longer stored in file-local variables, but passed around via a struct.

* Instead of using the magical constant -2 to quit the main loop, it is now defined as LIST_AND_CHOOSE_QUIT (and likewise, LIST_AND_CHOOSE_ERROR is defined as -1 and used where appropriate).

* Improved the add_prefix_item() function by avoiding buffer overruns, not reusing the struct that is used for lookup also for adding the new item, and by strengthening the bug check.

Daniel Ferreira (2):
diff: export diffstat interface
built-in add -i: implement the status command

Johannes Schindelin (6):
Start to implement a built-in version of git add --interactive built-in add -i: refresh the index before running status built-in add -i: color the header in the status command built-in add -i: implement the main loop
built-in add -i: support ? (prompt help)
built-in add -i: implement the help command

Slavica Djukic (3):
Add a function to determine unique prefixes for a list of strings built-in add -i: show unique prefixes of the commands built-in add -i: use color in the main loop

 Documentation/config/add.txt |   5 +
 Makefile                     |   3 +
 add-interactive.c            | 558 +++++++++++++++++++++++++++++++++++
 add-interactive.h            |   8 +
 builtin/add.c                |  10 +
 diff.c                       |  37 +--
 diff.h                       |  19 ++
 prefix-map.c                 | 109 +++++++
 prefix-map.h                 |  40 +++
 repository.c                 |  19 ++
 repository.h                 |   7 +
 t/README                     |   4 +
 t/helper/test-prefix-map.c   |  58 ++++
 t/helper/test-tool.c         |   1 +
 t/helper/test-tool.h         |   1 +
 t/t0018-prefix-map.sh        |  10 +
 t/t3701-add-interactive.sh   |  25 ++
 17 files changed, 892 insertions(+), 22 deletions(-)
 create mode 100644 add-interactive.c
 create mode 100644 add-interactive.h
 create mode 100644 prefix-map.c
 create mode 100644 prefix-map.h
 create mode 100644 t/helper/test-prefix-map.c
 create mode 100755 t/t0018-prefix-map.sh

base-commit: 9d418600f4d10dcbbfb0b5fdbc71d509e03ba719
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-170%2Fdscho%2Fadd-i-in-c-status-and-help-v3
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-170/dscho/add-i-in-c-status-and-help-v3
Pull-Request: https://github.com/gitgitgadget/git/pull/170

Range-diff vs v2:

1: ed53346b92 ! 1: 0a5ec9345d Start to implement a built-in version of git add --interactive

     @@ -47,7 +47,7 @@
       --- a/Makefile
       +++ b/Makefile
      @@
     - 	-name '*.h' -print)
     + 	-name '*.h' -print))
       
       LIB_OBJS += abspath.o
      +LIB_OBJS += add-interactive.o
     @@ -121,7 +121,7 @@
       --- a/t/README
       +++ b/t/README
      @@
     - builtin version of git-rebase. See 'rebase.useBuiltin' in
     + built-in version of git-stash. See 'stash.useBuiltin' in
       git-config(1).
       
      +GIT_TEST_ADD_I_USE_BUILTIN=<boolean>, when true, enables the
  2:  bc99009fbf =  2:  c7a377890d diff: export diffstat interface
  3:  5e23c0756b =  3:  b93b055ebe built-in add -i: implement the `status` command
  4:  8cafc6ae8d =  4:  daff24074a built-in add -i: refresh the index before running `status`
  5:  83d92a9762 =  5:  15f18f5b3e built-in add -i: color the header in the `status` command
  6:  3eec219124 =  6:  175409aaae built-in add -i: implement the main loop
  7:  e02a52c3ac !  7:  3000d7d08d Add a function to determine unique prefixes for a list of strings
     @@ -299,10 +299,10 @@
       int cmd__reach(int argc, const char **argv);
       int cmd__read_cache(int argc, const char **argv);
      
     - diff --git a/t/t0016-prefix-map.sh b/t/t0016-prefix-map.sh
     + diff --git a/t/t0018-prefix-map.sh b/t/t0018-prefix-map.sh
       new file mode 100755
       --- /dev/null
     - +++ b/t/t0016-prefix-map.sh
     + +++ b/t/t0018-prefix-map.sh
      @@
      +#!/bin/sh
      +
  8:  ced9b6aced =  8:  e23ddebfbf built-in add -i: show unique prefixes of the commands
  9:  7378af60ad =  9:  d8c012fce8 built-in add -i: support `?` (prompt help)
 10:  36edef85e6 = 10:  8121a3ca1b built-in add -i: use color in the main loop
 11:  266dbf2a6b ! 11:  db70c6475d built-in add -i: implement the `help` command
     @@ -61,9 +61,10 @@
       --- a/t/t3701-add-interactive.sh
       +++ b/t/t3701-add-interactive.sh
      @@
     - 	test_cmp expected-2 actual
     + 	test_write_lines a b a b a a b a b a >expect &&
     + 	test_cmp expect a
       '
     - 
     ++
      +test_expect_success 'show help from add--helper' '
      +	git reset --hard &&
      +	cat >expect <<-EOF &&
Signature · 1 line
gitgitgadget
#34 lore raw

PATCH v3 1/11 Start to implement a built-in version of `git add --interactive`

From: Johannes Schindelin <[email protected]>

This is hardly the first conversion of a Git command that is implemented as a script to a built-in. So far, the most successful strategy for such conversions has been to add a built-in helper and call that for more and more functionality from the script, as more and more parts are converted.

With the interactive add, we choose a different strategy. The sole reason for this is that on Windows (where such a conversion has the most benefits in terms of speed and robustness) we face the very specific problem that a system() call in Perl seems to close stdin in the parent process when the spawned process consumes even one character from stdin. And that just does not work for us here, as it would stop the main loop as soon as any interactive command was performed by the helper. Which is almost all of the commands in git add -i.

It is almost as if Perl told us once again that it does not want us to use it on Windows.

Instead, we follow the opposite route where we start with a bare-bones version of the built-in interactive add, guarded by the new add.interactive.useBuiltin config variable, and then add more and more functionality to it, until it is feature complete.

At this point, the built-in version of git add -i only states that it cannot do anything yet ;-)

---
 Documentation/config/add.txt |  5 +++++
 Makefile                     |  1 +
 add-interactive.c            |  7 +++++++
 add-interactive.h            |  8 ++++++++
 builtin/add.c                | 10 ++++++++++
 t/README                     |  4 ++++
 6 files changed, 35 insertions(+)
 create mode 100644 add-interactive.c
 create mode 100644 add-interactive.h
Documentation/config/add.txt +5 -0
index 4d753f006e..c9f748f81c 100644
@@ -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.
Makefile +1 -0
index 98a0588416..0a47200f47 100644
@@ -824,6 +824,7 @@ LIB_H := $(sort $(shell git ls-files '*.h' ':!t/' ':!Documentation/' 2>/dev/null
 	-name '*.h' -print))
 
 LIB_OBJS += abspath.o
+LIB_OBJS += add-interactive.o
 LIB_OBJS += advice.o
 LIB_OBJS += alias.o
 LIB_OBJS += alloc.o
add-interactive.c new file +7 -0
new file mode 100644
index 0000000000..482e458dc6
@@ -0,0 +1,7 @@
+#include "cache.h"
+#include "add-interactive.h"
+
+int run_add_i(struct repository *r, const struct pathspec *ps)
+{
+	die(_("No commands are available in the built-in `git add -i` yet!"));
+}
add-interactive.h new file +8 -0
new file mode 100644
index 0000000000..7043b8741d
@@ -0,0 +1,8 @@
+#ifndef ADD_INTERACTIVE_H
+#define ADD_INTERACTIVE_H
+
+struct repository;
+struct pathspec;
+int run_add_i(struct repository *r, const struct pathspec *ps);
+
+#endif
builtin/add.c +10 -0
index dd18e5c9b6..4f625691b5 100644
@@ -20,6 +20,7 @@
 #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>..."),
@@ -185,6 +186,14 @@ int run_add_interactive(const char *revision, const char *patch_mode,
 {
 	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 && !patch_mode)
+		return !!run_add_i(the_repository, pathspec);
 
 	argv_array_push(&argv, "add--interactive");
 	if (patch_mode)
@@ -319,6 +328,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);
 }
 
t/README +4 -0
index 9747971d58..28999cebd3 100644
@@ -397,6 +397,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
Signature · 1 line
gitgitgadget
#35 lore raw

Re: [PATCH v3 01/11] Start to implement a built-in version of `git add --interactive`

"Johannes Schindelin via GitGitGadget" <[email protected]>

Quoted 1 line · writes:

+add.interactive.useBuiltin::

I am not sure if three-level name is a good thing to use here.

If we have end-user controllable (like branch or remote names) unbounded number of subcommand/submode to "add", and "interactive" is merely one of it, then three-level name is a perfect fit, but otherwise, not.

builtin/add.c int run_add_interactive(const char *revision, const char *patch_mode, · line 196

Quoted 12 lines
builtin/add.c +8 -0
@@ -185,6 +186,14 @@ int run_add_interactive(const char *revision, const char *patch_mode,
 {
 	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 && !patch_mode)
+		return !!run_add_i(the_repository, pathspec);

I am hoping that eventually "add -p" will also be routed to the new codepath. Would it make sense to have "&& !patch_mode" here, especially at this step where run_add_i() won't do anything useful anyway yet?

builtin/add.c static int add_config(const char *var, const char *value, void *cb) · line 333

Quoted 7 lines
builtin/add.c +1 -0
@@ -319,6 +328,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);
 }

Good addition while at it.

#36 lore raw

Re: [PATCH v3 01/11] Start to implement a built-in version of `git add --interactive`

Hi Junio,

Quoted 11 lines · Junio C Hamano

"Johannes Schindelin via GitGitGadget" <[email protected]>

Quoted 1 line · writes:

+add.interactive.useBuiltin::

I am not sure if three-level name is a good thing to use here.

If we have end-user controllable (like branch or remote names) unbounded number of subcommand/submode to "add", and "interactive" is merely one of it, then three-level name is a perfect fit, but otherwise, not.

Well, my thinking was that add.useBuiltin would be misleading (because the non-interactive part of git add is _already_ built-in, even git add -e is built-in). And addInteractive.useBuiltin, to me, would pretend that add-interactive is the name of the command.

Besides, I really hope that this would be only temporary, as I already have a fully-built-in git add -i and git add -p in Git for Windows, as an experimental opt-in, and so far it looks like it could replace the scripted version relatively soon, so maybe that particular part is not worth all that much worry ;-)

Quoted 9 lines
Quoted 9 lines
builtin/add.c +5 -0
@@ -185,6 +186,14 @@ int run_add_interactive(const char *revision, const char *patch_mode,
 {
 	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);
Quoted 8 lines

builtin/add.c int run_add_interactive(const char *revision, const char *patch_mode, · line 196

Quoted 3 lines
builtin/add.c +3 -0
+
+	if (use_builtin_add_i == 1 && !patch_mode)
+		return !!run_add_i(the_repository, pathspec);

I am hoping that eventually "add -p" will also be routed to the new codepath. Would it make sense to have "&& !patch_mode" here, especially at this step where run_add_i() won't do anything useful anyway yet?

The && !patch_mode is here to allow for a gradual adoption of the built-in parts. I don't want users who opted in to using the built-in git add -i to be stopped from using git add -p, so I don't want to print even a warning, let alone an error message, when the patch mode needs to run under add.interactive.useBuiltin = true, even if that part is still scripted-only.

Of course, eventually this will be handled. See
https://github.com/gitgitgadget/git/pull/173 for the yet-to-be-contributed patch series.

I just don't want to send a multi-dozen patch series. I really don't think there is any effective way to review such a long patch series, let alone an efficient way to develop it incrementally based on feedback on the mailing list, hance I broke things up into 6 separate patch series (as indicated by the cover letter), and this one is the first of them.

Quoted 9 lines

builtin/add.c static int add_config(const char *var, const char *value, void *cb) · line 333

Quoted 7 lines
builtin/add.c +1 -0
@@ -319,6 +328,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);
 }

Good addition while at it.

:-)

This was actually an oversight, sorry... But since you're in favor ;-)

Ciao,
Dscho

#37 lore raw

Re: [PATCH v3 01/11] Start to implement a built-in version of `git add --interactive`

Quoted 1 line · Johannes Schindelin

Besides, I really hope that this would be only temporary,...

Oh, no question about it. This should be temporary knob.

I still do worry about giving a bad example for others to copy. People tend to copy & paste without thinking. Either we come up with and use a two-level name, or we add a comment to explain to developers (not users---as this is merely a temporary thing) why they should never follow suit using three-level names for things like this one written in big red letters, or something, then perhaps we won't have to worry about too much? I dunno.

Quoted 10 lines
Quoted 7 lines

builtin/add.c int run_add_interactive(const char *revision, const char *patch_mode, · line 196

Quoted 2 lines
builtin/add.c +2 -0
+	if (use_builtin_add_i == 1 && !patch_mode)
+		return !!run_add_i(the_repository, pathspec);

I am hoping that eventually "add -p" will also be routed to the new codepath. Would it make sense to have "&& !patch_mode" here, especially at this step where run_add_i() won't do anything useful anyway yet?

The && !patch_mode is here to allow for a gradual adoption of the built-in parts. ...

Ah, so "add.usebuiltin = interactive patch" can (eventually) choose to use the C code for both while "add.usebuiltin = interactive" would not use it for the patch mode, or something? Or even

add.interactive.usebuiltin = yes
add.patch.usebuiltin = no

perhaps?

Quoted 1 line

Of course, eventually this will be handled.

Yup, again, the knob is merely temporary.

#38 lore raw

Re: [PATCH v3 01/11] Start to implement a built-in version of `git add --interactive`

Hi Junio,

Quoted 22 lines · Junio C Hamano
Quoted 1 line · Johannes Schindelin

Besides, I really hope that this would be only temporary,...

Oh, no question about it. This should be temporary knob.

I still do worry about giving a bad example for others to copy. People tend to copy & paste without thinking. Either we come up with and use a two-level name, or we add a comment to explain to developers (not users---as this is merely a temporary thing) why they should never follow suit using three-level names for things like this one written in big red letters, or something, then perhaps we won't have to worry about too much? I dunno.

Quoted 8 lines
Quoted 7 lines

builtin/add.c int run_add_interactive(const char *revision, const char *patch_mode, · line 196

Quoted 2 lines
builtin/add.c +2 -0
+	if (use_builtin_add_i == 1 && !patch_mode)
+		return !!run_add_i(the_repository, pathspec);

I am hoping that eventually "add -p" will also be routed to the new codepath. Would it make sense to have "&& !patch_mode" here, especially at this step where run_add_i() won't do anything useful anyway yet?

Quoted 8 lines
Quoted 2 lines

The && !patch_mode is here to allow for a gradual adoption of the built-in parts. ...

Ah, so "add.usebuiltin = interactive patch" can (eventually) choose to use the C code for both while "add.usebuiltin = interactive" would not use it for the patch mode, or something? Or even

add.interactive.usebuiltin = yes

This is what I had in mind.

Quoted 1 line

add.patch.usebuiltin = no

And this is what I should have done ;-)

But maybe I should do

add.useBuiltin = wheneverPossible

?

Ciao,
Dscho

Quoted 6 lines

perhaps?

Quoted 1 line

Of course, eventually this will be handled.

Yup, again, the knob is merely temporary.

#39 lore raw

Re: [PATCH v3 01/11] Start to implement a built-in version of `git add --interactive`

Quoted 8 lines · Johannes Schindelin
Quoted 5 lines

Ah, so "add.usebuiltin = interactive patch" can (eventually) choose to use the C code for both while "add.usebuiltin = interactive" would not use it for the patch mode, or something? Or even

add.interactive.usebuiltin = yes

This is what I had in mind.

Quoted 7 lines
Quoted 1 line

add.patch.usebuiltin = no

And this is what I should have done ;-)

But maybe I should do

add.useBuiltin = wheneverPossible

Ah, either is fine, I think, but because this is meant to be temporary and not advertised to end-users for purposes other than purely an escape hatch, I would not spend too much engineering effort (iow, do the easiest to add and then easiest to later yank out).

Thanks.

#40 lore raw

PATCH v3 2/11 diff: export diffstat interface

In reply to Johannes Schindelin via GitGitGadget

From: Daniel Ferreira <[email protected]>

Make the diffstat interface (namely, the diffstat_t struct and compute_diffstat) no longer be internal to diff.c and allow it to be used by other parts of git.

This is helpful for code that may want to easily extract information from files using the diff machinery, while flushing it differently from how the show_* functions used by diff_flush() do it. One example is the builtin implementation of git-add--interactive's status.

---
 diff.c | 37 +++++++++++++++----------------------
 diff.h | 19 +++++++++++++++++++
 2 files changed, 34 insertions(+), 22 deletions(-)
diff.c +15 -22
index 1ee04e321b..03d6f00d97 100644
@@ -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)
@@ -6273,12 +6257,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)
@@ -6611,6 +6590,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,
diff.h +19 -0
index b680b377b2..34fc658946 100644
@@ -244,6 +244,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,
@@ -333,6 +349,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
 
Signature · 1 line
gitgitgadget
#41 lore raw

Re: [PATCH v3 02/11] diff: export diffstat interface

Quoted 21 lines · Daniel Ferreira via GitGitGadget
diff.c +4 -6
@@ -6273,12 +6257,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)
@@ -6611,6 +6590,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)

diff.c static int is_submodule_ignored(const char *path, struct diff_options *options) · line 6605

Quoted 10 lines
diff.c +10 -0
+{
+	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);
+	}
+}

Hmm, (1) clearing diffstat struct to initialize, (2) looping over diff_queue to compute stat for each path, (3) using diffstat information and then (4) finally freeing the diffstat info is the bog-standard sequence of the user of this API. Merging step (1) and
(2) may probably be OK (iow, I do not think of a use pattern for future users where being able to do some custom things between steps
(1) and (2) would be useful), which is this function is about. (3) is what the user of this API would do, but shouldn't (4) be exported at the same time, if we are making (1+2) as an external API?

Quoted 39 lines
diff.c +0 -0
 void diff_addremove(struct diff_options *options,
 		    int addremove, unsigned mode,
 		    const struct object_id *oid,
diff.h +19 -0
index b680b377b2..34fc658946 100644
@@ -244,6 +244,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,
@@ -333,6 +349,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
#42 lore raw

Re: [PATCH v3 02/11] diff: export diffstat interface

Hi Junio,

Quoted 35 lines · Junio C Hamano
Quoted 21 lines · Daniel Ferreira via GitGitGadget
diff.c +4 -6
@@ -6273,12 +6257,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)
@@ -6611,6 +6590,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)

diff.c static int is_submodule_ignored(const char *path, struct diff_options *options) · line 6605

Quoted 10 lines
diff.c +10 -0
+{
+	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);
+	}
+}

Hmm, (1) clearing diffstat struct to initialize, (2) looping over

Quoted 8 lines

diff_queue to compute stat for each path, (3) using diffstat information and then (4) finally freeing the diffstat info is the bog-standard sequence of the user of this API. Merging step (1) and
(2) may probably be OK (iow, I do not think of a use pattern for future users where being able to do some custom things between steps
(1) and (2) would be useful), which is this function is about. (3) is what the user of this API would do, but shouldn't (4) be exported at the same time, if we are making (1+2) as an external API?

Good point.

It _also_ hints at the fact that we're not releasing the memory properly after running the diffstat in the built-in add -i.

Will fix,
Dscho

Quoted 41 lines
Quoted 40 lines
diff.c +0 -0
 void diff_addremove(struct diff_options *options,
 		    int addremove, unsigned mode,
 		    const struct object_id *oid,
diff.h +19 -0
index b680b377b2..34fc658946 100644
@@ -244,6 +244,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,
@@ -333,6 +349,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
#43 lore raw

PATCH v3 3/11 built-in add -i: implement the `status` command

In reply to Johannes Schindelin via GitGitGadget

From: Daniel Ferreira <[email protected]>

This implements the status command of git add -i. The data structures introduced in this commit will be extended as needed later.

At this point, we re-implement only part of the list_and_choose() function of the Perl script git-add--interactive.perl and call it list(). It does not yet color anything, or do columns, or allow user input.

Over the course of the next commits, we will introduce a list_and_choose() function that uses list() to display the list of options and let the user choose one or more of the displayed items. This will be used to implement the main loop of the built-in git add -i, at which point the new status command can actually be used.

Note that we pass the list of items as a struct item ** as opposed to a struct item *, to allow for the actual items to contain much more information than merely the name.

---
 add-interactive.c | 265 +++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 264 insertions(+), 1 deletion(-)
add-interactive.c +264 -1
index 482e458dc6..59b28011f7 100644
@@ -1,7 +1,270 @@
 #include "cache.h"
 #include "add-interactive.h"
+#include "diffcore.h"
+#include "revision.h"
+#include "refs.h"
+
+struct item {
+	const char *name;
+};
+
+struct list_options {
+	const char *header;
+	void (*print_item)(int i, struct item *item, void *print_item_data);
+	void *print_item_data;
+};
+
+static void list(struct item **list, size_t nr, struct list_options *opts)
+{
+	int i;
+
+	if (!nr)
+		return;
+
+	if (opts->header)
+		printf("%s\n", opts->header);
+
+	for (i = 0; i < nr; i++) {
+		opts->print_item(i, list[i], opts->print_item_data);
+		putchar('\n');
+	}
+}
+
+struct adddel {
+	uintmax_t add, del;
+	unsigned seen:1, binary:1;
+};
+
+struct file_list {
+	struct file_item {
+		struct item item;
+		struct adddel index, worktree;
+	} **file;
+	size_t nr, alloc;
+};
+
+static void add_file_item(struct file_list *list, const char *name)
+{
+	struct file_item *item;
+
+	FLEXPTR_ALLOC_STR(item, item.name, name);
+
+	ALLOC_GROW(list->file, list->nr + 1, list->alloc);
+	list->file[list->nr++] = item;
+}
+
+static void reset_file_list(struct file_list *list)
+{
+	size_t i;
+
+	for (i = 0; i < list->nr; i++)
+		free(list->file[i]);
+	list->nr = 0;
+}
+
+static void release_file_list(struct file_list *list)
+{
+	reset_file_list(list);
+	FREE_AND_NULL(list->file);
+	list->alloc = 0;
+}
+
+static int file_item_cmp(const void *a, const void *b)
+{
+	const struct file_item * const *f1 = a;
+	const struct file_item * const *f2 = b;
+
+	return strcmp((*f1)->item.name, (*f2)->item.name);
+}
+
+struct pathname_entry {
+	struct hashmap_entry ent;
+	size_t index;
+	char pathname[FLEX_ARRAY];
+};
+
+static int pathname_entry_cmp(const void *unused_cmp_data,
+			      const void *entry, const void *entry_or_key,
+			      const void *pathname)
+{
+	const struct pathname_entry *e1 = entry, *e2 = entry_or_key;
+
+	return strcmp(e1->pathname,
+		      pathname ? (const char *)pathname : e2->pathname);
+}
+
+struct collection_status {
+	enum { FROM_WORKTREE = 0, FROM_INDEX = 1 } phase;
+
+	const char *reference;
+
+	struct file_list *list;
+	struct hashmap file_map;
+};
+
+static void collect_changes_cb(struct diff_queue_struct *q,
+			       struct diff_options *options,
+			       void *data)
+{
+	struct collection_status *s = data;
+	struct diffstat_t stat = { 0 };
+	int i;
+
+	if (!q->nr)
+		return;
+
+	compute_diffstat(options, &stat, q);
+
+	for (i = 0; i < stat.nr; i++) {
+		const char *name = stat.files[i]->name;
+		int hash = strhash(name);
+		struct pathname_entry *entry;
+		size_t file_index;
+		struct file_item *file;
+		struct adddel *adddel;
+
+		entry = hashmap_get_from_hash(&s->file_map, hash, name);
+		if (entry)
+			file_index = entry->index;
+		else {
+			FLEX_ALLOC_STR(entry, pathname, name);
+			hashmap_entry_init(entry, hash);
+			entry->index = file_index = s->list->nr;
+			hashmap_add(&s->file_map, entry);
+
+			add_file_item(s->list, name);
+		}
+		file = s->list->file[file_index];
+
+		adddel = s->phase == FROM_INDEX ? &file->index : &file->worktree;
+		adddel->seen = 1;
+		adddel->add = stat.files[i]->added;
+		adddel->del = stat.files[i]->deleted;
+		if (stat.files[i]->is_binary)
+			adddel->binary = 1;
+	}
+}
+
+static int get_modified_files(struct repository *r, struct file_list *list,
+			      const struct pathspec *ps)
+{
+	struct object_id head_oid;
+	int is_initial = !resolve_ref_unsafe("HEAD", RESOLVE_REF_READING,
+					     &head_oid, NULL);
+	struct collection_status s = { FROM_WORKTREE };
+
+	if (repo_read_index_preload(r, ps, 0) < 0)
+		return error(_("could not read index"));
+
+	s.list = list;
+	hashmap_init(&s.file_map, pathname_entry_cmp, NULL, 0);
+
+	for (s.phase = FROM_WORKTREE; s.phase <= FROM_INDEX; s.phase++) {
+		struct rev_info rev;
+		struct setup_revision_opt opt = { 0 };
+
+		opt.def = is_initial ?
+			empty_tree_oid_hex() : oid_to_hex(&head_oid);
+
+		init_revisions(&rev, NULL);
+		setup_revisions(0, NULL, &rev, &opt);
+
+		rev.diffopt.output_format = DIFF_FORMAT_CALLBACK;
+		rev.diffopt.format_callback = collect_changes_cb;
+		rev.diffopt.format_callback_data = &s;
+
+		if (ps)
+			copy_pathspec(&rev.prune_data, ps);
+
+		if (s.phase == FROM_INDEX)
+			run_diff_index(&rev, 1);
+		else {
+			rev.diffopt.flags.ignore_dirty_submodules = 1;
+			run_diff_files(&rev, 0);
+		}
+	}
+	hashmap_free(&s.file_map, 1);
+
+	/* While the diffs are ordered already, we ran *two* diffs... */
+	QSORT(list->file, list->nr, file_item_cmp);
+
+	return 0;
+}
+
+static void populate_wi_changes(struct strbuf *buf,
+				struct adddel *ad, const char *no_changes)
+{
+	if (ad->binary)
+		strbuf_addstr(buf, _("binary"));
+	else if (ad->seen)
+		strbuf_addf(buf, "+%"PRIuMAX"/-%"PRIuMAX,
+			    (uintmax_t)ad->add, (uintmax_t)ad->del);
+	else
+		strbuf_addstr(buf, no_changes);
+}
+
+struct print_file_item_data {
+	const char *modified_fmt;
+	struct strbuf buf, index, worktree;
+};
+
+static void print_file_item(int i, struct item *item,
+			    void *print_file_item_data)
+{
+	struct file_item *c = (struct file_item *)item;
+	struct print_file_item_data *d = print_file_item_data;
+
+	strbuf_reset(&d->index);
+	strbuf_reset(&d->worktree);
+	strbuf_reset(&d->buf);
+
+	populate_wi_changes(&d->worktree, &c->worktree, _("nothing"));
+	populate_wi_changes(&d->index, &c->index, _("unchanged"));
+	strbuf_addf(&d->buf, d->modified_fmt,
+		    d->index.buf, d->worktree.buf, item->name);
+
+	printf(" %2d: %s", i + 1, d->buf.buf);
+}
+
+static int run_status(struct repository *r, const struct pathspec *ps,
+		      struct file_list *files, struct list_options *opts)
+{
+	reset_file_list(files);
+
+	if (get_modified_files(r, files, ps) < 0)
+		return -1;
+
+	if (files->nr)
+		list((struct item **)files->file, files->nr, opts);
+	putchar('\n');
+
+	return 0;
+}
 
 int run_add_i(struct repository *r, const struct pathspec *ps)
 {
-	die(_("No commands are available in the built-in `git add -i` yet!"));
+	struct print_file_item_data print_file_item_data = {
+		"%12s %12s %s", STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
+	};
+	struct list_options opts = {
+		NULL, print_file_item, &print_file_item_data
+	};
+	struct strbuf header = STRBUF_INIT;
+	struct file_list files = { NULL };
+	int res = 0;
+
+	strbuf_addstr(&header, "      ");
+	strbuf_addf(&header, print_file_item_data.modified_fmt,
+		    _("staged"), _("unstaged"), _("path"));
+	opts.header = header.buf;
+
+	res = run_status(r, ps, &files, &opts);
+
+	release_file_list(&files);
+	strbuf_release(&print_file_item_data.buf);
+	strbuf_release(&print_file_item_data.index);
+	strbuf_release(&print_file_item_data.worktree);
+	strbuf_release(&header);
+
+	return res;
 }
Signature · 1 line
gitgitgadget
#44 lore raw

Re: [PATCH v3 03/11] built-in add -i: implement the `status` command

Quoted 18 lines · Daniel Ferreira via GitGitGadget
add-interactive.c +18 -0
+struct item {
+	const char *name;
+};
+
+struct list_options {
+	const char *header;
+	void (*print_item)(int i, struct item *item, void *print_item_data);
+	void *print_item_data;
+};
+
+struct adddel {
+	uintmax_t add, del;
+	unsigned seen:1, binary:1;
+};
+
+struct file_list {
+	struct file_item {
+		struct item item;

add-interactive.c line 84

Quoted 10 lines
add-interactive.c +10 -0
+		struct adddel index, worktree;
+	} **file;
+	size_t nr, alloc;
+};
+
+struct pathname_entry {
+	struct hashmap_entry ent;
+	size_t index;
+	char pathname[FLEX_ARRAY];
+};

All of the above are named too generic but assuming that add-i will stay to be a single file and these names will never leak outside the file to become global, it would be perfectly fine.

add-interactive.c line 204

Quoted 11 lines
add-interactive.c +11 -0
+static void populate_wi_changes(struct strbuf *buf,
+				struct adddel *ad, const char *no_changes)
+{
+	if (ad->binary)
+		strbuf_addstr(buf, _("binary"));
+	else if (ad->seen)
+		strbuf_addf(buf, "+%"PRIuMAX"/-%"PRIuMAX,
+			    (uintmax_t)ad->add, (uintmax_t)ad->del);
+	else
+		strbuf_addstr(buf, no_changes);
+}

I offhand do not see the need for (uintmax_t) casts here...

add-interactive.c line 239

Quoted 11 lines
add-interactive.c +11 -0
+static int run_status(struct repository *r, const struct pathspec *ps,
+		      struct file_list *files, struct list_options *opts)
+{
+	reset_file_list(files);
+
+	if (get_modified_files(r, files, ps) < 0)
+		return -1;
+
+	if (files->nr)
+		list((struct item **)files->file, files->nr, opts);
+	putchar('\n');

So, if there is anything to list, we show list() and then add an empty line; if there is nothing to list, we show an empty line anyway?

As long as that matches the current scripted "add -i", it's perfectly fine. It's just that the code structure above looked somewhat odd.

Quoted 32 lines
add-interactive.c +32 -0
+static void collect_changes_cb(struct diff_queue_struct *q,
+			       struct diff_options *options,
+			       void *data)
+{
+	struct collection_status *s = data;
+	struct diffstat_t stat = { 0 };
+	int i;
+
+	if (!q->nr)
+		return;
+
+	compute_diffstat(options, &stat, q);
+
+	for (i = 0; i < stat.nr; i++) {
+		const char *name = stat.files[i]->name;
+		int hash = strhash(name);
+		struct pathname_entry *entry;
+		size_t file_index;
+		struct file_item *file;
+		struct adddel *adddel;
+
+		entry = hashmap_get_from_hash(&s->file_map, hash, name);
+		if (entry)
+			file_index = entry->index;
+		else {
+			FLEX_ALLOC_STR(entry, pathname, name);
+			hashmap_entry_init(entry, hash);
+			entry->index = file_index = s->list->nr;
+			hashmap_add(&s->file_map, entry);
+
+			add_file_item(s->list, name);
+		}

add-interactive.c line 146

Quoted 10 lines
add-interactive.c +10 -0
+		file = s->list->file[file_index];
+
+		adddel = s->phase == FROM_INDEX ? &file->index : &file->worktree;
+		adddel->seen = 1;
+		adddel->add = stat.files[i]->added;
+		adddel->del = stat.files[i]->deleted;
+		if (stat.files[i]->is_binary)
+			adddel->binary = 1;
+	}
+}

Would resources held in the "stat" structure leak at the end of this function?

#45 lore raw

Re: [PATCH v3 03/11] built-in add -i: implement the `status` command

Hi Junio,

Quoted 26 lines · Junio C Hamano
Quoted 24 lines · Daniel Ferreira via GitGitGadget
add-interactive.c +24 -0
+struct item {
+	const char *name;
+};
+
+struct list_options {
+	const char *header;
+	void (*print_item)(int i, struct item *item, void *print_item_data);
+	void *print_item_data;
+};
+
+struct adddel {
+	uintmax_t add, del;
+	unsigned seen:1, binary:1;
+};
+
+struct file_list {
+	struct file_item {
+		struct item item;
+		struct adddel index, worktree;
+	} **file;
+	size_t nr, alloc;
+};
+
+struct pathname_entry {
Quoted 8 lines

add-interactive.c line 84

Quoted 4 lines
add-interactive.c +4 -0
+	struct hashmap_entry ent;
+	size_t index;
+	char pathname[FLEX_ARRAY];
+};

All of the above are named too generic but assuming that add-i will stay to be a single file and these names will never leak outside the file to become global, it would be perfectly fine.

Yep.

Quoted 5 lines
Quoted 5 lines
add-interactive.c +5 -0
+static void populate_wi_changes(struct strbuf *buf,
+				struct adddel *ad, const char *no_changes)
+{
+	if (ad->binary)
+		strbuf_addstr(buf, _("binary"));
Quoted 8 lines

add-interactive.c line 204

Quoted 6 lines
add-interactive.c +6 -0
+	else if (ad->seen)
+		strbuf_addf(buf, "+%"PRIuMAX"/-%"PRIuMAX,
+			    (uintmax_t)ad->add, (uintmax_t)ad->del);
+	else
+		strbuf_addstr(buf, no_changes);
+}

I offhand do not see the need for (uintmax_t) casts here...

Ah, that's my mistake. I had converted the add and del fields from the unsigned long data type that the 20th century wanted back.

Quoted 16 lines

add-interactive.c line 239

Quoted 11 lines
add-interactive.c +11 -0
+static int run_status(struct repository *r, const struct pathspec *ps,
+		      struct file_list *files, struct list_options *opts)
+{
+	reset_file_list(files);
+
+	if (get_modified_files(r, files, ps) < 0)
+		return -1;
+
+	if (files->nr)
+		list((struct item **)files->file, files->nr, opts);
+	putchar('\n');

So, if there is anything to list, we show list() and then add an empty line; if there is nothing to list, we show an empty line anyway?

Quoted 3 lines

As long as that matches the current scripted "add -i", it's perfectly fine. It's just that the code structure above looked somewhat odd.

Yep, this is the Perl version:

sub status_cmd {
        list_and_choose({ LIST_ONLY => 1, HEADER => $status_head },
			list_modified());
	print "\n";
}
Quoted 37 lines
Quoted 37 lines
add-interactive.c +37 -0
+static void collect_changes_cb(struct diff_queue_struct *q,
+			       struct diff_options *options,
+			       void *data)
+{
+	struct collection_status *s = data;
+	struct diffstat_t stat = { 0 };
+	int i;
+
+	if (!q->nr)
+		return;
+
+	compute_diffstat(options, &stat, q);
+
+	for (i = 0; i < stat.nr; i++) {
+		const char *name = stat.files[i]->name;
+		int hash = strhash(name);
+		struct pathname_entry *entry;
+		size_t file_index;
+		struct file_item *file;
+		struct adddel *adddel;
+
+		entry = hashmap_get_from_hash(&s->file_map, hash, name);
+		if (entry)
+			file_index = entry->index;
+		else {
+			FLEX_ALLOC_STR(entry, pathname, name);
+			hashmap_entry_init(entry, hash);
+			entry->index = file_index = s->list->nr;
+			hashmap_add(&s->file_map, entry);
+
+			add_file_item(s->list, name);
+		}
+		file = s->list->file[file_index];
+
+		adddel = s->phase == FROM_INDEX ? &file->index : &file->worktree;
+		adddel->seen = 1;
+		adddel->add = stat.files[i]->added;
Quoted 8 lines

add-interactive.c line 146

Quoted 5 lines
add-interactive.c +5 -0
+		adddel->del = stat.files[i]->deleted;
+		if (stat.files[i]->is_binary)
+			adddel->binary = 1;
+	}
+}

Would resources held in the "stat" structure leak at the end of this function?

Yep, I added a call to the now-public free_diffstat_info() function.

Thanks,
Dscho

#46 lore raw

PATCH v3 4/11 built-in add -i: refresh the index before running `status`

In reply to Johannes Schindelin via GitGitGadget

From: Johannes Schindelin <[email protected]>

This is what the Perl version does, and therefore it is what the built-in version should do, too.

---
 add-interactive.c |  4 +++-
 repository.c      | 19 +++++++++++++++++++
 repository.h      |  7 +++++++
 3 files changed, 29 insertions(+), 1 deletion(-)
add-interactive.c +3 -1
index 59b28011f7..2dbf29dee2 100644
@@ -258,7 +258,9 @@ int run_add_i(struct repository *r, const struct pathspec *ps)
 		    _("staged"), _("unstaged"), _("path"));
 	opts.header = header.buf;
 
-	res = run_status(r, ps, &files, &opts);
+	repo_refresh_and_write_index(r, REFRESH_QUIET, 1);
+	if (run_status(r, ps, &files, &opts) < 0)
+		res = -1;
 
 	release_file_list(&files);
 	strbuf_release(&print_file_item_data.buf);
repository.c +19 -0
index 682c239fe3..def35c40fc 100644
@@ -275,3 +275,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;
+}
repository.h +7 -0
index 4fb6a5885f..cf5d5bab48 100644
@@ -157,5 +157,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 */
Signature · 1 line
gitgitgadget
#47 lore raw

PATCH v3 5/11 built-in add -i: color the header in the `status` command

In reply to Johannes Schindelin via GitGitGadget

From: Johannes Schindelin <[email protected]>

For simplicity, we only implemented the status command without colors. This patch starts adding color, matching what the Perl script git-add--interactive.perl does.

---
 add-interactive.c | 60 ++++++++++++++++++++++++++++++++++++++++++-----
 1 file changed, 54 insertions(+), 6 deletions(-)
add-interactive.c +54 -6
index 2dbf29dee2..6c2fca12c1 100644
@@ -1,9 +1,51 @@
 #include "cache.h"
 #include "add-interactive.h"
+#include "color.h"
+#include "config.h"
 #include "diffcore.h"
 #include "revision.h"
 #include "refs.h"
 
+struct add_i_state {
+	struct repository *r;
+	int use_color;
+	char header_color[COLOR_MAXLEN];
+};
+
+static void init_color(struct repository *r, struct add_i_state *s,
+		       const char *slot_name, char *dst,
+		       const char *default_color)
+{
+	char *key = xstrfmt("color.interactive.%s", slot_name);
+	const char *value;
+
+	if (!s->use_color)
+		dst[0] = '\0';
+	else if (repo_config_get_value(r, key, &value) ||
+		 color_parse(value, dst))
+		strlcpy(dst, default_color, COLOR_MAXLEN);
+
+	free(key);
+}
+
+static int init_add_i_state(struct repository *r, struct add_i_state *s)
+{
+	const char *value;
+
+	s->r = r;
+
+	if (repo_config_get_value(r, "color.interactive", &value))
+		s->use_color = -1;
+	else
+		s->use_color =
+			git_config_colorbool("color.interactive", value);
+	s->use_color = want_color(s->use_color);
+
+	init_color(r, s, "header", s->header_color, GIT_COLOR_BOLD);
+
+	return 0;
+}
+
 struct item {
 	const char *name;
 };
@@ -14,7 +56,8 @@ struct list_options {
 	void *print_item_data;
 };
 
-static void list(struct item **list, size_t nr, struct list_options *opts)
+static void list(struct item **list, size_t nr,
+		 struct add_i_state *s, struct list_options *opts)
 {
 	int i;
 
@@ -22,7 +65,8 @@ static void list(struct item **list, size_t nr, struct list_options *opts)
 		return;
 
 	if (opts->header)
-		printf("%s\n", opts->header);
+		color_fprintf_ln(stdout, s->header_color,
+				 "%s", opts->header);
 
 	for (i = 0; i < nr; i++) {
 		opts->print_item(i, list[i], opts->print_item_data);
@@ -226,16 +270,16 @@ static void print_file_item(int i, struct item *item,
 	printf(" %2d: %s", i + 1, d->buf.buf);
 }
 
-static int run_status(struct repository *r, const struct pathspec *ps,
+static int run_status(struct add_i_state *s, const struct pathspec *ps,
 		      struct file_list *files, struct list_options *opts)
 {
 	reset_file_list(files);
 
-	if (get_modified_files(r, files, ps) < 0)
+	if (get_modified_files(s->r, files, ps) < 0)
 		return -1;
 
 	if (files->nr)
-		list((struct item **)files->file, files->nr, opts);
+		list((struct item **)files->file, files->nr, s, opts);
 	putchar('\n');
 
 	return 0;
@@ -243,6 +287,7 @@ static int run_status(struct repository *r, const struct pathspec *ps,
 
 int run_add_i(struct repository *r, const struct pathspec *ps)
 {
+	struct add_i_state s = { NULL };
 	struct print_file_item_data print_file_item_data = {
 		"%12s %12s %s", STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
 	};
@@ -253,13 +298,16 @@ int run_add_i(struct repository *r, const struct pathspec *ps)
 	struct file_list files = { NULL };
 	int res = 0;
 
+	if (init_add_i_state(r, &s))
+		return error("could not parse `add -i` config");
+
 	strbuf_addstr(&header, "      ");
 	strbuf_addf(&header, print_file_item_data.modified_fmt,
 		    _("staged"), _("unstaged"), _("path"));
 	opts.header = header.buf;
 
 	repo_refresh_and_write_index(r, REFRESH_QUIET, 1);
-	if (run_status(r, ps, &files, &opts) < 0)
+	if (run_status(&s, ps, &files, &opts) < 0)
 		res = -1;
 
 	release_file_list(&files);
Signature · 1 line
gitgitgadget
#48 lore raw

PATCH v3 6/11 built-in add -i: implement the main loop

In reply to Johannes Schindelin via GitGitGadget

From: Johannes Schindelin <[email protected]>

The reason why we did not start with the main loop to begin with is that it is the first user of list_and_choose(), which uses the list() function that we conveniently introduced for use by the status command.

Apart from the "and choose" part, there are more differences between the way the status command calls the list_and_choose() function in the Perl version of git add -i compared to the other callers of said function. The most important ones:

- The list is not only shown, but the user is also asked to make a choice, possibly selecting multiple entries.

- The list of items is prefixed with a marker indicating what items have been selected, if multi-selection is allowed.

- Initially, for each item a unique prefix (if there exists any within the given parameters) is determined, and shown in the list, and accepted as a shortcut for the selection.

These features will be implemented later, except the part where the user can choose a command. At this stage, though, the built-in git add -i still only supports the status command, with the remaining commands to follow over the course of the next commits.

In addition, we also modify list() to support displaying the commands in columns, even if there is currently only one.

The Perl script git-add--interactive.perl mixed the purposes of the "list" and the "and choose" part into the same function. In the C version, we will keep them separate instead, calling the list() function from the list_and_choose() function.

Note that we only have a prompt ending in a single ">" at this stage; later commits will add commands that display a double ">>" to indicate that the user is in a different loop than the main one.

---
 add-interactive.c | 129 +++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 127 insertions(+), 2 deletions(-)
add-interactive.c +127 -2
index 6c2fca12c1..0ea4f3edb8 100644
@@ -51,6 +51,7 @@ struct item {
 };
 
 struct list_options {
+	int columns;
 	const char *header;
 	void (*print_item)(int i, struct item *item, void *print_item_data);
 	void *print_item_data;
@@ -59,7 +60,7 @@ struct list_options {
 static void list(struct item **list, size_t nr,
 		 struct add_i_state *s, struct list_options *opts)
 {
-	int i;
+	int i, last_lf = 0;
 
 	if (!nr)
 		return;
@@ -70,8 +71,97 @@ static void list(struct item **list, size_t nr,
 
 	for (i = 0; i < nr; i++) {
 		opts->print_item(i, list[i], opts->print_item_data);
+
+		if ((opts->columns) && ((i + 1) % (opts->columns))) {
+			putchar('\t');
+			last_lf = 0;
+		}
+		else {
+			putchar('\n');
+			last_lf = 1;
+		}
+	}
+
+	if (!last_lf)
 		putchar('\n');
+}
+struct list_and_choose_options {
+	struct list_options list_opts;
+
+	const char *prompt;
+};
+
+#define LIST_AND_CHOOSE_ERROR (-1)
+#define LIST_AND_CHOOSE_QUIT  (-2)
+
+/*
+ * Returns the selected index.
+ *
+ * If an error occurred, returns `LIST_AND_CHOOSE_ERROR`. Upon EOF,
+ * `LIST_AND_CHOOSE_QUIT` is returned.
+ */
+static ssize_t list_and_choose(struct item **items, size_t nr,
+			       struct add_i_state *s,
+			       struct list_and_choose_options *opts)
+{
+	struct strbuf input = STRBUF_INIT;
+	ssize_t res = LIST_AND_CHOOSE_ERROR;
+
+	for (;;) {
+		char *p, *endp;
+
+		strbuf_reset(&input);
+
+		list(items, nr, s, &opts->list_opts);
+
+		printf("%s%s", opts->prompt, "> ");
+		fflush(stdout);
+
+		if (strbuf_getline(&input, stdin) == EOF) {
+			putchar('\n');
+			res = LIST_AND_CHOOSE_QUIT;
+			break;
+		}
+		strbuf_trim(&input);
+
+		if (!input.len)
+			break;
+
+		p = input.buf;
+		for (;;) {
+			size_t sep = strcspn(p, " \t\r\n,");
+			ssize_t index = -1;
+
+			if (!sep) {
+				if (!*p)
+					break;
+				p++;
+				continue;
+			}
+
+			if (isdigit(*p)) {
+				index = strtoul(p, &endp, 10) - 1;
+				if (endp != p + sep)
+					index = -1;
+			}
+
+			p[sep] = '\0';
+			if (index < 0 || index >= nr)
+				printf(_("Huh (%s)?\n"), p);
+			else {
+				res = index;
+				break;
+			}
+
+			p += sep + 1;
+		}
+
+		if (res != LIST_AND_CHOOSE_ERROR)
+			break;
 	}
+
+	strbuf_release(&input);
+	return res;
 }
 
 struct adddel {
@@ -285,17 +375,40 @@ static int run_status(struct add_i_state *s, const struct pathspec *ps,
 	return 0;
 }
 
+static void print_command_item(int i, struct item *item,
+			       void *print_command_item_data)
+{
+	printf(" %2d: %s", i + 1, item->name);
+}
+
+struct command_item {
+	struct item item;
+	int (*command)(struct add_i_state *s, const struct pathspec *ps,
+		       struct file_list *files, struct list_options *opts);
+};
+
 int run_add_i(struct repository *r, const struct pathspec *ps)
 {
 	struct add_i_state s = { NULL };
+	struct list_and_choose_options main_loop_opts = {
+		{ 4, N_("*** Commands ***"), print_command_item, NULL },
+		N_("What now")
+	};
+	struct command_item
+		status = { { "status" }, run_status };
+	struct command_item *commands[] = {
+		&status
+	};
+
 	struct print_file_item_data print_file_item_data = {
 		"%12s %12s %s", STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
 	};
 	struct list_options opts = {
-		NULL, print_file_item, &print_file_item_data
+		0, NULL, print_file_item, &print_file_item_data
 	};
 	struct strbuf header = STRBUF_INIT;
 	struct file_list files = { NULL };
+	ssize_t i;
 	int res = 0;
 
 	if (init_add_i_state(r, &s))
@@ -310,6 +423,18 @@ int run_add_i(struct repository *r, const struct pathspec *ps)
 	if (run_status(&s, ps, &files, &opts) < 0)
 		res = -1;
 
+	for (;;) {
+		i = list_and_choose((struct item **)commands,
+				    ARRAY_SIZE(commands), &s, &main_loop_opts);
+		if (i == LIST_AND_CHOOSE_QUIT) {
+			printf(_("Bye.\n"));
+			res = 0;
+			break;
+		}
+		if (i != LIST_AND_CHOOSE_ERROR)
+			res = commands[i]->command(&s, ps, &files, &opts);
+	}
+
 	release_file_list(&files);
 	strbuf_release(&print_file_item_data.buf);
 	strbuf_release(&print_file_item_data.index);
Signature · 1 line
gitgitgadget
#49 lore raw

Re: [PATCH v3 06/11] built-in add -i: implement the main loop

"Johannes Schindelin via GitGitGadget" <[email protected]>

Quoted 4 lines · writes:

The Perl script git-add--interactive.perl mixed the purposes of the "list" and the "and choose" part into the same function. In the C version, we will keep them separate instead, calling the list() function from the list_and_choose() function.

Makes sense. After all, I recall writing "add -i" in a few iterations of lunch-time hack. If we are to rewrite it from scratch, we should engineer it the right way ;-)

#50 lore raw

PATCH v3 7/11 Add a function to determine unique prefixes for a list of strings

In reply to Johannes Schindelin via GitGitGadget

From: Slavica Djukic <[email protected]>

In the git add -i command, we show unique prefixes of the commands and files, to give an indication what prefix would select them.

Naturally, the C implementation looks a lot different than the Perl
implementation: in Perl, a trie is much easier implemented, while we already have a pretty neat hashmap implementation in C that we use for the purpose of storing (not necessarily unique) prefixes.

The idea: for each item that we add, we generate prefixes starting with the first letter, then the first two letters, then three, etc, until we find a prefix that is unique (or until the prefix length would be longer than we want). If we encounter a previously-unique prefix on the way, we adjust that item's prefix to make it unique again (or we mark it as having no unique prefix if we failed to find one). These partial prefixes are stored in a hash map (for quick lookup times).

To make sure that this function works as expected, we add a test using a special-purpose test helper that was added for that purpose.

Note: We expect the list of prefix items to be passed in as a list of pointers rather than as regular list to avoid having to copy information (the actual items will most likely contain more information than just the name and the length of the unique prefix, but passing in struct prefix_item * would not allow for that).

---
 Makefile                   |   2 +
 prefix-map.c               | 109 +++++++++++++++++++++++++++++++++++++
 prefix-map.h               |  40 ++++++++++++++
 t/helper/test-prefix-map.c |  58 ++++++++++++++++++++
 t/helper/test-tool.c       |   1 +
 t/helper/test-tool.h       |   1 +
 t/t0018-prefix-map.sh      |  10 ++++
 7 files changed, 221 insertions(+)
 create mode 100644 prefix-map.c
 create mode 100644 prefix-map.h
 create mode 100644 t/helper/test-prefix-map.c
 create mode 100755 t/t0018-prefix-map.sh
Makefile +2 -0
index 0a47200f47..545a0e8743 100644
@@ -725,6 +725,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
@@ -943,6 +944,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
prefix-map.c new file +109 -0

109 lines not on this page · raw message

prefix-map.h new file +40 -0
new file mode 100644
index 0000000000..ce3b8a4a32
@@ -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
t/helper/test-prefix-map.c new file +58 -0

58 lines not on this page · raw message

t/helper/test-tool.c +1 -0
index 087a8c0cc9..e076771b96 100644
@@ -39,6 +39,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 },
t/helper/test-tool.h +1 -0
index 7e703f3038..891bac703b 100644
@@ -29,6 +29,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);
t/t0018-prefix-map.sh new file +10 -0
new file mode 100755
index 0000000000..187fa92aec
@@ -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
Signature · 1 line
gitgitgadget
#51 lore raw

Re: [PATCH v3 07/11] Add a function to determine unique prefixes for a list of strings

Quoted 7 lines · Slavica Djukic via GitGitGadget

The idea: for each item that we add, we generate prefixes starting with the first letter, then the first two letters, then three, etc, until we find a prefix that is unique (or until the prefix length would be longer than we want). If we encounter a previously-unique prefix on the way, we adjust that item's prefix to make it unique again (or we mark it as having no unique prefix if we failed to find one). These partial prefixes are stored in a hash map (for quick lookup times).

OK. I suppose such a machinery that accepts a set of strings and then give us back a set of unique prefix length for each element of the set would be useful in general, even outside the "and choose" part of "list and choose". Nice design.

Quoted 2 lines

To make sure that this function works as expected, we add a test using a special-purpose test helper that was added for that purpose.

Somehow the above repeatedly says how special purpose it is redundantly ;-)

Quoted 36 lines
prefix-map.h new file +30 -0
new file mode 100644
index 0000000000..ce3b8a4a32
@@ -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.
+ *

prefix-map.h line 40

Quoted 10 lines
prefix-map.h new file +10 -0
+ * 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

Looks like a quite sane interface to me.

Thanks.

#52 lore raw

Re: [PATCH v3 07/11] Add a function to determine unique prefixes for a list of strings

In reply to Slavica Djukic via GitGitGadget
Quoted 130 lines · Slavica Djukic via GitGitGadget

In the git add -i command, we show unique prefixes of the commands and files, to give an indication what prefix would select them.

Naturally, the C implementation looks a lot different than the Perl
implementation: in Perl, a trie is much easier implemented, while we already have a pretty neat hashmap implementation in C that we use for the purpose of storing (not necessarily unique) prefixes.

The idea: for each item that we add, we generate prefixes starting with the first letter, then the first two letters, then three, etc, until we find a prefix that is unique (or until the prefix length would be longer than we want). If we encounter a previously-unique prefix on the way, we adjust that item's prefix to make it unique again (or we mark it as having no unique prefix if we failed to find one). These partial prefixes are stored in a hash map (for quick lookup times).

To make sure that this function works as expected, we add a test using a special-purpose test helper that was added for that purpose.

Note: We expect the list of prefix items to be passed in as a list of pointers rather than as regular list to avoid having to copy information (the actual items will most likely contain more information than just the name and the length of the unique prefix, but passing in struct prefix_item * would not allow for that).

prefix-map.c new file +99 -0
new file mode 100644
index 0000000000..747ddb4ebc
@@ -0,0 +1,109 @@
+#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 = { { NULL } }, *e2;
+	int j;
+
+	e.item = item;
+	e.name = item->name;
+
+	for (j = prefix_map->min_length;
+	     j <= prefix_map->max_length && e.name[j]; j++) {
+		/* Avoid breaking UTF-8 multi-byte sequences */
+		if (!isascii(e.name[j]))
+			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 at this stage */
+			item->prefix_length = j;
+			add_prefix_entry(&prefix_map->map, e.name, j, item);
+			break;
+		}
+
+		if (!e2->item)
+			continue; /* non-unique prefix */
+
+		if (j != e2->item->prefix_length || memcmp(e.name, e2->name, j))
+			BUG("unexpected prefix length: %d != %d (%s != %s)",
+			    j, (int)e2->item->prefix_length, e.name, e2->name);
+
+		/* 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;
+
+		break;
+	}
+}
+
+void find_unique_prefixes(struct prefix_item **list, size_t nr,

prefix-map.c line 109

Quoted 10 lines
prefix-map.c new file +10 -0
+			  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);
+}

Between the commit message, the in-code comment, the names of the new files, and implementation I was left somewhat confused about what this is about and how it works. TBH, I didn't even try to understand how all the above works, in particular the add_prefix_item() function.

However, I think it would be much-much simpler to first sort (a copy of?) the array of prefix item pointers based on their 'name' field, and then look for a unique prefix in each neighboring pair. Perhaps it would even be faster, because it doesn't have to allocate a bunch of hashmap items, though I don't think that it matters much in practice (i.e. I expect the number of items to be fairly small; presumably nobody will run interactive add after a mass refactoring modifying thousands of files).

Quoted 6 lines
prefix-map.h new file +0 -0
new file mode 100644
index 0000000000..ce3b8a4a32

prefix-map.h line 9

Quoted 9 lines
prefix-map.h new file +8 -0
@@ -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;
+};

This struct is part of find_unique_prefixes()'s signature, good.

prefix-map.h line 22

Quoted 12 lines
prefix-map.h new file +12 -0
+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;
+};

However, neither of these two structs nor the hashmap appear in the function's signature, but are all implementation details. Therefore, they should not be defined and included here in the header but in the .c source file. (But as mentioned above, I think this could be implemented much simpler without these data structures.)

Furthermore, this is not a map.
A map, in general, is a container of key-value pairs that allows efficient insertion, removal and lookup. This so-called prefix_map does none of that, so it should not be called a map.

Quoted 2 lines

+/*
+ * Find unique prefixes in a given list of strings.

... and stores the length of the unique prefixes in the 'prefix_length' field of the elements of the given array.

Quoted 2 lines

+ *
+ * Typically, the struct prefix_item information will be but a field in the

s/but //, perhaps?

Quoted 10 lines

+ * 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,

The first argument is not a list but an array.

prefix-map.h line 38

Quoted 1 line
prefix-map.h new file +1 -0
+			  int min_length, int max_length);

size_t, perhaps? These are closely related to 'prefix_item.prefix_length', which is already (rightfully) size_t.

Quoted 32 lines
prefix-map.h new file +2 -0
+
+#endif
t/helper/test-prefix-map.c new file +24 -0
new file mode 100644
index 0000000000..3f1c90eaf0
@@ -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++;
+}
+

t/helper/test-prefix-map.c line 34

Quoted 10 lines
t/helper/test-prefix-map.c new file +10 -0
+#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] = {

You don't have to tell the compiler how many elements this array will contain, it will figure that out on its own.

t/helper/test-prefix-map.c line 41

Quoted 7 lines
t/helper/test-prefix-map.c new file +7 -0
+		{ "unique" },
+		{ "hell" },
+		{ "hello" },
+		{ "wok" },
+		{ "world" },
+	};
+	struct prefix_item *list[NR] = {

Likewise.

t/helper/test-prefix-map.c line 45

Quoted 4 lines
t/helper/test-prefix-map.c new file +4 -0
+		items, items + 1, items + 2, items + 3, items + 4
+	};
+
+	find_unique_prefixes(list, NR, 1, 3);

This could be find_unique_prefixes(list, ARRAY_SIZE(list), 1, 3), and then there is no need for that NR macro anymore.

Quoted 13 lines
t/helper/test-prefix-map.c new file +13 -0
+
+#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;
+}
#53 lore raw

Re: [PATCH v3 07/11] Add a function to determine unique prefixes for a list of strings

Hi Gábor,

Quoted 138 lines · SZEDER Gábor
Quoted 137 lines · Slavica Djukic via GitGitGadget

In the git add -i command, we show unique prefixes of the commands and files, to give an indication what prefix would select them.

Naturally, the C implementation looks a lot different than the Perl
implementation: in Perl, a trie is much easier implemented, while we already have a pretty neat hashmap implementation in C that we use for the purpose of storing (not necessarily unique) prefixes.

The idea: for each item that we add, we generate prefixes starting with the first letter, then the first two letters, then three, etc, until we find a prefix that is unique (or until the prefix length would be longer than we want). If we encounter a previously-unique prefix on the way, we adjust that item's prefix to make it unique again (or we mark it as having no unique prefix if we failed to find one). These partial prefixes are stored in a hash map (for quick lookup times).

To make sure that this function works as expected, we add a test using a special-purpose test helper that was added for that purpose.

Note: We expect the list of prefix items to be passed in as a list of pointers rather than as regular list to avoid having to copy information (the actual items will most likely contain more information than just the name and the length of the unique prefix, but passing in struct prefix_item * would not allow for that).

prefix-map.c new file +106 -0
new file mode 100644
index 0000000000..747ddb4ebc
@@ -0,0 +1,109 @@
+#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 = { { NULL } }, *e2;
+	int j;
+
+	e.item = item;
+	e.name = item->name;
+
+	for (j = prefix_map->min_length;
+	     j <= prefix_map->max_length && e.name[j]; j++) {
+		/* Avoid breaking UTF-8 multi-byte sequences */
+		if (!isascii(e.name[j]))
+			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 at this stage */
+			item->prefix_length = j;
+			add_prefix_entry(&prefix_map->map, e.name, j, item);
+			break;
+		}
+
+		if (!e2->item)
+			continue; /* non-unique prefix */
+
+		if (j != e2->item->prefix_length || memcmp(e.name, e2->name, j))
+			BUG("unexpected prefix length: %d != %d (%s != %s)",
+			    j, (int)e2->item->prefix_length, e.name, e2->name);
+
+		/* 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;
+
+		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++)
Quoted 8 lines

prefix-map.c line 109

Quoted 3 lines
prefix-map.c new file +3 -0
+		add_prefix_item(&prefix_map, list[i]);
+	hashmap_free(&prefix_map.map, 1);
+}

Between the commit message, the in-code comment, the names of the new files, and implementation I was left somewhat confused about what this is about and how it works. TBH, I didn't even try to understand how all the above works, in particular the add_prefix_item() function.

Let me try to explain it here, and maybe you can help me by suggesting an improved commit message and/or code comments?

The problem is this: given a set of items with labels (e.g. file names), find, for each item, the unique prefix that identifies it. Example:
given the files hello.txt, heaven.txt and hell.txt, the items' unique prefixes would be hello, hea and hell., respectively.

In git add -i, we actually only want to allow alphanumerical prefixes, and we also want at least one, and at most three characters, so only the second item would have an admissible unique prefix: hea.

Quoted 8 lines

However, I think it would be much-much simpler to first sort (a copy of?) the array of prefix item pointers based on their 'name' field, and then look for a unique prefix in each neighboring pair. Perhaps it would even be faster, because it doesn't have to allocate a bunch of hashmap items, though I don't think that it matters much in practice (i.e. I expect the number of items to be fairly small; presumably nobody will run interactive add after a mass refactoring modifying thousands of files).

The time complexity of the sorted list would be O(n*log(n)), while the hashmap-based complexity would be an amortized O(n).

And yes, you would not _want_ to run interactive add after a mass refactoring. But it happens. It happens to me more times than I care to admit. And you know what? I really appreciate that even the Perl version is relatively snappy in those circumstances.

Quoted 28 lines
Quoted 6 lines
prefix-map.h new file +0 -0
new file mode 100644
index 0000000000..ce3b8a4a32

prefix-map.h line 9

Quoted 9 lines
prefix-map.h new file +8 -0
@@ -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;
+};

This struct is part of find_unique_prefixes()'s signature, good.

Quoted 10 lines
prefix-map.h new file +10 -0
+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;
Quoted 8 lines

prefix-map.h line 22

Quoted 2 lines
prefix-map.h new file +2 -0
+	int min_length, max_length;
+};

However, neither of these two structs nor the hashmap appear in the function's signature, but are all implementation details. Therefore, they should not be defined and included here in the header but in the .c source file. (But as mentioned above, I think this could be implemented much simpler without these data structures.)

Right you are!

Quoted 4 lines

Furthermore, this is not a map.
A map, in general, is a container of key-value pairs that allows efficient insertion, removal and lookup. This so-called prefix_map does none of that, so it should not be called a map.

What would you call it instead?

(I went with "map" because the underlying data structure is a "hash map", I know, not the best argument, but I failed to find a better name...)

I also have to admit that I thought that I could fix the design where git-add--interactive.perl creates this trie, but then still performs a linear search when searching by prefix. That seems not to be possible, though, as the unique prefixes are limited to certain character ranges, while the lookup-by-prefix is not limited in that way.

Quoted 5 lines
Quoted 2 lines

+/*
+ * Find unique prefixes in a given list of strings.

... and stores the length of the unique prefixes in the 'prefix_length' field of the elements of the given array.

Good idea. I changed it to also explain what is meant by "unique prefix":

* Given a list of names, find unique prefixes (i.e. the first <n> characters
* that uniquely identify the names) and store the lengths of the unique
* prefixes in the 'prefix_length' field of the elements of the given array..

Quoted 4 lines
Quoted 2 lines

+ *
+ * Typically, the struct prefix_item information will be but a field in the

s/but //, perhaps?

Sure. I am relatively certain that it is correct grammar, but it is probably a good idea to remove it.

Quoted 11 lines
Quoted 10 lines

+ * 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,

Quoted 1 line

The first argument is not a list but an array.

Indeed.

Quoted 4 lines

prefix-map.h line 38

Quoted 1 line
prefix-map.h new file +1 -0
+			  int min_length, int max_length);

size_t, perhaps? These are closely related to 'prefix_item.prefix_length', which is already (rightfully) size_t.

True.

Quoted 45 lines
Quoted 31 lines
prefix-map.h new file +1 -0
+#endif
t/helper/test-prefix-map.c new file +24 -0
new file mode 100644
index 0000000000..3f1c90eaf0
@@ -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++;
+}
+

t/helper/test-prefix-map.c line 34

Quoted 10 lines
t/helper/test-prefix-map.c new file +10 -0
+#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] = {

You don't have to tell the compiler how many elements this array will contain, it will figure that out on its own.

Quoted 9 lines

t/helper/test-prefix-map.c line 41

Quoted 7 lines
t/helper/test-prefix-map.c new file +7 -0
+		{ "unique" },
+		{ "hell" },
+		{ "hello" },
+		{ "wok" },
+		{ "world" },
+	};
+	struct prefix_item *list[NR] = {

Likewise.

That is correct.

What the compiler _cannot_ figure out, on its own, however, is that items and list _need_ to contain the same number of items.

Hence the need for NR.

Quoted 8 lines

t/helper/test-prefix-map.c line 45

Quoted 4 lines
t/helper/test-prefix-map.c new file +4 -0
+		items, items + 1, items + 2, items + 3, items + 4
+	};
+
+	find_unique_prefixes(list, NR, 1, 3);

This could be find_unique_prefixes(list, ARRAY_SIZE(list), 1, 3), and then there is no need for that NR macro anymore.

Quoted 13 lines

t/helper/test-prefix-map.c line 58

Quoted 13 lines
t/helper/test-prefix-map.c new file +13 -0
+
+#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;
+}

Thank you for your review!
Dscho

#54 lore raw

Re: [PATCH v3 07/11] Add a function to determine unique prefixes for a list of strings

Quoted 74 lines · Johannes Schindelin

Hi Gábor,

Quoted 70 lines · SZEDER Gábor
Quoted 69 lines · Slavica Djukic via GitGitGadget

In the git add -i command, we show unique prefixes of the commands and files, to give an indication what prefix would select them.

Naturally, the C implementation looks a lot different than the Perl
implementation: in Perl, a trie is much easier implemented, while we already have a pretty neat hashmap implementation in C that we use for the purpose of storing (not necessarily unique) prefixes.

The idea: for each item that we add, we generate prefixes starting with the first letter, then the first two letters, then three, etc, until we find a prefix that is unique (or until the prefix length would be longer than we want). If we encounter a previously-unique prefix on the way, we adjust that item's prefix to make it unique again (or we mark it as having no unique prefix if we failed to find one). These partial prefixes are stored in a hash map (for quick lookup times).

To make sure that this function works as expected, we add a test using a special-purpose test helper that was added for that purpose.

Note: We expect the list of prefix items to be passed in as a list of pointers rather than as regular list to avoid having to copy information (the actual items will most likely contain more information than just the name and the length of the unique prefix, but passing in struct prefix_item * would not allow for that).

prefix-map.c new file +38 -0
new file mode 100644
index 0000000000..747ddb4ebc
@@ -0,0 +1,109 @@
+#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 = { { NULL } }, *e2;
Quoted 8 lines
Quoted 8 lines

prefix-map.c line 46

Quoted 8 lines
prefix-map.c new file +8 -0
+	int j;
+
+	e.item = item;
+	e.name = item->name;
+
+	for (j = prefix_map->min_length;
+	     j <= prefix_map->max_length && e.name[j]; j++) {
+		/* Avoid breaking UTF-8 multi-byte sequences */

Ok, that's a good idea, but...

Quoted 2 lines
Quoted 2 lines

prefix-map.c line 48

Quoted 2 lines
prefix-map.c new file +2 -0
+		if (!isascii(e.name[j]))
+			break;

This doesn't seem right. It just breaks the loop, potentially leaving 'item->prefix_length' set from the previous iteration. Try the two strings "123" and "12ő", in this order: they both get prefix_length = 3, i.e. the prefix ends between the two bytes of the multi-byte character.

Furthermore, neither the docstring of find_unique_prefixes() nor the commit message mention what is supposed to happen with paths with multi-byte characters. I think at least one of them should.

Quoted 70 lines
Quoted 58 lines
Quoted 58 lines
prefix-map.c new file +58 -0
+
+		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 at this stage */
+			item->prefix_length = j;
+			add_prefix_entry(&prefix_map->map, e.name, j, item);
+			break;
+		}
+
+		if (!e2->item)
+			continue; /* non-unique prefix */
+
+		if (j != e2->item->prefix_length || memcmp(e.name, e2->name, j))
+			BUG("unexpected prefix length: %d != %d (%s != %s)",
+			    j, (int)e2->item->prefix_length, e.name, e2->name);
+
+		/* 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;
+
+		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++)
Quoted 8 lines

prefix-map.c line 109

Quoted 3 lines
prefix-map.c new file +3 -0
+		add_prefix_item(&prefix_map, list[i]);
+	hashmap_free(&prefix_map.map, 1);
+}

Between the commit message, the in-code comment, the names of the new files, and implementation I was left somewhat confused about what this is about and how it works. TBH, I didn't even try to understand how all the above works, in particular the add_prefix_item() function.

Let me try to explain it here, and maybe you can help me by suggesting an improved commit message and/or code comments?

Quoted 8 lines

The problem is this: given a set of items with labels (e.g. file names), find, for each item, the unique prefix that identifies it. Example:
given the files hello.txt, heaven.txt and hell.txt, the items' unique prefixes would be hello, hea and hell., respectively.

In git add -i, we actually only want to allow alphanumerical prefixes, and we also want at least one, and at most three characters, so only the second item would have an admissible unique prefix: hea.

You say "at most three characters", but I notive that the call added to 'add-interactive.c' in a later commit specifies 4.

I wonder what's the reason for the minimum prefix length, though. I mean, you can't really have a unique prefix shorter than one character...

Quoted 11 lines
Quoted 8 lines

However, I think it would be much-much simpler to first sort (a copy of?) the array of prefix item pointers based on their 'name' field, and then look for a unique prefix in each neighboring pair. Perhaps it would even be faster, because it doesn't have to allocate a bunch of hashmap items, though I don't think that it matters much in practice (i.e. I expect the number of items to be fairly small; presumably nobody will run interactive add after a mass refactoring modifying thousands of files).

The time complexity of the sorted list would be O(n*log(n)), while the hashmap-based complexity would be an amortized O(n).

Oh, so you don't know how/why it works, either!? ;)

This hashmap-based implementation calls hashmap_get() approx. n * (max_length - min_length) times.

Furthermore, depending on the strings and 'max_length', it can call add_prefix_entry() more than n times. E.g. the six strings "a1", "a2", "ab1", "ab2", "abc1", "abc2", in this order, and max_length=4 result in 9 add_prefix_entry() calls. And each add_prefix_entry() call involves a memory allocation.

Ok, that's theory, let's see practice.

For a (very) quick'n'dirty performance test I replaced those five items in 't/helper/test-prefix-map.c' with the output of:

  git -C ..../webkit.git/ ls-tree -r --name-only HEAD | sed -e 's/^/{ "/; s/$/" },/'

That's 280734 real paths. Then a simple 'time test-tool prefix-map' with the hashmap-based implementation ran for 0.055s, while using my PoC sort-based algorithm it ran for 0.063s. So while the hashmap-based implementation is indeed faster, they are both well below 0.1s, so in the context of 'git add -i' the performance difference between the two approaches doesn't really matter.

If we look beyond 'git add -i's requirements, and specify larger max_length values, the hashmap-based implementation gets progressively slower. Already at max_length=5 it's a tad slower than the sort-based implementation, and at 9 it takes twice as long.

The current hashmap-based implementation is ~50% more lines than my sort-based PoC. I'll send it out in a minute.

Quoted 42 lines

And yes, you would not _want_ to run interactive add after a mass refactoring. But it happens. It happens to me more times than I care to admit. And you know what? I really appreciate that even the Perl version is relatively snappy in those circumstances.

Quoted 28 lines
Quoted 6 lines
prefix-map.h new file +0 -0
new file mode 100644
index 0000000000..ce3b8a4a32

prefix-map.h line 9

Quoted 9 lines
prefix-map.h new file +8 -0
@@ -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;
+};

This struct is part of find_unique_prefixes()'s signature, good.

Quoted 10 lines
prefix-map.h new file +10 -0
+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;
Quoted 8 lines

prefix-map.h line 22

Quoted 2 lines
prefix-map.h new file +2 -0
+	int min_length, max_length;
+};

However, neither of these two structs nor the hashmap appear in the function's signature, but are all implementation details. Therefore, they should not be defined and included here in the header but in the .c source file. (But as mentioned above, I think this could be implemented much simpler without these data structures.)

Quoted 8 lines

Right you are!

Quoted 4 lines

Furthermore, this is not a map.
A map, in general, is a container of key-value pairs that allows efficient insertion, removal and lookup. This so-called prefix_map does none of that, so it should not be called a map.

What would you call it instead?

Well, I would rather get rid of it in the first place :)

In my PoC I called the files 'unique-prefix.{c,h}'.

Quoted 6 lines
Quoted 5 lines
Quoted 2 lines

+/*
+ * Find unique prefixes in a given list of strings.

... and stores the length of the unique prefixes in the 'prefix_length' field of the elements of the given array.

Quoted 6 lines

Good idea. I changed it to also explain what is meant by "unique prefix":

* Given a list of names, find unique prefixes (i.e. the first <n> characters
* that uniquely identify the names) and store the lengths of the unique
* prefixes in the 'prefix_length' field of the elements of the given array..

Sounds good.

Quoted 11 lines
Quoted 11 lines
Quoted 11 lines

+ *
+ * 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.

I ran into cases where the hashmap-based implementation didn't set 'prefix_length' to 0 when it, in my opinion, should have, but left it untouched. Just try any string shorter than 'min_length'.

May or may not be related: try "ab", "ab", "abc", in this order: both "ab" get their 'prefix_length' set to 0, as they should, but the 'prefix_length' of "abc" is left untouched, although it should be set to 3.

Hmm, now that I've mentioned "in this order" three times already:
another benefit of the sort-based approach is that the order of elements doesn't matter.

Quoted 69 lines
Quoted 4 lines
Quoted 2 lines

+ */
+void find_unique_prefixes(struct prefix_item **list, size_t nr,

The first argument is not a list but an array.

Indeed.

Quoted 4 lines

prefix-map.h line 38

Quoted 1 line
prefix-map.h new file +1 -0
+			  int min_length, int max_length);

size_t, perhaps? These are closely related to 'prefix_item.prefix_length', which is already (rightfully) size_t.

True.

Quoted 45 lines
Quoted 31 lines
prefix-map.h new file +1 -0
+#endif
t/helper/test-prefix-map.c new file +24 -0
new file mode 100644
index 0000000000..3f1c90eaf0
@@ -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++;
+}
+

t/helper/test-prefix-map.c line 34

Quoted 10 lines
t/helper/test-prefix-map.c new file +10 -0
+#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] = {

You don't have to tell the compiler how many elements this array will contain, it will figure that out on its own.

Quoted 9 lines

t/helper/test-prefix-map.c line 41

Quoted 7 lines
t/helper/test-prefix-map.c new file +7 -0
+		{ "unique" },
+		{ "hell" },
+		{ "hello" },
+		{ "wok" },
+		{ "world" },
+	};
+	struct prefix_item *list[NR] = {

Likewise.

Quoted 6 lines

That is correct.

What the compiler _cannot_ figure out, on its own, however, is that items and list _need_ to contain the same number of items.

Hence the need for NR.

OK, that's a good point. I'd still like to get rid of that NR with something like this instead:

  struct prefix_item items[] = { .... };
  struct prefix_item *list[ARRAY_SIZE(items)];
  for (i = 0; i < ARRAY_SIZE(items); i++) {
      list[i] = &items[i];
      list[i]->prefix_length = 12345; /* dummy */
  }
  find_unique_prefixes(list, ARRAY_SIZE(list), 0, 3);

Note the dummy value, to catch cases when 'prefix_length' is left untouched.

This made this test tool more convenient to use, because I didn't have to fiddle with NR and, worse, with those 'items, items+1, items+N'.

It's still a bit PITA (having to re-build each time I change the test vector), though, compared to driving it from stdin and verifying it through its stdout.

Quoted 24 lines
Quoted 8 lines

t/helper/test-prefix-map.c line 45

Quoted 4 lines
t/helper/test-prefix-map.c new file +4 -0
+		items, items + 1, items + 2, items + 3, items + 4
+	};
+
+	find_unique_prefixes(list, NR, 1, 3);

This could be find_unique_prefixes(list, ARRAY_SIZE(list), 1, 3), and then there is no need for that NR macro anymore.

Quoted 13 lines

t/helper/test-prefix-map.c line 58

Quoted 13 lines
t/helper/test-prefix-map.c new file +13 -0
+
+#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;
+}

Thank you for your review!
Dscho

#55 lore raw

PATCH 1/1 A simpler find_unique_prefixes() implementation

Implement find_unique_prefixes() by sorting the prefix items lexicographically and then looking for unique prefixes in adjacent pairs. It's definitely shorter than the current hashmap-based implementation (101 vs 149 lines), subjectively easier to follow, and hopefully less buggy (some of the new elements in the test trigger failure with the hashmap).

TODO: deal multi-byte caracter sequences, make the test tool more
developer-friendly

---
 Makefile                   |   2 +-
 add-interactive.c          |   2 +-
 prefix-map.c               | 109 -------------------------------------
 prefix-map.h               |  40 --------------
 t/helper/test-prefix-map.c |  31 +++++++----
 unique-prefix.c            |  73 +++++++++++++++++++++++++
 unique-prefix.h            |  28 ++++++++++
 7 files changed, 124 insertions(+), 161 deletions(-)
 delete mode 100644 prefix-map.c
 delete mode 100644 prefix-map.h
 create mode 100644 unique-prefix.c
 create mode 100644 unique-prefix.h
Makefile +1 -1
index 545a0e8743..6773041f0d 100644
@@ -944,7 +944,6 @@ 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
@@ -1013,6 +1012,7 @@ LIB_OBJS += transport-helper.o
 LIB_OBJS += tree-diff.o
 LIB_OBJS += tree.o
 LIB_OBJS += tree-walk.o
+LIB_OBJS += unique-prefix.o
 LIB_OBJS += unpack-trees.o
 LIB_OBJS += upload-pack.o
 LIB_OBJS += url.o
add-interactive.c +1 -1
index c431c72e3f..cf22749b81 100644
@@ -5,7 +5,7 @@
 #include "diffcore.h"
 #include "revision.h"
 #include "refs.h"
-#include "prefix-map.h"
+#include "unique-prefix.h"
 
 struct add_i_state {
 	struct repository *r;
prefix-map.c deleted +0 -109

109 lines not on this page · raw message

prefix-map.h deleted +0 -40
deleted file mode 100644
index ce3b8a4a32..0000000000
@@ -1,40 +0,0 @@
-#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
t/helper/test-prefix-map.c +21 -10
index 3f1c90eaf0..d472ff5dc6 100644
@@ -1,6 +1,5 @@
 #include "test-tool.h"
-#include "cache.h"
-#include "prefix-map.h"
+#include "unique-prefix.h"
 
 static size_t test_count, failed_count;
 
@@ -24,25 +23,32 @@ static void check(int succeeded, const char *file, size_t line_no,
 
 #define EXPECT_SIZE_T_EQUALS(expect, actual, hint) \
 	check(expect == actual, __FILE__, __LINE__, \
-	      "size_t's do not match: %" \
-	      PRIdMAX " != %" PRIdMAX " (%s) (%s)", \
+	      "size_t's do not match: expected: %" \
+	      PRIdMAX " got: %" 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] = {
+	size_t i;
+	struct prefix_item items[] = {
 		{ "unique" },
 		{ "hell" },
 		{ "hello" },
 		{ "wok" },
 		{ "world" },
+		{ "a" },
+		{ "" },
+		{ "pq" },
+		{ "pq" },
+		{ "pqr" },
 	};
-	struct prefix_item *list[NR] = {
-		items, items + 1, items + 2, items + 3, items + 4
-	};
+	struct prefix_item *list[ARRAY_SIZE(items)];
+	for (i = 0; i < ARRAY_SIZE(items); i++) {
+		list[i] = &items[i];
+		list[i]->prefix_length = 12345; /* dummy */
+	}
 
-	find_unique_prefixes(list, NR, 1, 3);
+	find_unique_prefixes(list, ARRAY_SIZE(list), 1, 3);
 
 #define EXPECT_PREFIX_LENGTH_EQUALS(expect, index) \
 	EXPECT_SIZE_T_EQUALS(expect, list[index]->prefix_length, \
@@ -53,6 +59,11 @@ int cmd__prefix_map(int argc, const char **argv)
 	EXPECT_PREFIX_LENGTH_EQUALS(0, 2);
 	EXPECT_PREFIX_LENGTH_EQUALS(3, 3);
 	EXPECT_PREFIX_LENGTH_EQUALS(3, 4);
+	EXPECT_PREFIX_LENGTH_EQUALS(1, 5);
+	EXPECT_PREFIX_LENGTH_EQUALS(0, 6);
+	EXPECT_PREFIX_LENGTH_EQUALS(0, 7);
+	EXPECT_PREFIX_LENGTH_EQUALS(0, 8);
+	EXPECT_PREFIX_LENGTH_EQUALS(3, 9);
 
 	return !!failed_count;
 }
unique-prefix.c new file +73 -0

73 lines not on this page · raw message

unique-prefix.h new file +28 -0
new file mode 100644
index 0000000000..cbddc29a9a
@@ -0,0 +1,28 @@
+#ifndef UNIQUE_PREFIX_H
+#define UNIQUE_PREFIX_H
+
+struct prefix_item {
+	const char *name;
+	size_t prefix_length;
+};
+
+/*
+ * Find unique prefixes in a given array of strings.
+ * Given a list of names, find unique prefixes (i.e. the first <n> characters
+ * that uniquely identify the names) and store the lengths of the unique
+ * prefixes in the 'prefix_length' field of the elements of the given array.
+ *
+ * Typically, the `struct prefix_item` information will be a field in the
+ * actual item struct; For this reason, the `items` parameter is specified
+ * as an array 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 **items, size_t nr,
+			  int min_length, int max_length);
+
+#endif
Signature · 1 line
2.23.0.331.g4e51dcdf11
#56 lore raw

Re: [PATCH] [PoC] A simpler find_unique_prefixes() implementation

Hi Gábor,

Quoted 9 lines · SZEDER Gábor

Implement find_unique_prefixes() by sorting the prefix items lexicographically and then looking for unique prefixes in adjacent pairs. It's definitely shorter than the current hashmap-based implementation (101 vs 149 lines), subjectively easier to follow, and hopefully less buggy (some of the new elements in the test trigger failure with the hashmap).

TODO: deal multi-byte caracter sequences, make the test tool more
developer-friendly

Sorry for my late response, I am still trying to catch up with an ever-growing inbox after trying to pretend to be on vacation.

Your argument is a strong one, and I will try to re-roll, folding the functionality to find the unique prefixes into add-interactive.c while trying to reuse the already-sorted list for the lookup by prefix.

As to the multi-byte UTF-8 characters: you're right, that code has a bug. The intention was to require only single-byte characters in the prefix, for simplicity.

Thanks,
Dscho

#57 lore raw

PATCH v3 8/11 built-in add -i: show unique prefixes of the commands

In reply to Johannes Schindelin via GitGitGadget

From: Slavica Djukic <[email protected]>

Just like in the Perl script git-add--interactive.perl, for each command a unique prefix is determined (if there exists any within the given parameters), and shown in the list, and accepted as a shortcut for the command.

We use the prefix map implementation that we just added in the previous commit for that purpose.

---
 add-interactive.c | 69 ++++++++++++++++++++++++++++++++++++++---------
 1 file changed, 56 insertions(+), 13 deletions(-)
add-interactive.c +56 -13
index 0ea4f3edb8..7ff87bae1b 100644
@@ -5,6 +5,7 @@
 #include "diffcore.h"
 #include "revision.h"
 #include "refs.h"
+#include "prefix-map.h"
 
 struct add_i_state {
 	struct repository *r;
@@ -46,18 +47,32 @@ static int init_add_i_state(struct repository *r, struct add_i_state *s)
 	return 0;
 }
 
-struct item {
-	const char *name;
-};
+static ssize_t find_unique(const char *string,
+			   struct prefix_item **list, size_t nr)
+{
+	ssize_t found = -1, i;
+
+	for (i = 0; i < nr; i++) {
+		struct prefix_item *item = list[i];
+		if (!starts_with(item->name, string))
+			continue;
+		if (found >= 0)
+			return -1;
+		found = i;
+	}
+
+	return found;
+}
 
 struct list_options {
 	int columns;
 	const char *header;
-	void (*print_item)(int i, struct item *item, void *print_item_data);
+	void (*print_item)(int i, struct prefix_item *item,
+			   void *print_item_data);
 	void *print_item_data;
 };
 
-static void list(struct item **list, size_t nr,
+static void list(struct prefix_item **list, size_t nr,
 		 struct add_i_state *s, struct list_options *opts)
 {
 	int i, last_lf = 0;
@@ -100,13 +115,15 @@ struct list_and_choose_options {
  * If an error occurred, returns `LIST_AND_CHOOSE_ERROR`. Upon EOF,
  * `LIST_AND_CHOOSE_QUIT` is returned.
  */
-static ssize_t list_and_choose(struct item **items, size_t nr,
+static ssize_t list_and_choose(struct prefix_item **items, size_t nr,
 			       struct add_i_state *s,
 			       struct list_and_choose_options *opts)
 {
 	struct strbuf input = STRBUF_INIT;
 	ssize_t res = LIST_AND_CHOOSE_ERROR;
 
+	find_unique_prefixes(items, nr, 1, 4);
+
 	for (;;) {
 		char *p, *endp;
 
@@ -146,6 +163,9 @@ static ssize_t list_and_choose(struct item **items, size_t nr,
 			}
 
 			p[sep] = '\0';
+			if (index < 0)
+				index = find_unique(p, items, nr);
+
 			if (index < 0 || index >= nr)
 				printf(_("Huh (%s)?\n"), p);
 			else {
@@ -171,7 +191,7 @@ struct adddel {
 
 struct file_list {
 	struct file_item {
-		struct item item;
+		struct prefix_item item;
 		struct adddel index, worktree;
 	} **file;
 	size_t nr, alloc;
@@ -337,12 +357,29 @@ static void populate_wi_changes(struct strbuf *buf,
 		strbuf_addstr(buf, no_changes);
 }
 
+/* filters out prefixes which have special meaning to list_and_choose() */
+static int is_valid_prefix(const char *prefix, size_t prefix_len)
+{
+	return prefix_len && prefix &&
+		/*
+		 * We expect `prefix` to be NUL terminated, therefore this
+		 * `strcspn()` call is okay, even if it might do much more
+		 * work than strictly necessary.
+		 */
+		strcspn(prefix, " \t\r\n,") >= prefix_len &&	/* separators */
+		*prefix != '-' &&				/* deselection */
+		!isdigit(*prefix) &&				/* selection */
+		(prefix_len != 1 ||
+		 (*prefix != '*' &&				/* "all" wildcard */
+		  *prefix != '?'));				/* prompt help */
+}
+
 struct print_file_item_data {
 	const char *modified_fmt;
 	struct strbuf buf, index, worktree;
 };
 
-static void print_file_item(int i, struct item *item,
+static void print_file_item(int i, struct prefix_item *item,
 			    void *print_file_item_data)
 {
 	struct file_item *c = (struct file_item *)item;
@@ -369,20 +406,26 @@ static int run_status(struct add_i_state *s, const struct pathspec *ps,
 		return -1;
 
 	if (files->nr)
-		list((struct item **)files->file, files->nr, s, opts);
+		list((struct prefix_item **)files->file, files->nr, s, opts);
 	putchar('\n');
 
 	return 0;
 }
 
-static void print_command_item(int i, struct item *item,
+static void print_command_item(int i, struct prefix_item *item,
 			       void *print_command_item_data)
 {
-	printf(" %2d: %s", i + 1, item->name);
+	if (!item->prefix_length ||
+	    !is_valid_prefix(item->name, item->prefix_length))
+		printf(" %2d: %s", i + 1, item->name);
+	else
+		printf(" %3d: [%.*s]%s", i + 1,
+		       (int)item->prefix_length, item->name,
+		       item->name + item->prefix_length);
 }
 
 struct command_item {
-	struct item item;
+	struct prefix_item item;
 	int (*command)(struct add_i_state *s, const struct pathspec *ps,
 		       struct file_list *files, struct list_options *opts);
 };
@@ -424,7 +467,7 @@ int run_add_i(struct repository *r, const struct pathspec *ps)
 		res = -1;
 
 	for (;;) {
-		i = list_and_choose((struct item **)commands,
+		i = list_and_choose((struct prefix_item **)commands,
 				    ARRAY_SIZE(commands), &s, &main_loop_opts);
 		if (i == LIST_AND_CHOOSE_QUIT) {
 			printf(_("Bye.\n"));
Signature · 1 line
gitgitgadget
#58 lore raw

PATCH v3 9/11 built-in add -i: support `?` (prompt help)

In reply to Johannes Schindelin via GitGitGadget

From: Johannes Schindelin <[email protected]>

With this change, we print out the same colored help text that the Perl-based git add -i prints in the main loop when question mark is entered.

---
 add-interactive.c | 22 +++++++++++++++++++++-
 1 file changed, 21 insertions(+), 1 deletion(-)
add-interactive.c +21 -1
index 7ff87bae1b..1435c12be9 100644
@@ -11,6 +11,7 @@ struct add_i_state {
 	struct repository *r;
 	int use_color;
 	char header_color[COLOR_MAXLEN];
+	char help_color[COLOR_MAXLEN];
 };
 
 static void init_color(struct repository *r, struct add_i_state *s,
@@ -43,6 +44,7 @@ static int init_add_i_state(struct repository *r, struct add_i_state *s)
 	s->use_color = want_color(s->use_color);
 
 	init_color(r, s, "header", s->header_color, GIT_COLOR_BOLD);
+	init_color(r, s, "help", s->help_color, GIT_COLOR_BOLD_RED);
 
 	return 0;
 }
@@ -104,6 +106,7 @@ struct list_and_choose_options {
 	struct list_options list_opts;
 
 	const char *prompt;
+	void (*print_help)(struct add_i_state *s);
 };
 
 #define LIST_AND_CHOOSE_ERROR (-1)
@@ -144,6 +147,11 @@ static ssize_t list_and_choose(struct prefix_item **items, size_t nr,
 		if (!input.len)
 			break;
 
+		if (!strcmp(input.buf, "?")) {
+			opts->print_help(s);
+			continue;
+		}
+
 		p = input.buf;
 		for (;;) {
 			size_t sep = strcspn(p, " \t\r\n,");
@@ -430,12 +438,24 @@ struct command_item {
 		       struct file_list *files, struct list_options *opts);
 };
 
+static void command_prompt_help(struct add_i_state *s)
+{
+	const char *help_color = s->help_color;
+	color_fprintf_ln(stdout, help_color, "%s", _("Prompt help:"));
+	color_fprintf_ln(stdout, help_color, "1          - %s",
+			 _("select a numbered item"));
+	color_fprintf_ln(stdout, help_color, "foo        - %s",
+			 _("select item based on unique prefix"));
+	color_fprintf_ln(stdout, help_color, "           - %s",
+			 _("(empty) select nothing"));
+}
+
 int run_add_i(struct repository *r, const struct pathspec *ps)
 {
 	struct add_i_state s = { NULL };
 	struct list_and_choose_options main_loop_opts = {
 		{ 4, N_("*** Commands ***"), print_command_item, NULL },
-		N_("What now")
+		N_("What now"), command_prompt_help
 	};
 	struct command_item
 		status = { { "status" }, run_status };
Signature · 1 line
gitgitgadget
#59 lore raw

PATCH v3 10/11 built-in add -i: use color in the main loop

In reply to Johannes Schindelin via GitGitGadget

From: Slavica Djukic <[email protected]>

The error messages as well as the unique prefixes are colored in git add -i by default; We need to do the same in the built-in version.

---
 add-interactive.c | 37 ++++++++++++++++++++++++++++++++-----
 1 file changed, 32 insertions(+), 5 deletions(-)
add-interactive.c +32 -5
index 1435c12be9..538658dfa7 100644
@@ -12,6 +12,9 @@ struct add_i_state {
 	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];
 };
 
 static void init_color(struct repository *r, struct add_i_state *s,
@@ -45,6 +48,9 @@ static int init_add_i_state(struct repository *r, struct add_i_state *s)
 
 	init_color(r, s, "header", s->header_color, GIT_COLOR_BOLD);
 	init_color(r, s, "help", s->help_color, GIT_COLOR_BOLD_RED);
+	init_color(r, s, "prompt", s->prompt_color, GIT_COLOR_BOLD_BLUE);
+	init_color(r, s, "error", s->error_color, GIT_COLOR_BOLD_RED);
+	init_color(r, s, "reset", s->reset_color, GIT_COLOR_RESET);
 
 	return 0;
 }
@@ -134,7 +140,8 @@ static ssize_t list_and_choose(struct prefix_item **items, size_t nr,
 
 		list(items, nr, s, &opts->list_opts);
 
-		printf("%s%s", opts->prompt, "> ");
+		color_fprintf(stdout, s->prompt_color, "%s", opts->prompt);
+		fputs("> ", stdout);
 		fflush(stdout);
 
 		if (strbuf_getline(&input, stdin) == EOF) {
@@ -175,7 +182,8 @@ static ssize_t list_and_choose(struct prefix_item **items, size_t nr,
 				index = find_unique(p, items, nr);
 
 			if (index < 0 || index >= nr)
-				printf(_("Huh (%s)?\n"), p);
+				color_fprintf_ln(stdout, s->error_color,
+						 _("Huh (%s)?"), p);
 			else {
 				res = index;
 				break;
@@ -420,15 +428,21 @@ static int run_status(struct add_i_state *s, const struct pathspec *ps,
 	return 0;
 }
 
+struct print_command_item_data {
+	const char *color, *reset;
+};
+
 static void print_command_item(int i, struct prefix_item *item,
 			       void *print_command_item_data)
 {
+	struct print_command_item_data *d = print_command_item_data;
+
 	if (!item->prefix_length ||
 	    !is_valid_prefix(item->name, item->prefix_length))
 		printf(" %2d: %s", i + 1, item->name);
 	else
-		printf(" %3d: [%.*s]%s", i + 1,
-		       (int)item->prefix_length, item->name,
+		printf(" %2d: %s%.*s%s%s", i + 1,
+		       d->color, (int)item->prefix_length, item->name, d->reset,
 		       item->name + item->prefix_length);
 }
 
@@ -453,8 +467,9 @@ static void command_prompt_help(struct add_i_state *s)
 int run_add_i(struct repository *r, const struct pathspec *ps)
 {
 	struct add_i_state s = { NULL };
+	struct print_command_item_data data;
 	struct list_and_choose_options main_loop_opts = {
-		{ 4, N_("*** Commands ***"), print_command_item, NULL },
+		{ 4, N_("*** Commands ***"), print_command_item, &data },
 		N_("What now"), command_prompt_help
 	};
 	struct command_item
@@ -477,6 +492,18 @@ int run_add_i(struct repository *r, const struct pathspec *ps)
 	if (init_add_i_state(r, &s))
 		return error("could not parse `add -i` config");
 
+	/*
+	 * When color was asked for, use the prompt color for
+	 * highlighting, otherwise use square brackets.
+	 */
+	if (s.use_color) {
+		data.color = s.prompt_color;
+		data.reset = s.reset_color;
+	} else {
+		data.color = "[";
+		data.reset = "]";
+	}
+
 	strbuf_addstr(&header, "      ");
 	strbuf_addf(&header, print_file_item_data.modified_fmt,
 		    _("staged"), _("unstaged"), _("path"));
Signature · 1 line
gitgitgadget
#60 lore raw

PATCH v3 11/11 built-in add -i: implement the `help` command

In reply to Johannes Schindelin via GitGitGadget

From: Johannes Schindelin <[email protected]>

This imitates the code to show the help text from the Perl script git-add--interactive.perl in the built-in version.

To make sure that it renders exactly like the Perl version of git add -i, we also add a test case for that to t3701-add-interactive.sh.

---
 add-interactive.c          | 27 +++++++++++++++++++++++++--
 t/t3701-add-interactive.sh | 25 +++++++++++++++++++++++++
 2 files changed, 50 insertions(+), 2 deletions(-)
add-interactive.c +25 -2
index 538658dfa7..c431c72e3f 100644
@@ -428,6 +428,27 @@ static int run_status(struct add_i_state *s, const struct pathspec *ps,
 	return 0;
 }
 
+static int run_help(struct add_i_state *s, const struct pathspec *ps,
+		    struct file_list *files, struct list_options *opts)
+{
+	const char *help_color = s->help_color;
+
+	color_fprintf_ln(stdout, help_color, "status        - %s",
+			 _("show paths with changes"));
+	color_fprintf_ln(stdout, help_color, "update        - %s",
+			 _("add working tree state to the staged set of changes"));
+	color_fprintf_ln(stdout, help_color, "revert        - %s",
+			 _("revert staged set of changes back to the HEAD version"));
+	color_fprintf_ln(stdout, help_color, "patch         - %s",
+			 _("pick hunks and update selectively"));
+	color_fprintf_ln(stdout, help_color, "diff          - %s",
+			 _("view diff between HEAD and index"));
+	color_fprintf_ln(stdout, help_color, "add untracked - %s",
+			 _("add contents of untracked files to the staged set of changes"));
+
+	return 0;
+}
+
 struct print_command_item_data {
 	const char *color, *reset;
 };
@@ -473,9 +494,11 @@ int run_add_i(struct repository *r, const struct pathspec *ps)
 		N_("What now"), command_prompt_help
 	};
 	struct command_item
-		status = { { "status" }, run_status };
+		status = { { "status" }, run_status },
+		help = { { "help" }, run_help };
 	struct command_item *commands[] = {
-		&status
+		&status,
+		&help
 	};
 
 	struct print_file_item_data print_file_item_data = {
t/t3701-add-interactive.sh +25 -0
index 69991a3168..cf67756b85 100755
@@ -647,4 +647,29 @@ test_expect_success 'checkout -p works with pathological context lines' '
 	test_write_lines a b a b a a b a b a >expect &&
 	test_cmp expect a
 '
+
+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 | GIT_PAGER_IN_USE=true TERM=vt100 git add -i >actual.colored &&
+	test_decode_color <actual.colored >actual &&
+	test_i18ncmp expect actual
+'
+
 test_done
Signature · 1 line
gitgitgadget
#61 lore raw

Re: [PATCH v3 11/11] built-in add -i: implement the `help` command

"Johannes Schindelin via GitGitGadget" <[email protected]>

Quoted 7 lines · writes:
add-interactive.c +7 -0
+static int run_help(struct add_i_state *s, const struct pathspec *ps,
+		    struct file_list *files, struct list_options *opts)
+{
+	const char *help_color = s->help_color;
+
+	color_fprintf_ln(stdout, help_color, "status        - %s",
+			 _("show paths with changes"));

add-interactive.c static int run_status(struct add_i_state *s, const struct pathspec *ps, · line 447

Quoted 10 lines
add-interactive.c +10 -0
+	color_fprintf_ln(stdout, help_color, "update        - %s",
+			 _("add working tree state to the staged set of changes"));
+	color_fprintf_ln(stdout, help_color, "revert        - %s",
+			 _("revert staged set of changes back to the HEAD version"));
+	color_fprintf_ln(stdout, help_color, "patch         - %s",
+			 _("pick hunks and update selectively"));
+	color_fprintf_ln(stdout, help_color, "diff          - %s",
+			 _("view diff between HEAD and index"));
+	color_fprintf_ln(stdout, help_color, "add untracked - %s",
+			 _("add contents of untracked files to the staged set of changes"));

As we do not allow the command names to get translated, this makes sense.

Have we adopted the convention to name callback parameters that have to stay unused (because the callback function must have a function signature that accepts the union of what everybody needs to take) differently from the parameters that actually get used? It may make sense to use it in a function like this, to prevent readers from wasting time by wondering how pathspec is used in this function, for example.

Quoted 22 lines
t/t3701-add-interactive.sh +14 -0
index 69991a3168..cf67756b85 100755
@@ -647,4 +647,29 @@ test_expect_success 'checkout -p works with pathological context lines' '
 	test_write_lines a b a b a a b a b a >expect &&
 	test_cmp expect a
 '
+
+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>

t/t3701-add-interactive.sh test_expect_success 'checkout -p works with pathological context lines' ' · line 673

Quoted 10 lines
t/t3701-add-interactive.sh +10 -0
+	<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 | GIT_PAGER_IN_USE=true TERM=vt100 git add -i >actual.colored &&
+	test_decode_color <actual.colored >actual &&
+	test_i18ncmp expect actual
+'

Nicely done.

#62 lore raw

Re: [PATCH v3 11/11] built-in add -i: implement the `help` command

Quoted 7 lines · Junio C Hamano

add-interactive.c static int run_status(struct add_i_state *s, const struct pathspec *ps, · line 432

Quoted 2 lines
add-interactive.c +2 -0
+static int run_help(struct add_i_state *s, const struct pathspec *ps,
+		    struct file_list *files, struct list_options *opts)

[...]

As we do not allow the command names to get translated, this makes sense.

Quoted 7 lines

Have we adopted the convention to name callback parameters that have to stay unused (because the callback function must have a function signature that accepts the union of what everybody needs to take) differently from the parameters that actually get used? It may make sense to use it in a function like this, to prevent readers from wasting time by wondering how pathspec is used in this function, for example.

I haven't yet[1] polished up the remainder of my patches to make us -Wunused-parameter clean, but the pattern there would look like:

  void some_function(const char *foo, void *UNUSED(bar))
  {
     ... use foo but not bar ...
  }

which both tells the compiler that "bar" may be unused, and renames it behind the scenes to unused_bar so that it cannot be accidentally used (or more importantly, so that we can drop the annotation when it does get used).

All of which is to say that I'm fine if you call it "unused_bar" manually for now, but I'd switch it to the above in my series. So it may not matter all that much in the meantime.

-Peff

[1] The sticking point is a few more cases I found where it's unclear to me whether they should be marked, or if it's a latent bug.

#63 lore raw
In reply to Johannes Schindelin via GitGitGadget

Hi Junio,

Quoted 25 lines · Johannes Schindelin via GitGitGadget

This is the first leg on the long journey to a fully built-in git add -i (next up: parts 2 [https://github.com/gitgitgadget/git/pull/171], 3
[https://github.com/gitgitgadget/git/pull/172], 4
[https://github.com/gitgitgadget/git/pull/173], 5
[https://github.com/gitgitgadget/git/pull/174], and 6
[https://github.com/gitgitgadget/git/pull/175]). Note: the latter PRs are not necessarily up to date, and will be re-targeted to the appropriate branches in https://github.com/gitster/git as soon as Junio picks them up.

This here patch series reflects the part that was submitted a couple of times (see https://github.com/gitgitgadget/git/pull/103) during the Outreachy project by Slavica Ðukic that continued the journey based on an initial patch series by Daniel Ferreira.

It only implements the status and the help part, in the interest of making the review remotely more reviewable.

As I am a heavy user of git add -p myself and use a patched version for weeks already (it is so nice to not suffer over one second startup until the MSYS2 Perl finally shows me anything, instead it feels instantaneous), I integrated these patch series into Git for Windows' master already, as an opt-in feature guarded by the config variable add.interactive.useBuiltin (and Git for Windows' installer is prepared to detect this version and offer the option in the graphical user interface).

Quoted 2 lines

I had planned on submitting this before v2.22.0-rc0, but there was such a backlog of builds from a big pushout that I had to wait ;-)

FWIW the v2 round has not seen any objections...

Maybe pick it up in pu this time round?

Thanks,
Dscho

#64 lore raw
Quoted 7 lines · Johannes Schindelin
Quoted 7 lines

As I am a heavy user of git add -p myself and use a patched version for weeks already (it is so nice to not suffer over one second startup until the MSYS2 Perl finally shows me anything, instead it feels instantaneous), I integrated these patch series into Git for Windows' master already, as an opt-in feature guarded by the config variable add.interactive.useBuiltin (and Git for Windows' installer is prepared to detect this version and offer the option in the graphical user interface).

I've sent comments on a few patches in the series; overall it was a pleasant read.

Thanks.

#65 lore raw

v4 cover git add -i: add a rudimentary version in C (supporting only status and help so far)

In reply to Johannes Schindelin via GitGitGadget

This is the first leg on the long journey to a fully built-in git add -i (next up: parts 2 [https://github.com/gitgitgadget/git/pull/171], 3
[https://github.com/gitgitgadget/git/pull/172], 4
[https://github.com/gitgitgadget/git/pull/173], 5
[https://github.com/gitgitgadget/git/pull/174], and 6
[https://github.com/gitgitgadget/git/pull/175]). Note: the latter PRs are not necessarily up to date, and will be re-targeted to the appropriate branches in https://github.com/gitster/git as soon as Junio picks them up.

This here patch series reflects the part that was submitted a couple of times (see https://github.com/gitgitgadget/git/pull/103) during the Outreachy project by Slavica Ðukic that continued the journey based on an initial patch series by Daniel Ferreira.

It only implements the status and the help part, in the interest of making the review remotely more reviewable.

As I am a heavy user of git add -p myself and use a patched version for weeks already (it is so nice to not suffer over one second startup until the MSYS2 Perl finally shows me anything, instead it feels instantaneous), I integrated these patch series into Git for Windows' master already, as an opt-in feature guarded by the config variable add.interactive.useBuiltin (and Git for Windows' installer is prepared to detect this version and offer the option in the graphical user interface).

I had planned on submitting this before v2.22.0-rc0, but there was such a backlog of builds from a big pushout that I had to wait ;-)

Changes since v3:

* Rebased to v2.23.0 to reduce friction.
* free_diffstat_info() is now made public as well, and used, to avoid a memory leak.
* Prepared the patches for ew/hashmap (which is strict about the hashmap entries' type in hashmap_entry_init() and friends).
* The private data types have been moved from prefix-map.h to prefix-map.c.
* A lot of int types were converted to more appropriate size_t in prefix-map.c.
* A misleading parameter name list was renamed to the correct array.
* The code comment above find_unique_prefixes() was (hopefully) improved.
* The run_help() function's signature now reflects that most of the parameters are actually unused.

Changes since v2:

* Rebased to master to avoid merge conflicts.
* Renumbered the prefix-map test to avoid conflicts with two patch series that are currently in-flight in pu.

Changes since v1:

* The config machinery was reworked completely, to not use a callback to git_config(), but instead to query the config via the repo_config_get_*() functions. This also prevents a future "Huh???" moment: the internal add --interactive API accepts a parameter of type struct repository *r, but the previous configuration did not use that to query the config (and could in the future be a repository other than the_repository).

* As a consequence, the color sequences are no longer stored in file-local variables, but passed around via a struct.

* Instead of using the magical constant -2 to quit the main loop, it is now defined as LIST_AND_CHOOSE_QUIT (and likewise, LIST_AND_CHOOSE_ERROR is defined as -1 and used where appropriate).

* Improved the add_prefix_item() function by avoiding buffer overruns, not reusing the struct that is used for lookup also for adding the new item, and by strengthening the bug check.

Daniel Ferreira (2):
diff: export diffstat interface
built-in add -i: implement the status command

Johannes Schindelin (6):
Start to implement a built-in version of git add --interactive built-in add -i: refresh the index before running status built-in add -i: color the header in the status command built-in add -i: implement the main loop
built-in add -i: support ? (prompt help)
built-in add -i: implement the help command

Slavica Djukic (3):
Add a function to determine unique prefixes for a list of strings built-in add -i: show unique prefixes of the commands built-in add -i: use color in the main loop

 Documentation/config/add.txt |   5 +
 Makefile                     |   3 +
 add-interactive.c            | 558 +++++++++++++++++++++++++++++++++++
 add-interactive.h            |   8 +
 builtin/add.c                |  10 +
 diff.c                       |  39 +--
 diff.h                       |  20 ++
 prefix-map.c                 | 123 ++++++++
 prefix-map.h                 |  29 ++
 repository.c                 |  19 ++
 repository.h                 |   7 +
 t/README                     |   4 +
 t/helper/test-prefix-map.c   |  58 ++++
 t/helper/test-tool.c         |   1 +
 t/helper/test-tool.h         |   1 +
 t/t0018-prefix-map.sh        |  10 +
 t/t3701-add-interactive.sh   |  25 ++
 17 files changed, 897 insertions(+), 23 deletions(-)
 create mode 100644 add-interactive.c
 create mode 100644 add-interactive.h
 create mode 100644 prefix-map.c
 create mode 100644 prefix-map.h
 create mode 100644 t/helper/test-prefix-map.c
 create mode 100755 t/t0018-prefix-map.sh

base-commit: 5fa0f5238b0cd46cfe7f6fa76c3f526ea98148d9
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-170%2Fdscho%2Fadd-i-in-c-status-and-help-v4
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-170/dscho/add-i-in-c-status-and-help-v4
Pull-Request: https://github.com/gitgitgadget/git/pull/170

Range-diff vs v3:

  1:  0a5ec9345d =  1:  ad8752eca7 Start to implement a built-in version of `git add --interactive`
  2:  c7a377890d !  2:  38cc04c1d9 diff: export diffstat interface
     @@ -41,6 +41,15 @@
       static struct diffstat_file *diffstat_add(struct diffstat_t *diffstat,
       					  const char *name_a,
       					  const char *name_b)
     +@@
     + 	gather_dirstat(options, &dir, changed, "", 0);
     + }
     + 
     +-static void free_diffstat_info(struct diffstat_t *diffstat)
     ++void free_diffstat_info(struct diffstat_t *diffstat)
     + {
     + 	int i;
     + 	for (i = 0; i < diffstat->nr; i++) {
      @@
       	    dirstat_by_line) {
       		struct diffstat_t diffstat;
     @@ -109,6 +118,7 @@
       
      +void compute_diffstat(struct diff_options *options, struct diffstat_t *diffstat,
      +		      struct diff_queue_struct *q);
     ++void free_diffstat_info(struct diffstat_t *diffstat);
      +
       #define DIFF_SETUP_REVERSE      	1
       #define DIFF_SETUP_USE_SIZE_CACHE	4
  3:  b93b055ebe !  3:  ee3e40293c built-in add -i: implement the `status` command
     @@ -158,9 +158,9 @@
      +			file_index = entry->index;
      +		else {
      +			FLEX_ALLOC_STR(entry, pathname, name);
     -+			hashmap_entry_init(entry, hash);
     ++			hashmap_entry_init(&entry->ent, hash);
      +			entry->index = file_index = s->list->nr;
     -+			hashmap_add(&s->file_map, entry);
     ++			hashmap_add(&s->file_map, &entry->ent);
      +
      +			add_file_item(s->list, name);
      +		}
     @@ -173,6 +173,7 @@
      +		if (stat.files[i]->is_binary)
      +			adddel->binary = 1;
      +	}
     ++	free_diffstat_info(&stat);
      +}
      +
      +static int get_modified_files(struct repository *r, struct file_list *list,
  4:  daff24074a =  4:  3c855d9fa5 built-in add -i: refresh the index before running `status`
  5:  15f18f5b3e =  5:  24737a09f7 built-in add -i: color the header in the `status` command
  6:  175409aaae =  6:  ac67731cf1 built-in add -i: implement the main loop
  7:  3000d7d08d !  7:  c5a699b6b2 Add a function to determine unique prefixes for a list of strings
     @@ -19,7 +19,7 @@
          prefixes are stored in a hash map (for quick lookup times).
      
          To make sure that this function works as expected, we add a test using a
     -    special-purpose test helper that was added for that purpose.
     +    special-purpose test helper.
      
          Note: We expect the list of prefix items to be passed in as a list of
          pointers rather than as regular list to avoid having to copy information
     @@ -58,6 +58,19 @@
      +#include "cache.h"
      +#include "prefix-map.h"
      +
     ++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;
     ++	size_t min_length, max_length;
     ++};
     ++
      +static int map_cmp(const void *unused_cmp_data,
      +		   const void *entry,
      +		   const void *entry_or_key,
     @@ -77,12 +90,12 @@
      +	result->name = name;
      +	result->prefix_length = prefix_length;
      +	result->item = item;
     -+	hashmap_entry_init(result, memhash(name, prefix_length));
     -+	hashmap_add(map, result);
     ++	hashmap_entry_init(&result->e, memhash(name, prefix_length));
     ++	hashmap_add(map, &result->e);
      +}
      +
      +static void init_prefix_map(struct prefix_map *prefix_map,
     -+			    int min_prefix_length, int max_prefix_length)
     ++			    size_t min_prefix_length, size_t max_prefix_length)
      +{
      +	hashmap_init(&prefix_map->map, map_cmp, NULL, 0);
      +	prefix_map->min_length = min_prefix_length;
     @@ -93,7 +106,7 @@
      +			    struct prefix_item *item)
      +{
      +	struct prefix_map_entry e = { { NULL } }, *e2;
     -+	int j;
     ++	size_t j;
      +
      +	e.item = item;
      +	e.name = item->name;
     @@ -105,8 +118,8 @@
      +			break;
      +
      +		e.prefix_length = j;
     -+		hashmap_entry_init(&e, memhash(e.name, j));
     -+		e2 = hashmap_get(&prefix_map->map, &e, NULL);
     ++		hashmap_entry_init(&e.e, memhash(e.name, j));
     ++		e2 = hashmap_get(&prefix_map->map, &e.e, NULL);
      +		if (!e2) {
      +			/* prefix is unique at this stage */
      +			item->prefix_length = j;
     @@ -119,7 +132,8 @@
      +
      +		if (j != e2->item->prefix_length || memcmp(e.name, e2->name, j))
      +			BUG("unexpected prefix length: %d != %d (%s != %s)",
     -+			    j, (int)e2->item->prefix_length, e.name, e2->name);
     ++			    (int)j, (int)e2->item->prefix_length,
     ++			    e.name, e2->name);
      +
      +		/* skip common prefix */
      +		for (; j < prefix_map->max_length && e.name[j]; j++) {
     @@ -153,15 +167,15 @@
      +	}
      +}
      +
     -+void find_unique_prefixes(struct prefix_item **list, size_t nr,
     -+			  int min_length, int max_length)
     ++void find_unique_prefixes(struct prefix_item **array, size_t nr,
     ++			  size_t min_length, size_t max_length)
      +{
     -+	int i;
     ++	size_t 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]);
     ++		add_prefix_item(&prefix_map, array[i]);
      +	hashmap_free(&prefix_map.map, 1);
      +}
      
     @@ -180,25 +194,14 @@
      +	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.
     ++ * Given an array of names, find unique prefixes (i.e. the first <n> characters
     ++ * that uniquely identify the names) and store the lengths of the unique
     ++ * prefixes in the 'prefix_length' field of the elements of the given array..
      + *
     -+ * 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.
     ++ * Typically, the `struct prefix_item` information is a field in the actual
     ++ * item struct; For this reason, the `array` parameter is specified as an array
     ++ * of pointers to the items.
      + *
      + * The `min_length`/`max_length` parameters define what length the unique
      + * prefixes should have.
     @@ -206,8 +209,8 @@
      + * 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);
     ++void find_unique_prefixes(struct prefix_item **array, size_t nr,
     ++			  size_t min_length, size_t max_length);
      +
      +#endif
  8:  e23ddebfbf =  8:  bc7a74f697 built-in add -i: show unique prefixes of the commands
  9:  d8c012fce8 =  9:  74f73e26b4 built-in add -i: support `?` (prompt help)
 10:  8121a3ca1b = 10:  88001009bc built-in add -i: use color in the main loop
 11:  db70c6475d ! 11:  b27fbe289f built-in add -i: implement the `help` command
     @@ -18,22 +18,21 @@
       	return 0;
       }
       
     -+static int run_help(struct add_i_state *s, const struct pathspec *ps,
     -+		    struct file_list *files, struct list_options *opts)
     ++static int run_help(struct add_i_state *s, const struct pathspec *unused_ps,
     ++		    struct file_list *unused_files,
     ++		    struct list_options *unused_opts)
      +{
     -+	const char *help_color = s->help_color;
     -+
     -+	color_fprintf_ln(stdout, help_color, "status        - %s",
     ++	color_fprintf_ln(stdout, s->help_color, "status        - %s",
      +			 _("show paths with changes"));
     -+	color_fprintf_ln(stdout, help_color, "update        - %s",
     ++	color_fprintf_ln(stdout, s->help_color, "update        - %s",
      +			 _("add working tree state to the staged set of changes"));
     -+	color_fprintf_ln(stdout, help_color, "revert        - %s",
     ++	color_fprintf_ln(stdout, s->help_color, "revert        - %s",
      +			 _("revert staged set of changes back to the HEAD version"));
     -+	color_fprintf_ln(stdout, help_color, "patch         - %s",
     ++	color_fprintf_ln(stdout, s->help_color, "patch         - %s",
      +			 _("pick hunks and update selectively"));
     -+	color_fprintf_ln(stdout, help_color, "diff          - %s",
     ++	color_fprintf_ln(stdout, s->help_color, "diff          - %s",
      +			 _("view diff between HEAD and index"));
     -+	color_fprintf_ln(stdout, help_color, "add untracked - %s",
     ++	color_fprintf_ln(stdout, s->help_color, "add untracked - %s",
      +			 _("add contents of untracked files to the staged set of changes"));
      +
      +	return 0;
Signature · 1 line
gitgitgadget
#66 lore raw

PATCH v4 1/11 Start to implement a built-in version of `git add --interactive`

From: Johannes Schindelin <[email protected]>

This is hardly the first conversion of a Git command that is implemented as a script to a built-in. So far, the most successful strategy for such conversions has been to add a built-in helper and call that for more and more functionality from the script, as more and more parts are converted.

With the interactive add, we choose a different strategy. The sole reason for this is that on Windows (where such a conversion has the most benefits in terms of speed and robustness) we face the very specific problem that a system() call in Perl seems to close stdin in the parent process when the spawned process consumes even one character from stdin. And that just does not work for us here, as it would stop the main loop as soon as any interactive command was performed by the helper. Which is almost all of the commands in git add -i.

It is almost as if Perl told us once again that it does not want us to use it on Windows.

Instead, we follow the opposite route where we start with a bare-bones version of the built-in interactive add, guarded by the new add.interactive.useBuiltin config variable, and then add more and more functionality to it, until it is feature complete.

At this point, the built-in version of git add -i only states that it cannot do anything yet ;-)

---
 Documentation/config/add.txt |  5 +++++
 Makefile                     |  1 +
 add-interactive.c            |  7 +++++++
 add-interactive.h            |  8 ++++++++
 builtin/add.c                | 10 ++++++++++
 t/README                     |  4 ++++
 6 files changed, 35 insertions(+)
 create mode 100644 add-interactive.c
 create mode 100644 add-interactive.h
Documentation/config/add.txt +5 -0
index 4d753f006e..c9f748f81c 100644
@@ -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.
Makefile +1 -0
index f9255344ae..d04daf9fd8 100644
@@ -826,6 +826,7 @@ LIB_H := $(sort $(shell git ls-files '*.h' ':!t/' ':!Documentation/' 2>/dev/null
 	-name '*.h' -print))
 
 LIB_OBJS += abspath.o
+LIB_OBJS += add-interactive.o
 LIB_OBJS += advice.o
 LIB_OBJS += alias.o
 LIB_OBJS += alloc.o
add-interactive.c new file +7 -0
new file mode 100644
index 0000000000..482e458dc6
@@ -0,0 +1,7 @@
+#include "cache.h"
+#include "add-interactive.h"
+
+int run_add_i(struct repository *r, const struct pathspec *ps)
+{
+	die(_("No commands are available in the built-in `git add -i` yet!"));
+}
add-interactive.h new file +8 -0
new file mode 100644
index 0000000000..7043b8741d
@@ -0,0 +1,8 @@
+#ifndef ADD_INTERACTIVE_H
+#define ADD_INTERACTIVE_H
+
+struct repository;
+struct pathspec;
+int run_add_i(struct repository *r, const struct pathspec *ps);
+
+#endif
builtin/add.c +10 -0
index dd18e5c9b6..4f625691b5 100644
@@ -20,6 +20,7 @@
 #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>..."),
@@ -185,6 +186,14 @@ int run_add_interactive(const char *revision, const char *patch_mode,
 {
 	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 && !patch_mode)
+		return !!run_add_i(the_repository, pathspec);
 
 	argv_array_push(&argv, "add--interactive");
 	if (patch_mode)
@@ -319,6 +328,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);
 }
 
t/README +4 -0
index 60d5b77bcc..bda93fe603 100644
@@ -397,6 +397,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
Signature · 1 line
gitgitgadget
#67 lore raw

PATCH v4 2/11 diff: export diffstat interface

In reply to Johannes Schindelin via GitGitGadget

From: Daniel Ferreira <[email protected]>

Make the diffstat interface (namely, the diffstat_t struct and compute_diffstat) no longer be internal to diff.c and allow it to be used by other parts of git.

This is helpful for code that may want to easily extract information from files using the diff machinery, while flushing it differently from how the show_* functions used by diff_flush() do it. One example is the builtin implementation of git-add--interactive's status.

---
 diff.c | 39 ++++++++++++++++-----------------------
 diff.h | 20 ++++++++++++++++++++
 2 files changed, 36 insertions(+), 23 deletions(-)
diff.c +16 -23
index efe42b341a..6829a74065 100644
@@ -2492,22 +2492,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)
@@ -3154,7 +3138,7 @@ static void show_dirstat_by_line(struct diffstat_t *data, struct diff_options *o
 	gather_dirstat(options, &dir, changed, "", 0);
 }
 
-static void free_diffstat_info(struct diffstat_t *diffstat)
+void free_diffstat_info(struct diffstat_t *diffstat)
 {
 	int i;
 	for (i = 0; i < diffstat->nr; i++) {
@@ -6278,12 +6262,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)
@@ -6616,6 +6595,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,
diff.h +20 -0
index c2c3056810..c791ecc972 100644
@@ -245,6 +245,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,
@@ -334,6 +350,10 @@ 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);
+void free_diffstat_info(struct diffstat_t *diffstat);
+
 #define DIFF_SETUP_REVERSE      	1
 #define DIFF_SETUP_USE_SIZE_CACHE	4
 
Signature · 1 line
gitgitgadget
#68 lore raw

PATCH v4 3/11 built-in add -i: implement the `status` command

In reply to Johannes Schindelin via GitGitGadget

From: Daniel Ferreira <[email protected]>

This implements the status command of git add -i. The data structures introduced in this commit will be extended as needed later.

At this point, we re-implement only part of the list_and_choose() function of the Perl script git-add--interactive.perl and call it list(). It does not yet color anything, or do columns, or allow user input.

Over the course of the next commits, we will introduce a list_and_choose() function that uses list() to display the list of options and let the user choose one or more of the displayed items. This will be used to implement the main loop of the built-in git add -i, at which point the new status command can actually be used.

Note that we pass the list of items as a struct item ** as opposed to a struct item *, to allow for the actual items to contain much more information than merely the name.

---
 add-interactive.c | 266 +++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 265 insertions(+), 1 deletion(-)
add-interactive.c +265 -1
index 482e458dc6..d64206ba1c 100644
@@ -1,7 +1,271 @@
 #include "cache.h"
 #include "add-interactive.h"
+#include "diffcore.h"
+#include "revision.h"
+#include "refs.h"
+
+struct item {
+	const char *name;
+};
+
+struct list_options {
+	const char *header;
+	void (*print_item)(int i, struct item *item, void *print_item_data);
+	void *print_item_data;
+};
+
+static void list(struct item **list, size_t nr, struct list_options *opts)
+{
+	int i;
+
+	if (!nr)
+		return;
+
+	if (opts->header)
+		printf("%s\n", opts->header);
+
+	for (i = 0; i < nr; i++) {
+		opts->print_item(i, list[i], opts->print_item_data);
+		putchar('\n');
+	}
+}
+
+struct adddel {
+	uintmax_t add, del;
+	unsigned seen:1, binary:1;
+};
+
+struct file_list {
+	struct file_item {
+		struct item item;
+		struct adddel index, worktree;
+	} **file;
+	size_t nr, alloc;
+};
+
+static void add_file_item(struct file_list *list, const char *name)
+{
+	struct file_item *item;
+
+	FLEXPTR_ALLOC_STR(item, item.name, name);
+
+	ALLOC_GROW(list->file, list->nr + 1, list->alloc);
+	list->file[list->nr++] = item;
+}
+
+static void reset_file_list(struct file_list *list)
+{
+	size_t i;
+
+	for (i = 0; i < list->nr; i++)
+		free(list->file[i]);
+	list->nr = 0;
+}
+
+static void release_file_list(struct file_list *list)
+{
+	reset_file_list(list);
+	FREE_AND_NULL(list->file);
+	list->alloc = 0;
+}
+
+static int file_item_cmp(const void *a, const void *b)
+{
+	const struct file_item * const *f1 = a;
+	const struct file_item * const *f2 = b;
+
+	return strcmp((*f1)->item.name, (*f2)->item.name);
+}
+
+struct pathname_entry {
+	struct hashmap_entry ent;
+	size_t index;
+	char pathname[FLEX_ARRAY];
+};
+
+static int pathname_entry_cmp(const void *unused_cmp_data,
+			      const void *entry, const void *entry_or_key,
+			      const void *pathname)
+{
+	const struct pathname_entry *e1 = entry, *e2 = entry_or_key;
+
+	return strcmp(e1->pathname,
+		      pathname ? (const char *)pathname : e2->pathname);
+}
+
+struct collection_status {
+	enum { FROM_WORKTREE = 0, FROM_INDEX = 1 } phase;
+
+	const char *reference;
+
+	struct file_list *list;
+	struct hashmap file_map;
+};
+
+static void collect_changes_cb(struct diff_queue_struct *q,
+			       struct diff_options *options,
+			       void *data)
+{
+	struct collection_status *s = data;
+	struct diffstat_t stat = { 0 };
+	int i;
+
+	if (!q->nr)
+		return;
+
+	compute_diffstat(options, &stat, q);
+
+	for (i = 0; i < stat.nr; i++) {
+		const char *name = stat.files[i]->name;
+		int hash = strhash(name);
+		struct pathname_entry *entry;
+		size_t file_index;
+		struct file_item *file;
+		struct adddel *adddel;
+
+		entry = hashmap_get_from_hash(&s->file_map, hash, name);
+		if (entry)
+			file_index = entry->index;
+		else {
+			FLEX_ALLOC_STR(entry, pathname, name);
+			hashmap_entry_init(&entry->ent, hash);
+			entry->index = file_index = s->list->nr;
+			hashmap_add(&s->file_map, &entry->ent);
+
+			add_file_item(s->list, name);
+		}
+		file = s->list->file[file_index];
+
+		adddel = s->phase == FROM_INDEX ? &file->index : &file->worktree;
+		adddel->seen = 1;
+		adddel->add = stat.files[i]->added;
+		adddel->del = stat.files[i]->deleted;
+		if (stat.files[i]->is_binary)
+			adddel->binary = 1;
+	}
+	free_diffstat_info(&stat);
+}
+
+static int get_modified_files(struct repository *r, struct file_list *list,
+			      const struct pathspec *ps)
+{
+	struct object_id head_oid;
+	int is_initial = !resolve_ref_unsafe("HEAD", RESOLVE_REF_READING,
+					     &head_oid, NULL);
+	struct collection_status s = { FROM_WORKTREE };
+
+	if (repo_read_index_preload(r, ps, 0) < 0)
+		return error(_("could not read index"));
+
+	s.list = list;
+	hashmap_init(&s.file_map, pathname_entry_cmp, NULL, 0);
+
+	for (s.phase = FROM_WORKTREE; s.phase <= FROM_INDEX; s.phase++) {
+		struct rev_info rev;
+		struct setup_revision_opt opt = { 0 };
+
+		opt.def = is_initial ?
+			empty_tree_oid_hex() : oid_to_hex(&head_oid);
+
+		init_revisions(&rev, NULL);
+		setup_revisions(0, NULL, &rev, &opt);
+
+		rev.diffopt.output_format = DIFF_FORMAT_CALLBACK;
+		rev.diffopt.format_callback = collect_changes_cb;
+		rev.diffopt.format_callback_data = &s;
+
+		if (ps)
+			copy_pathspec(&rev.prune_data, ps);
+
+		if (s.phase == FROM_INDEX)
+			run_diff_index(&rev, 1);
+		else {
+			rev.diffopt.flags.ignore_dirty_submodules = 1;
+			run_diff_files(&rev, 0);
+		}
+	}
+	hashmap_free(&s.file_map, 1);
+
+	/* While the diffs are ordered already, we ran *two* diffs... */
+	QSORT(list->file, list->nr, file_item_cmp);
+
+	return 0;
+}
+
+static void populate_wi_changes(struct strbuf *buf,
+				struct adddel *ad, const char *no_changes)
+{
+	if (ad->binary)
+		strbuf_addstr(buf, _("binary"));
+	else if (ad->seen)
+		strbuf_addf(buf, "+%"PRIuMAX"/-%"PRIuMAX,
+			    (uintmax_t)ad->add, (uintmax_t)ad->del);
+	else
+		strbuf_addstr(buf, no_changes);
+}
+
+struct print_file_item_data {
+	const char *modified_fmt;
+	struct strbuf buf, index, worktree;
+};
+
+static void print_file_item(int i, struct item *item,
+			    void *print_file_item_data)
+{
+	struct file_item *c = (struct file_item *)item;
+	struct print_file_item_data *d = print_file_item_data;
+
+	strbuf_reset(&d->index);
+	strbuf_reset(&d->worktree);
+	strbuf_reset(&d->buf);
+
+	populate_wi_changes(&d->worktree, &c->worktree, _("nothing"));
+	populate_wi_changes(&d->index, &c->index, _("unchanged"));
+	strbuf_addf(&d->buf, d->modified_fmt,
+		    d->index.buf, d->worktree.buf, item->name);
+
+	printf(" %2d: %s", i + 1, d->buf.buf);
+}
+
+static int run_status(struct repository *r, const struct pathspec *ps,
+		      struct file_list *files, struct list_options *opts)
+{
+	reset_file_list(files);
+
+	if (get_modified_files(r, files, ps) < 0)
+		return -1;
+
+	if (files->nr)
+		list((struct item **)files->file, files->nr, opts);
+	putchar('\n');
+
+	return 0;
+}
 
 int run_add_i(struct repository *r, const struct pathspec *ps)
 {
-	die(_("No commands are available in the built-in `git add -i` yet!"));
+	struct print_file_item_data print_file_item_data = {
+		"%12s %12s %s", STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
+	};
+	struct list_options opts = {
+		NULL, print_file_item, &print_file_item_data
+	};
+	struct strbuf header = STRBUF_INIT;
+	struct file_list files = { NULL };
+	int res = 0;
+
+	strbuf_addstr(&header, "      ");
+	strbuf_addf(&header, print_file_item_data.modified_fmt,
+		    _("staged"), _("unstaged"), _("path"));
+	opts.header = header.buf;
+
+	res = run_status(r, ps, &files, &opts);
+
+	release_file_list(&files);
+	strbuf_release(&print_file_item_data.buf);
+	strbuf_release(&print_file_item_data.index);
+	strbuf_release(&print_file_item_data.worktree);
+	strbuf_release(&header);
+
+	return res;
 }
Signature · 1 line
gitgitgadget
#69 lore raw

PATCH v4 4/11 built-in add -i: refresh the index before running `status`

In reply to Johannes Schindelin via GitGitGadget

From: Johannes Schindelin <[email protected]>

This is what the Perl version does, and therefore it is what the built-in version should do, too.

---
 add-interactive.c |  4 +++-
 repository.c      | 19 +++++++++++++++++++
 repository.h      |  7 +++++++
 3 files changed, 29 insertions(+), 1 deletion(-)
add-interactive.c +3 -1
index d64206ba1c..427abe505e 100644
@@ -259,7 +259,9 @@ int run_add_i(struct repository *r, const struct pathspec *ps)
 		    _("staged"), _("unstaged"), _("path"));
 	opts.header = header.buf;
 
-	res = run_status(r, ps, &files, &opts);
+	repo_refresh_and_write_index(r, REFRESH_QUIET, 1);
+	if (run_status(r, ps, &files, &opts) < 0)
+		res = -1;
 
 	release_file_list(&files);
 	strbuf_release(&print_file_item_data.buf);
repository.c +19 -0
index 682c239fe3..def35c40fc 100644
@@ -275,3 +275,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;
+}
repository.h +7 -0
index 4fb6a5885f..cf5d5bab48 100644
@@ -157,5 +157,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 */
Signature · 1 line
gitgitgadget
#70 lore raw

PATCH v4 5/11 built-in add -i: color the header in the `status` command

In reply to Johannes Schindelin via GitGitGadget

From: Johannes Schindelin <[email protected]>

For simplicity, we only implemented the status command without colors. This patch starts adding color, matching what the Perl script git-add--interactive.perl does.

---
 add-interactive.c | 60 ++++++++++++++++++++++++++++++++++++++++++-----
 1 file changed, 54 insertions(+), 6 deletions(-)
add-interactive.c +54 -6
index 427abe505e..f5577a80d3 100644
@@ -1,9 +1,51 @@
 #include "cache.h"
 #include "add-interactive.h"
+#include "color.h"
+#include "config.h"
 #include "diffcore.h"
 #include "revision.h"
 #include "refs.h"
 
+struct add_i_state {
+	struct repository *r;
+	int use_color;
+	char header_color[COLOR_MAXLEN];
+};
+
+static void init_color(struct repository *r, struct add_i_state *s,
+		       const char *slot_name, char *dst,
+		       const char *default_color)
+{
+	char *key = xstrfmt("color.interactive.%s", slot_name);
+	const char *value;
+
+	if (!s->use_color)
+		dst[0] = '\0';
+	else if (repo_config_get_value(r, key, &value) ||
+		 color_parse(value, dst))
+		strlcpy(dst, default_color, COLOR_MAXLEN);
+
+	free(key);
+}
+
+static int init_add_i_state(struct repository *r, struct add_i_state *s)
+{
+	const char *value;
+
+	s->r = r;
+
+	if (repo_config_get_value(r, "color.interactive", &value))
+		s->use_color = -1;
+	else
+		s->use_color =
+			git_config_colorbool("color.interactive", value);
+	s->use_color = want_color(s->use_color);
+
+	init_color(r, s, "header", s->header_color, GIT_COLOR_BOLD);
+
+	return 0;
+}
+
 struct item {
 	const char *name;
 };
@@ -14,7 +56,8 @@ struct list_options {
 	void *print_item_data;
 };
 
-static void list(struct item **list, size_t nr, struct list_options *opts)
+static void list(struct item **list, size_t nr,
+		 struct add_i_state *s, struct list_options *opts)
 {
 	int i;
 
@@ -22,7 +65,8 @@ static void list(struct item **list, size_t nr, struct list_options *opts)
 		return;
 
 	if (opts->header)
-		printf("%s\n", opts->header);
+		color_fprintf_ln(stdout, s->header_color,
+				 "%s", opts->header);
 
 	for (i = 0; i < nr; i++) {
 		opts->print_item(i, list[i], opts->print_item_data);
@@ -227,16 +271,16 @@ static void print_file_item(int i, struct item *item,
 	printf(" %2d: %s", i + 1, d->buf.buf);
 }
 
-static int run_status(struct repository *r, const struct pathspec *ps,
+static int run_status(struct add_i_state *s, const struct pathspec *ps,
 		      struct file_list *files, struct list_options *opts)
 {
 	reset_file_list(files);
 
-	if (get_modified_files(r, files, ps) < 0)
+	if (get_modified_files(s->r, files, ps) < 0)
 		return -1;
 
 	if (files->nr)
-		list((struct item **)files->file, files->nr, opts);
+		list((struct item **)files->file, files->nr, s, opts);
 	putchar('\n');
 
 	return 0;
@@ -244,6 +288,7 @@ static int run_status(struct repository *r, const struct pathspec *ps,
 
 int run_add_i(struct repository *r, const struct pathspec *ps)
 {
+	struct add_i_state s = { NULL };
 	struct print_file_item_data print_file_item_data = {
 		"%12s %12s %s", STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
 	};
@@ -254,13 +299,16 @@ int run_add_i(struct repository *r, const struct pathspec *ps)
 	struct file_list files = { NULL };
 	int res = 0;
 
+	if (init_add_i_state(r, &s))
+		return error("could not parse `add -i` config");
+
 	strbuf_addstr(&header, "      ");
 	strbuf_addf(&header, print_file_item_data.modified_fmt,
 		    _("staged"), _("unstaged"), _("path"));
 	opts.header = header.buf;
 
 	repo_refresh_and_write_index(r, REFRESH_QUIET, 1);
-	if (run_status(r, ps, &files, &opts) < 0)
+	if (run_status(&s, ps, &files, &opts) < 0)
 		res = -1;
 
 	release_file_list(&files);
Signature · 1 line
gitgitgadget
#71 lore raw

PATCH v4 6/11 built-in add -i: implement the main loop

In reply to Johannes Schindelin via GitGitGadget

From: Johannes Schindelin <[email protected]>

The reason why we did not start with the main loop to begin with is that it is the first user of list_and_choose(), which uses the list() function that we conveniently introduced for use by the status command.

Apart from the "and choose" part, there are more differences between the way the status command calls the list_and_choose() function in the Perl version of git add -i compared to the other callers of said function. The most important ones:

- The list is not only shown, but the user is also asked to make a choice, possibly selecting multiple entries.

- The list of items is prefixed with a marker indicating what items have been selected, if multi-selection is allowed.

- Initially, for each item a unique prefix (if there exists any within the given parameters) is determined, and shown in the list, and accepted as a shortcut for the selection.

These features will be implemented later, except the part where the user can choose a command. At this stage, though, the built-in git add -i still only supports the status command, with the remaining commands to follow over the course of the next commits.

In addition, we also modify list() to support displaying the commands in columns, even if there is currently only one.

The Perl script git-add--interactive.perl mixed the purposes of the "list" and the "and choose" part into the same function. In the C version, we will keep them separate instead, calling the list() function from the list_and_choose() function.

Note that we only have a prompt ending in a single ">" at this stage; later commits will add commands that display a double ">>" to indicate that the user is in a different loop than the main one.

---
 add-interactive.c | 129 +++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 127 insertions(+), 2 deletions(-)
add-interactive.c +127 -2
index f5577a80d3..bbab69d4bc 100644
@@ -51,6 +51,7 @@ struct item {
 };
 
 struct list_options {
+	int columns;
 	const char *header;
 	void (*print_item)(int i, struct item *item, void *print_item_data);
 	void *print_item_data;
@@ -59,7 +60,7 @@ struct list_options {
 static void list(struct item **list, size_t nr,
 		 struct add_i_state *s, struct list_options *opts)
 {
-	int i;
+	int i, last_lf = 0;
 
 	if (!nr)
 		return;
@@ -70,8 +71,97 @@ static void list(struct item **list, size_t nr,
 
 	for (i = 0; i < nr; i++) {
 		opts->print_item(i, list[i], opts->print_item_data);
+
+		if ((opts->columns) && ((i + 1) % (opts->columns))) {
+			putchar('\t');
+			last_lf = 0;
+		}
+		else {
+			putchar('\n');
+			last_lf = 1;
+		}
+	}
+
+	if (!last_lf)
 		putchar('\n');
+}
+struct list_and_choose_options {
+	struct list_options list_opts;
+
+	const char *prompt;
+};
+
+#define LIST_AND_CHOOSE_ERROR (-1)
+#define LIST_AND_CHOOSE_QUIT  (-2)
+
+/*
+ * Returns the selected index.
+ *
+ * If an error occurred, returns `LIST_AND_CHOOSE_ERROR`. Upon EOF,
+ * `LIST_AND_CHOOSE_QUIT` is returned.
+ */
+static ssize_t list_and_choose(struct item **items, size_t nr,
+			       struct add_i_state *s,
+			       struct list_and_choose_options *opts)
+{
+	struct strbuf input = STRBUF_INIT;
+	ssize_t res = LIST_AND_CHOOSE_ERROR;
+
+	for (;;) {
+		char *p, *endp;
+
+		strbuf_reset(&input);
+
+		list(items, nr, s, &opts->list_opts);
+
+		printf("%s%s", opts->prompt, "> ");
+		fflush(stdout);
+
+		if (strbuf_getline(&input, stdin) == EOF) {
+			putchar('\n');
+			res = LIST_AND_CHOOSE_QUIT;
+			break;
+		}
+		strbuf_trim(&input);
+
+		if (!input.len)
+			break;
+
+		p = input.buf;
+		for (;;) {
+			size_t sep = strcspn(p, " \t\r\n,");
+			ssize_t index = -1;
+
+			if (!sep) {
+				if (!*p)
+					break;
+				p++;
+				continue;
+			}
+
+			if (isdigit(*p)) {
+				index = strtoul(p, &endp, 10) - 1;
+				if (endp != p + sep)
+					index = -1;
+			}
+
+			p[sep] = '\0';
+			if (index < 0 || index >= nr)
+				printf(_("Huh (%s)?\n"), p);
+			else {
+				res = index;
+				break;
+			}
+
+			p += sep + 1;
+		}
+
+		if (res != LIST_AND_CHOOSE_ERROR)
+			break;
 	}
+
+	strbuf_release(&input);
+	return res;
 }
 
 struct adddel {
@@ -286,17 +376,40 @@ static int run_status(struct add_i_state *s, const struct pathspec *ps,
 	return 0;
 }
 
+static void print_command_item(int i, struct item *item,
+			       void *print_command_item_data)
+{
+	printf(" %2d: %s", i + 1, item->name);
+}
+
+struct command_item {
+	struct item item;
+	int (*command)(struct add_i_state *s, const struct pathspec *ps,
+		       struct file_list *files, struct list_options *opts);
+};
+
 int run_add_i(struct repository *r, const struct pathspec *ps)
 {
 	struct add_i_state s = { NULL };
+	struct list_and_choose_options main_loop_opts = {
+		{ 4, N_("*** Commands ***"), print_command_item, NULL },
+		N_("What now")
+	};
+	struct command_item
+		status = { { "status" }, run_status };
+	struct command_item *commands[] = {
+		&status
+	};
+
 	struct print_file_item_data print_file_item_data = {
 		"%12s %12s %s", STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
 	};
 	struct list_options opts = {
-		NULL, print_file_item, &print_file_item_data
+		0, NULL, print_file_item, &print_file_item_data
 	};
 	struct strbuf header = STRBUF_INIT;
 	struct file_list files = { NULL };
+	ssize_t i;
 	int res = 0;
 
 	if (init_add_i_state(r, &s))
@@ -311,6 +424,18 @@ int run_add_i(struct repository *r, const struct pathspec *ps)
 	if (run_status(&s, ps, &files, &opts) < 0)
 		res = -1;
 
+	for (;;) {
+		i = list_and_choose((struct item **)commands,
+				    ARRAY_SIZE(commands), &s, &main_loop_opts);
+		if (i == LIST_AND_CHOOSE_QUIT) {
+			printf(_("Bye.\n"));
+			res = 0;
+			break;
+		}
+		if (i != LIST_AND_CHOOSE_ERROR)
+			res = commands[i]->command(&s, ps, &files, &opts);
+	}
+
 	release_file_list(&files);
 	strbuf_release(&print_file_item_data.buf);
 	strbuf_release(&print_file_item_data.index);
Signature · 1 line
gitgitgadget
#72 lore raw

PATCH v4 7/11 Add a function to determine unique prefixes for a list of strings

In reply to Johannes Schindelin via GitGitGadget

From: Slavica Djukic <[email protected]>

In the git add -i command, we show unique prefixes of the commands and files, to give an indication what prefix would select them.

Naturally, the C implementation looks a lot different than the Perl
implementation: in Perl, a trie is much easier implemented, while we already have a pretty neat hashmap implementation in C that we use for the purpose of storing (not necessarily unique) prefixes.

The idea: for each item that we add, we generate prefixes starting with the first letter, then the first two letters, then three, etc, until we find a prefix that is unique (or until the prefix length would be longer than we want). If we encounter a previously-unique prefix on the way, we adjust that item's prefix to make it unique again (or we mark it as having no unique prefix if we failed to find one). These partial prefixes are stored in a hash map (for quick lookup times).

To make sure that this function works as expected, we add a test using a special-purpose test helper.

Note: We expect the list of prefix items to be passed in as a list of pointers rather than as regular list to avoid having to copy information (the actual items will most likely contain more information than just the name and the length of the unique prefix, but passing in struct prefix_item * would not allow for that).

---
 Makefile                   |   2 +
 prefix-map.c               | 123 +++++++++++++++++++++++++++++++++++++
 prefix-map.h               |  29 +++++++++
 t/helper/test-prefix-map.c |  58 +++++++++++++++++
 t/helper/test-tool.c       |   1 +
 t/helper/test-tool.h       |   1 +
 t/t0018-prefix-map.sh      |  10 +++
 7 files changed, 224 insertions(+)
 create mode 100644 prefix-map.c
 create mode 100644 prefix-map.h
 create mode 100644 t/helper/test-prefix-map.c
 create mode 100755 t/t0018-prefix-map.sh
Makefile +2 -0
index d04daf9fd8..5c141c4609 100644
@@ -727,6 +727,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
@@ -945,6 +946,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
prefix-map.c new file +123 -0

123 lines not on this page · raw message

prefix-map.h new file +29 -0
new file mode 100644
index 0000000000..0c55ee0b2b
@@ -0,0 +1,29 @@
+#ifndef PREFIX_MAP_H
+#define PREFIX_MAP_H
+
+#include "hashmap.h"
+
+struct prefix_item {
+	const char *name;
+	size_t prefix_length;
+};
+
+/*
+ * Given an array of names, find unique prefixes (i.e. the first <n> characters
+ * that uniquely identify the names) and store the lengths of the unique
+ * prefixes in the 'prefix_length' field of the elements of the given array..
+ *
+ * Typically, the `struct prefix_item` information is a field in the actual
+ * item struct; For this reason, the `array` parameter is specified as an array
+ * 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 **array, size_t nr,
+			  size_t min_length, size_t max_length);
+
+#endif
t/helper/test-prefix-map.c new file +58 -0

58 lines not on this page · raw message

t/helper/test-tool.c +1 -0
index ce7e89028c..34ddf3f8b7 100644
@@ -41,6 +41,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 },
t/helper/test-tool.h +1 -0
index f805bb39ae..400854e60d 100644
@@ -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);
t/t0018-prefix-map.sh new file +10 -0
new file mode 100755
index 0000000000..187fa92aec
@@ -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
Signature · 1 line
gitgitgadget
#73 lore raw

PATCH v4 8/11 built-in add -i: show unique prefixes of the commands

In reply to Johannes Schindelin via GitGitGadget

From: Slavica Djukic <[email protected]>

Just like in the Perl script git-add--interactive.perl, for each command a unique prefix is determined (if there exists any within the given parameters), and shown in the list, and accepted as a shortcut for the command.

We use the prefix map implementation that we just added in the previous commit for that purpose.

---
 add-interactive.c | 69 ++++++++++++++++++++++++++++++++++++++---------
 1 file changed, 56 insertions(+), 13 deletions(-)
add-interactive.c +56 -13
index bbab69d4bc..12a4c2adb8 100644
@@ -5,6 +5,7 @@
 #include "diffcore.h"
 #include "revision.h"
 #include "refs.h"
+#include "prefix-map.h"
 
 struct add_i_state {
 	struct repository *r;
@@ -46,18 +47,32 @@ static int init_add_i_state(struct repository *r, struct add_i_state *s)
 	return 0;
 }
 
-struct item {
-	const char *name;
-};
+static ssize_t find_unique(const char *string,
+			   struct prefix_item **list, size_t nr)
+{
+	ssize_t found = -1, i;
+
+	for (i = 0; i < nr; i++) {
+		struct prefix_item *item = list[i];
+		if (!starts_with(item->name, string))
+			continue;
+		if (found >= 0)
+			return -1;
+		found = i;
+	}
+
+	return found;
+}
 
 struct list_options {
 	int columns;
 	const char *header;
-	void (*print_item)(int i, struct item *item, void *print_item_data);
+	void (*print_item)(int i, struct prefix_item *item,
+			   void *print_item_data);
 	void *print_item_data;
 };
 
-static void list(struct item **list, size_t nr,
+static void list(struct prefix_item **list, size_t nr,
 		 struct add_i_state *s, struct list_options *opts)
 {
 	int i, last_lf = 0;
@@ -100,13 +115,15 @@ struct list_and_choose_options {
  * If an error occurred, returns `LIST_AND_CHOOSE_ERROR`. Upon EOF,
  * `LIST_AND_CHOOSE_QUIT` is returned.
  */
-static ssize_t list_and_choose(struct item **items, size_t nr,
+static ssize_t list_and_choose(struct prefix_item **items, size_t nr,
 			       struct add_i_state *s,
 			       struct list_and_choose_options *opts)
 {
 	struct strbuf input = STRBUF_INIT;
 	ssize_t res = LIST_AND_CHOOSE_ERROR;
 
+	find_unique_prefixes(items, nr, 1, 4);
+
 	for (;;) {
 		char *p, *endp;
 
@@ -146,6 +163,9 @@ static ssize_t list_and_choose(struct item **items, size_t nr,
 			}
 
 			p[sep] = '\0';
+			if (index < 0)
+				index = find_unique(p, items, nr);
+
 			if (index < 0 || index >= nr)
 				printf(_("Huh (%s)?\n"), p);
 			else {
@@ -171,7 +191,7 @@ struct adddel {
 
 struct file_list {
 	struct file_item {
-		struct item item;
+		struct prefix_item item;
 		struct adddel index, worktree;
 	} **file;
 	size_t nr, alloc;
@@ -338,12 +358,29 @@ static void populate_wi_changes(struct strbuf *buf,
 		strbuf_addstr(buf, no_changes);
 }
 
+/* filters out prefixes which have special meaning to list_and_choose() */
+static int is_valid_prefix(const char *prefix, size_t prefix_len)
+{
+	return prefix_len && prefix &&
+		/*
+		 * We expect `prefix` to be NUL terminated, therefore this
+		 * `strcspn()` call is okay, even if it might do much more
+		 * work than strictly necessary.
+		 */
+		strcspn(prefix, " \t\r\n,") >= prefix_len &&	/* separators */
+		*prefix != '-' &&				/* deselection */
+		!isdigit(*prefix) &&				/* selection */
+		(prefix_len != 1 ||
+		 (*prefix != '*' &&				/* "all" wildcard */
+		  *prefix != '?'));				/* prompt help */
+}
+
 struct print_file_item_data {
 	const char *modified_fmt;
 	struct strbuf buf, index, worktree;
 };
 
-static void print_file_item(int i, struct item *item,
+static void print_file_item(int i, struct prefix_item *item,
 			    void *print_file_item_data)
 {
 	struct file_item *c = (struct file_item *)item;
@@ -370,20 +407,26 @@ static int run_status(struct add_i_state *s, const struct pathspec *ps,
 		return -1;
 
 	if (files->nr)
-		list((struct item **)files->file, files->nr, s, opts);
+		list((struct prefix_item **)files->file, files->nr, s, opts);
 	putchar('\n');
 
 	return 0;
 }
 
-static void print_command_item(int i, struct item *item,
+static void print_command_item(int i, struct prefix_item *item,
 			       void *print_command_item_data)
 {
-	printf(" %2d: %s", i + 1, item->name);
+	if (!item->prefix_length ||
+	    !is_valid_prefix(item->name, item->prefix_length))
+		printf(" %2d: %s", i + 1, item->name);
+	else
+		printf(" %3d: [%.*s]%s", i + 1,
+		       (int)item->prefix_length, item->name,
+		       item->name + item->prefix_length);
 }
 
 struct command_item {
-	struct item item;
+	struct prefix_item item;
 	int (*command)(struct add_i_state *s, const struct pathspec *ps,
 		       struct file_list *files, struct list_options *opts);
 };
@@ -425,7 +468,7 @@ int run_add_i(struct repository *r, const struct pathspec *ps)
 		res = -1;
 
 	for (;;) {
-		i = list_and_choose((struct item **)commands,
+		i = list_and_choose((struct prefix_item **)commands,
 				    ARRAY_SIZE(commands), &s, &main_loop_opts);
 		if (i == LIST_AND_CHOOSE_QUIT) {
 			printf(_("Bye.\n"));
Signature · 1 line
gitgitgadget
#74 lore raw

PATCH v4 9/11 built-in add -i: support `?` (prompt help)

In reply to Johannes Schindelin via GitGitGadget

From: Johannes Schindelin <[email protected]>

With this change, we print out the same colored help text that the Perl-based git add -i prints in the main loop when question mark is entered.

---
 add-interactive.c | 22 +++++++++++++++++++++-
 1 file changed, 21 insertions(+), 1 deletion(-)
add-interactive.c +21 -1
index 12a4c2adb8..710317e599 100644
@@ -11,6 +11,7 @@ struct add_i_state {
 	struct repository *r;
 	int use_color;
 	char header_color[COLOR_MAXLEN];
+	char help_color[COLOR_MAXLEN];
 };
 
 static void init_color(struct repository *r, struct add_i_state *s,
@@ -43,6 +44,7 @@ static int init_add_i_state(struct repository *r, struct add_i_state *s)
 	s->use_color = want_color(s->use_color);
 
 	init_color(r, s, "header", s->header_color, GIT_COLOR_BOLD);
+	init_color(r, s, "help", s->help_color, GIT_COLOR_BOLD_RED);
 
 	return 0;
 }
@@ -104,6 +106,7 @@ struct list_and_choose_options {
 	struct list_options list_opts;
 
 	const char *prompt;
+	void (*print_help)(struct add_i_state *s);
 };
 
 #define LIST_AND_CHOOSE_ERROR (-1)
@@ -144,6 +147,11 @@ static ssize_t list_and_choose(struct prefix_item **items, size_t nr,
 		if (!input.len)
 			break;
 
+		if (!strcmp(input.buf, "?")) {
+			opts->print_help(s);
+			continue;
+		}
+
 		p = input.buf;
 		for (;;) {
 			size_t sep = strcspn(p, " \t\r\n,");
@@ -431,12 +439,24 @@ struct command_item {
 		       struct file_list *files, struct list_options *opts);
 };
 
+static void command_prompt_help(struct add_i_state *s)
+{
+	const char *help_color = s->help_color;
+	color_fprintf_ln(stdout, help_color, "%s", _("Prompt help:"));
+	color_fprintf_ln(stdout, help_color, "1          - %s",
+			 _("select a numbered item"));
+	color_fprintf_ln(stdout, help_color, "foo        - %s",
+			 _("select item based on unique prefix"));
+	color_fprintf_ln(stdout, help_color, "           - %s",
+			 _("(empty) select nothing"));
+}
+
 int run_add_i(struct repository *r, const struct pathspec *ps)
 {
 	struct add_i_state s = { NULL };
 	struct list_and_choose_options main_loop_opts = {
 		{ 4, N_("*** Commands ***"), print_command_item, NULL },
-		N_("What now")
+		N_("What now"), command_prompt_help
 	};
 	struct command_item
 		status = { { "status" }, run_status };
Signature · 1 line
gitgitgadget
#75 lore raw

PATCH v4 10/11 built-in add -i: use color in the main loop

In reply to Johannes Schindelin via GitGitGadget

From: Slavica Djukic <[email protected]>

The error messages as well as the unique prefixes are colored in git add -i by default; We need to do the same in the built-in version.

---
 add-interactive.c | 37 ++++++++++++++++++++++++++++++++-----
 1 file changed, 32 insertions(+), 5 deletions(-)
add-interactive.c +32 -5
index 710317e599..a343195a67 100644
@@ -12,6 +12,9 @@ struct add_i_state {
 	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];
 };
 
 static void init_color(struct repository *r, struct add_i_state *s,
@@ -45,6 +48,9 @@ static int init_add_i_state(struct repository *r, struct add_i_state *s)
 
 	init_color(r, s, "header", s->header_color, GIT_COLOR_BOLD);
 	init_color(r, s, "help", s->help_color, GIT_COLOR_BOLD_RED);
+	init_color(r, s, "prompt", s->prompt_color, GIT_COLOR_BOLD_BLUE);
+	init_color(r, s, "error", s->error_color, GIT_COLOR_BOLD_RED);
+	init_color(r, s, "reset", s->reset_color, GIT_COLOR_RESET);
 
 	return 0;
 }
@@ -134,7 +140,8 @@ static ssize_t list_and_choose(struct prefix_item **items, size_t nr,
 
 		list(items, nr, s, &opts->list_opts);
 
-		printf("%s%s", opts->prompt, "> ");
+		color_fprintf(stdout, s->prompt_color, "%s", opts->prompt);
+		fputs("> ", stdout);
 		fflush(stdout);
 
 		if (strbuf_getline(&input, stdin) == EOF) {
@@ -175,7 +182,8 @@ static ssize_t list_and_choose(struct prefix_item **items, size_t nr,
 				index = find_unique(p, items, nr);
 
 			if (index < 0 || index >= nr)
-				printf(_("Huh (%s)?\n"), p);
+				color_fprintf_ln(stdout, s->error_color,
+						 _("Huh (%s)?"), p);
 			else {
 				res = index;
 				break;
@@ -421,15 +429,21 @@ static int run_status(struct add_i_state *s, const struct pathspec *ps,
 	return 0;
 }
 
+struct print_command_item_data {
+	const char *color, *reset;
+};
+
 static void print_command_item(int i, struct prefix_item *item,
 			       void *print_command_item_data)
 {
+	struct print_command_item_data *d = print_command_item_data;
+
 	if (!item->prefix_length ||
 	    !is_valid_prefix(item->name, item->prefix_length))
 		printf(" %2d: %s", i + 1, item->name);
 	else
-		printf(" %3d: [%.*s]%s", i + 1,
-		       (int)item->prefix_length, item->name,
+		printf(" %2d: %s%.*s%s%s", i + 1,
+		       d->color, (int)item->prefix_length, item->name, d->reset,
 		       item->name + item->prefix_length);
 }
 
@@ -454,8 +468,9 @@ static void command_prompt_help(struct add_i_state *s)
 int run_add_i(struct repository *r, const struct pathspec *ps)
 {
 	struct add_i_state s = { NULL };
+	struct print_command_item_data data;
 	struct list_and_choose_options main_loop_opts = {
-		{ 4, N_("*** Commands ***"), print_command_item, NULL },
+		{ 4, N_("*** Commands ***"), print_command_item, &data },
 		N_("What now"), command_prompt_help
 	};
 	struct command_item
@@ -478,6 +493,18 @@ int run_add_i(struct repository *r, const struct pathspec *ps)
 	if (init_add_i_state(r, &s))
 		return error("could not parse `add -i` config");
 
+	/*
+	 * When color was asked for, use the prompt color for
+	 * highlighting, otherwise use square brackets.
+	 */
+	if (s.use_color) {
+		data.color = s.prompt_color;
+		data.reset = s.reset_color;
+	} else {
+		data.color = "[";
+		data.reset = "]";
+	}
+
 	strbuf_addstr(&header, "      ");
 	strbuf_addf(&header, print_file_item_data.modified_fmt,
 		    _("staged"), _("unstaged"), _("path"));
Signature · 1 line
gitgitgadget
#76 lore raw

PATCH v4 11/11 built-in add -i: implement the `help` command

In reply to Johannes Schindelin via GitGitGadget

From: Johannes Schindelin <[email protected]>

This imitates the code to show the help text from the Perl script git-add--interactive.perl in the built-in version.

To make sure that it renders exactly like the Perl version of git add -i, we also add a test case for that to t3701-add-interactive.sh.

---
 add-interactive.c          | 26 ++++++++++++++++++++++++--
 t/t3701-add-interactive.sh | 25 +++++++++++++++++++++++++
 2 files changed, 49 insertions(+), 2 deletions(-)
add-interactive.c +24 -2
index a343195a67..765455a3fc 100644
@@ -429,6 +429,26 @@ static int run_status(struct add_i_state *s, const struct pathspec *ps,
 	return 0;
 }
 
+static int run_help(struct add_i_state *s, const struct pathspec *unused_ps,
+		    struct file_list *unused_files,
+		    struct list_options *unused_opts)
+{
+	color_fprintf_ln(stdout, s->help_color, "status        - %s",
+			 _("show paths with changes"));
+	color_fprintf_ln(stdout, s->help_color, "update        - %s",
+			 _("add working tree state to the staged set of changes"));
+	color_fprintf_ln(stdout, s->help_color, "revert        - %s",
+			 _("revert staged set of changes back to the HEAD version"));
+	color_fprintf_ln(stdout, s->help_color, "patch         - %s",
+			 _("pick hunks and update selectively"));
+	color_fprintf_ln(stdout, s->help_color, "diff          - %s",
+			 _("view diff between HEAD and index"));
+	color_fprintf_ln(stdout, s->help_color, "add untracked - %s",
+			 _("add contents of untracked files to the staged set of changes"));
+
+	return 0;
+}
+
 struct print_command_item_data {
 	const char *color, *reset;
 };
@@ -474,9 +494,11 @@ int run_add_i(struct repository *r, const struct pathspec *ps)
 		N_("What now"), command_prompt_help
 	};
 	struct command_item
-		status = { { "status" }, run_status };
+		status = { { "status" }, run_status },
+		help = { { "help" }, run_help };
 	struct command_item *commands[] = {
-		&status
+		&status,
+		&help
 	};
 
 	struct print_file_item_data print_file_item_data = {
t/t3701-add-interactive.sh +25 -0
index 69991a3168..cf67756b85 100755
@@ -647,4 +647,29 @@ test_expect_success 'checkout -p works with pathological context lines' '
 	test_write_lines a b a b a a b a b a >expect &&
 	test_cmp expect a
 '
+
+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 | GIT_PAGER_IN_USE=true TERM=vt100 git add -i >actual.colored &&
+	test_decode_color <actual.colored >actual &&
+	test_i18ncmp expect actual
+'
+
 test_done
Signature · 1 line
gitgitgadget
#77 lore raw

v5 cover git add -i: add a rudimentary version in C (supporting only status and help so far)

In reply to Johannes Schindelin via GitGitGadget

This is the first leg on the long journey to a fully built-in git add -i (next up: parts 2 [https://github.com/gitgitgadget/git/pull/171], 3
[https://github.com/gitgitgadget/git/pull/172], 4
[https://github.com/gitgitgadget/git/pull/173], 5
[https://github.com/gitgitgadget/git/pull/174], and 6
[https://github.com/gitgitgadget/git/pull/175]). Note: the latter PRs are not necessarily up to date, and will be re-targeted to the appropriate branches in https://github.com/gitster/git as soon as Junio picks them up.

This here patch series reflects the part that was submitted a couple of times (see https://github.com/gitgitgadget/git/pull/103) during the Outreachy project by Slavica Ðukic that continued the journey based on an initial patch series by Daniel Ferreira.

It only implements the status and the help part, in the interest of making the review remotely more reviewable.

As I am a heavy user of git add -p myself and use a patched version for several months already (it is so nice to not suffer over one second startup until the MSYS2 Perl finally shows me anything, instead it feels instantaneous), I integrated these patch series into Git for Windows already, as an opt-in feature guarded by the config variable add.interactive.useBuiltin (and Git for Windows' installer knows to detect this version and offer the option in the graphical user interface).

Changes since v4:

* Rebased onto current master to make use of Thomas Gummerer's repo_refresh_and_write_index() as well as to avoid merge conflicts with Eric Wong's work on struct hashmap.
* Instead of rolling a dedicated data struct to simulate a Trie, we now use string-list extensively (an unsorted copy and a sorted one, the latter to determine unique prefixes). This had massive ramifications on the rest of the patches... For example, the struct command_item structure no longer contains the name field, but is intended to be a util in a string_list.
* Changed the commit messages and author lines to reflect Slavica's name correctly.
* Touched up a couple commit messages.

Changes since v3:

* Rebased to v2.23.0 to reduce friction.
* free_diffstat_info() is now made public as well, and used, to avoid a memory leak.
* Prepared the patches for ew/hashmap (which is strict about the hashmap entries' type in hashmap_entry_init() and friends).
* The private data types have been moved from prefix-map.h to prefix-map.c.
* A lot of int types were converted to more appropriate size_t in prefix-map.c.
* A misleading parameter name list was renamed to the correct array.
* The code comment above find_unique_prefixes() was (hopefully) improved.
* The run_help() function's signature now reflects that most of the parameters are actually unused.

Changes since v2:

* Rebased to master to avoid merge conflicts.
* Renumbered the prefix-map test to avoid conflicts with two patch series that are currently in-flight in pu.

Changes since v1:

* The config machinery was reworked completely, to not use a callback to git_config(), but instead to query the config via the repo_config_get_*() functions. This also prevents a future "Huh???" moment: the internal add --interactive API accepts a parameter of type struct repository *r, but the previous configuration did not use that to query the config (and could in the future be a repository other than the_repository).

* As a consequence, the color sequences are no longer stored in file-local variables, but passed around via a struct.

* Instead of using the magical constant -2 to quit the main loop, it is now defined as LIST_AND_CHOOSE_QUIT (and likewise, LIST_AND_CHOOSE_ERROR is defined as -1 and used where appropriate).

* Improved the add_prefix_item() function by avoiding buffer overruns, not reusing the struct that is used for lookup also for adding the new item, and by strengthening the bug check.

Daniel Ferreira (2):
diff: export diffstat interface
built-in add -i: implement the status command

Johannes Schindelin (4):
Start to implement a built-in version of git add --interactive built-in add -i: implement the main loop
built-in add -i: show unique prefixes of the commands built-in add -i: support ? (prompt help)

Slavica Đukić (3):
built-in add -i: color the header in the status command built-in add -i: use color in the main loop built-in add -i: implement the help command

 Documentation/config/add.txt |   5 +
 Makefile                     |   1 +
 add-interactive.c            | 650 +++++++++++++++++++++++++++++++++++
 add-interactive.h            |   8 +
 builtin/add.c                |  10 +
 diff.c                       |  39 +--
 diff.h                       |  20 ++
 t/README                     |   4 +
 t/t3701-add-interactive.sh   |  25 ++
 9 files changed, 739 insertions(+), 23 deletions(-)
 create mode 100644 add-interactive.c
 create mode 100644 add-interactive.h

base-commit: 566a1439f6f56c2171b8853ddbca0ad3f5098770
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-170%2Fdscho%2Fadd-i-in-c-status-and-help-v5
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-170/dscho/add-i-in-c-status-and-help-v5
Pull-Request: https://github.com/gitgitgadget/git/pull/170

Range-diff vs v4:

1: ad8752eca7 ! 1: ff59d2d0b3 Start to implement a built-in version of git add --interactive

     @@ -47,7 +47,7 @@
       --- a/Makefile
       +++ b/Makefile
      @@
     - 	-name '*.h' -print))
     + 	-name '*.h' -print)))
       
       LIB_OBJS += abspath.o
      +LIB_OBJS += add-interactive.o
     @@ -125,7 +125,7 @@
       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
     ++built-in version of git add -i. See 'add.interactive.useBuiltin' in
      +git-config(1).
      +
       GIT_TEST_INDEX_THREADS=<n> enables exercising the multi-threaded loading

2: 38cc04c1d9 ! 2: 2fc8cc3546 diff: export diffstat interface
@@ -12,7 +12,7 @@
builtin implementation of git-add--interactive's status.

- Signed-off-by: Slavica Djukic <[email protected]>
+ Signed-off-by: Slavica Đukić <[email protected]>

diff --git a/diff.c b/diff.c
3: ee3e40293c ! 3: 6aaa0de4f4 built-in add -i: implement the status command

     @@ -3,7 +3,7 @@
          built-in add -i: implement the `status` command
      
          This implements the `status` command of `git add -i`. The data
     -    structures introduced in this commit will be extended as needed later.
     +    structures introduced in this commit will be extended later, as needed.
      
          At this point, we re-implement only part of the `list_and_choose()`
          function of the Perl script `git-add--interactive.perl` and call it
     @@ -16,12 +16,8 @@
          will be used to implement the main loop of the built-in `git add -i`, at
          which point the new `status` command can actually be used.
      
     -    Note that we pass the list of items as a `struct item **` as opposed to
     -    a `struct item *`, to allow for the actual items to contain much more
     -    information than merely the name.
     -
          Signed-off-by: Daniel Ferreira <[email protected]>
     -    Signed-off-by: Slavica Djukic <[email protected]>
     +    Signed-off-by: Slavica Đukić <[email protected]>
          Signed-off-by: Johannes Schindelin <[email protected]>
      
       diff --git a/add-interactive.c b/add-interactive.c
     @@ -33,29 +29,35 @@
      +#include "diffcore.h"
      +#include "revision.h"
      +#include "refs.h"
     ++#include "string-list.h"
      +
     -+struct item {
     -+	const char *name;
     ++struct add_i_state {
     ++	struct repository *r;
      +};
      +
     ++static void init_add_i_state(struct add_i_state *s, struct repository *r)
     ++{
     ++       s->r = r;
     ++}
     ++
      +struct list_options {
      +	const char *header;
     -+	void (*print_item)(int i, struct item *item, void *print_item_data);
     ++	void (*print_item)(int i, struct string_list_item *item, void *print_item_data);
      +	void *print_item_data;
      +};
      +
     -+static void list(struct item **list, size_t nr, struct list_options *opts)
     ++static void list(struct string_list *list, struct list_options *opts)
      +{
      +	int i;
      +
     -+	if (!nr)
     ++	if (!list->nr)
      +		return;
      +
      +	if (opts->header)
      +		printf("%s\n", opts->header);
      +
     -+	for (i = 0; i < nr; i++) {
     -+		opts->print_item(i, list[i], opts->print_item_data);
     ++	for (i = 0; i < list->nr; i++) {
     ++		opts->print_item(i, list->items + i, opts->print_item_data);
      +		putchar('\n');
      +	}
      +}
     @@ -65,62 +67,34 @@
      +	unsigned seen:1, binary:1;
      +};
      +
     -+struct file_list {
     -+	struct file_item {
     -+		struct item item;
     -+		struct adddel index, worktree;
     -+	} **file;
     -+	size_t nr, alloc;
     ++struct file_item {
     ++	struct adddel index, worktree;
      +};
      +
     -+static void add_file_item(struct file_list *list, const char *name)
     ++static void add_file_item(struct string_list *files, const char *name)
      +{
     -+	struct file_item *item;
     -+
     -+	FLEXPTR_ALLOC_STR(item, item.name, name);
     ++	struct file_item *item = xcalloc(sizeof(*item), 1);
      +
     -+	ALLOC_GROW(list->file, list->nr + 1, list->alloc);
     -+	list->file[list->nr++] = item;
     -+}
     -+
     -+static void reset_file_list(struct file_list *list)
     -+{
     -+	size_t i;
     -+
     -+	for (i = 0; i < list->nr; i++)
     -+		free(list->file[i]);
     -+	list->nr = 0;
     -+}
     -+
     -+static void release_file_list(struct file_list *list)
     -+{
     -+	reset_file_list(list);
     -+	FREE_AND_NULL(list->file);
     -+	list->alloc = 0;
     -+}
     -+
     -+static int file_item_cmp(const void *a, const void *b)
     -+{
     -+	const struct file_item * const *f1 = a;
     -+	const struct file_item * const *f2 = b;
     -+
     -+	return strcmp((*f1)->item.name, (*f2)->item.name);
     ++	string_list_append(files, name)->util = item;
      +}
      +
      +struct pathname_entry {
      +	struct hashmap_entry ent;
     -+	size_t index;
     -+	char pathname[FLEX_ARRAY];
     ++	const char *name;
     ++	struct file_item *item;
      +};
      +
      +static int pathname_entry_cmp(const void *unused_cmp_data,
     -+			      const void *entry, const void *entry_or_key,
     -+			      const void *pathname)
     ++			      const struct hashmap_entry *he1,
     ++			      const struct hashmap_entry *he2,
     ++			      const void *name)
      +{
     -+	const struct pathname_entry *e1 = entry, *e2 = entry_or_key;
     ++	const struct pathname_entry *e1 =
     ++		container_of(he1, const struct pathname_entry, ent);
     ++	const struct pathname_entry *e2 =
     ++		container_of(he2, const struct pathname_entry, ent);
      +
     -+	return strcmp(e1->pathname,
     -+		      pathname ? (const char *)pathname : e2->pathname);
     ++	return strcmp(e1->name, name ? (const char *)name : e2->name);
      +}
      +
      +struct collection_status {
     @@ -128,7 +102,7 @@
      +
      +	const char *reference;
      +
     -+	struct file_list *list;
     ++	struct string_list *files;
      +	struct hashmap file_map;
      +};
      +
     @@ -149,24 +123,24 @@
      +		const char *name = stat.files[i]->name;
      +		int hash = strhash(name);
      +		struct pathname_entry *entry;
     -+		size_t file_index;
     -+		struct file_item *file;
     ++		struct file_item *file_item;
      +		struct adddel *adddel;
      +
     -+		entry = hashmap_get_from_hash(&s->file_map, hash, name);
     -+		if (entry)
     -+			file_index = entry->index;
     -+		else {
     -+			FLEX_ALLOC_STR(entry, pathname, name);
     ++		entry = hashmap_get_entry_from_hash(&s->file_map, hash, name,
     ++						    struct pathname_entry, ent);
     ++		if (!entry) {
     ++			add_file_item(s->files, name);
     ++
     ++			entry = xcalloc(sizeof(*entry), 1);
      +			hashmap_entry_init(&entry->ent, hash);
     -+			entry->index = file_index = s->list->nr;
     ++			entry->name = s->files->items[s->files->nr - 1].string;
     ++			entry->item = s->files->items[s->files->nr - 1].util;
      +			hashmap_add(&s->file_map, &entry->ent);
     -+
     -+			add_file_item(s->list, name);
      +		}
     -+		file = s->list->file[file_index];
      +
     -+		adddel = s->phase == FROM_INDEX ? &file->index : &file->worktree;
     ++		file_item = entry->item;
     ++		adddel = s->phase == FROM_INDEX ?
     ++			&file_item->index : &file_item->worktree;
      +		adddel->seen = 1;
      +		adddel->add = stat.files[i]->added;
      +		adddel->del = stat.files[i]->deleted;
     @@ -176,7 +150,7 @@
      +	free_diffstat_info(&stat);
      +}
      +
     -+static int get_modified_files(struct repository *r, struct file_list *list,
     ++static int get_modified_files(struct repository *r, struct string_list *files,
      +			      const struct pathspec *ps)
      +{
      +	struct object_id head_oid;
     @@ -184,10 +158,12 @@
      +					     &head_oid, NULL);
      +	struct collection_status s = { FROM_WORKTREE };
      +
     -+	if (repo_read_index_preload(r, ps, 0) < 0)
     ++	if (discard_index(r->index) < 0 ||
     ++	    repo_read_index_preload(r, ps, 0) < 0)
      +		return error(_("could not read index"));
      +
     -+	s.list = list;
     ++	string_list_clear(files, 1);
     ++	s.files = files;
      +	hashmap_init(&s.file_map, pathname_entry_cmp, NULL, 0);
      +
      +	for (s.phase = FROM_WORKTREE; s.phase <= FROM_INDEX; s.phase++) {
     @@ -214,15 +190,15 @@
      +			run_diff_files(&rev, 0);
      +		}
      +	}
     -+	hashmap_free(&s.file_map, 1);
     ++	hashmap_free_entries(&s.file_map, struct pathname_entry, ent);
      +
      +	/* While the diffs are ordered already, we ran *two* diffs... */
     -+	QSORT(list->file, list->nr, file_item_cmp);
     ++	string_list_sort(files);
      +
      +	return 0;
      +}
      +
     -+static void populate_wi_changes(struct strbuf *buf,
     ++static void render_adddel(struct strbuf *buf,
      +				struct adddel *ad, const char *no_changes)
      +{
      +	if (ad->binary)
     @@ -239,34 +215,31 @@
      +	struct strbuf buf, index, worktree;
      +};
      +
     -+static void print_file_item(int i, struct item *item,
     ++static void print_file_item(int i, struct string_list_item *item,
      +			    void *print_file_item_data)
      +{
     -+	struct file_item *c = (struct file_item *)item;
     ++	struct file_item *c = item->util;
      +	struct print_file_item_data *d = print_file_item_data;
      +
      +	strbuf_reset(&d->index);
      +	strbuf_reset(&d->worktree);
      +	strbuf_reset(&d->buf);
      +
     -+	populate_wi_changes(&d->worktree, &c->worktree, _("nothing"));
     -+	populate_wi_changes(&d->index, &c->index, _("unchanged"));
     ++	render_adddel(&d->worktree, &c->worktree, _("nothing"));
     ++	render_adddel(&d->index, &c->index, _("unchanged"));
      +	strbuf_addf(&d->buf, d->modified_fmt,
     -+		    d->index.buf, d->worktree.buf, item->name);
     ++		    d->index.buf, d->worktree.buf, item->string);
      +
      +	printf(" %2d: %s", i + 1, d->buf.buf);
      +}
      +
     -+static int run_status(struct repository *r, const struct pathspec *ps,
     -+		      struct file_list *files, struct list_options *opts)
     ++static int run_status(struct add_i_state *s, const struct pathspec *ps,
     ++		      struct string_list *files, struct list_options *opts)
      +{
     -+	reset_file_list(files);
     -+
     -+	if (get_modified_files(r, files, ps) < 0)
     ++	if (get_modified_files(s->r, files, ps) < 0)
      +		return -1;
      +
     -+	if (files->nr)
     -+		list((struct item **)files->file, files->nr, opts);
     ++	list(files, opts);
      +	putchar('\n');
      +
      +	return 0;
     @@ -275,6 +248,7 @@
       int run_add_i(struct repository *r, const struct pathspec *ps)
       {
      -	die(_("No commands are available in the built-in `git add -i` yet!"));
     ++	struct add_i_state s = { NULL };
      +	struct print_file_item_data print_file_item_data = {
      +		"%12s %12s %s", STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
      +	};
     @@ -282,17 +256,24 @@
      +		NULL, print_file_item, &print_file_item_data
      +	};
      +	struct strbuf header = STRBUF_INIT;
     -+	struct file_list files = { NULL };
     ++	struct string_list files = STRING_LIST_INIT_DUP;
      +	int res = 0;
      +
     ++	init_add_i_state(&s, r);
      +	strbuf_addstr(&header, "      ");
      +	strbuf_addf(&header, print_file_item_data.modified_fmt,
      +		    _("staged"), _("unstaged"), _("path"));
      +	opts.header = header.buf;
      +
     -+	res = run_status(r, ps, &files, &opts);
     ++	if (discard_index(r->index) < 0 ||
     ++	    repo_read_index(r) < 0 ||
     ++	    repo_refresh_and_write_index(r, REFRESH_QUIET, 0, 1,
     ++					 NULL, NULL, NULL) < 0)
     ++		warning(_("could not refresh index"));
     ++
     ++	res = run_status(&s, ps, &files, &opts);
      +
     -+	release_file_list(&files);
     ++	string_list_clear(&files, 1);
      +	strbuf_release(&print_file_item_data.buf);
      +	strbuf_release(&print_file_item_data.index);
      +	strbuf_release(&print_file_item_data.worktree);
  4:  3c855d9fa5 <  -:  ---------- built-in add -i: refresh the index before running `status`
  5:  24737a09f7 !  4:  e405f07110 built-in add -i: color the header in the `status` command
     @@ -1,4 +1,4 @@
     -Author: Johannes Schindelin <[email protected]>
     +Author: Slavica Đukić <[email protected]>
      
          built-in add -i: color the header in the `status` command
      
     @@ -7,7 +7,7 @@
          `git-add--interactive.perl` does.
      
          Original-Patch-By: Daniel Ferreira <[email protected]>
     -    Signed-off-by: Slavica Djukic <[email protected]>
     +    Signed-off-by: Slavica Đukić <[email protected]>
          Signed-off-by: Johannes Schindelin <[email protected]>
      
       diff --git a/add-interactive.c b/add-interactive.c
     @@ -21,13 +21,14 @@
       #include "diffcore.h"
       #include "revision.h"
       #include "refs.h"
     +@@
       
     -+struct add_i_state {
     -+	struct repository *r;
     + struct add_i_state {
     + 	struct repository *r;
      +	int use_color;
      +	char header_color[COLOR_MAXLEN];
     -+};
     -+
     + };
     + 
      +static void init_color(struct repository *r, struct add_i_state *s,
      +		       const char *slot_name, char *dst,
      +		       const char *default_color)
     @@ -44,8 +45,9 @@
      +	free(key);
      +}
      +
     -+static int init_add_i_state(struct repository *r, struct add_i_state *s)
     -+{
     + static void init_add_i_state(struct add_i_state *s, struct repository *r)
     + {
     +-       s->r = r;
      +	const char *value;
      +
      +	s->r = r;
     @@ -58,20 +60,16 @@
      +	s->use_color = want_color(s->use_color);
      +
      +	init_color(r, s, "header", s->header_color, GIT_COLOR_BOLD);
     -+
     -+	return 0;
     -+}
     -+
     - struct item {
     - 	const char *name;
     - };
     + }
     + 
     + struct list_options {
      @@
       	void *print_item_data;
       };
       
     --static void list(struct item **list, size_t nr, struct list_options *opts)
     -+static void list(struct item **list, size_t nr,
     -+		 struct add_i_state *s, struct list_options *opts)
     +-static void list(struct string_list *list, struct list_options *opts)
     ++static void list(struct add_i_state *s, struct string_list *list,
     ++		 struct list_options *opts)
       {
       	int i;
       
     @@ -83,51 +81,14 @@
      +		color_fprintf_ln(stdout, s->header_color,
      +				 "%s", opts->header);
       
     - 	for (i = 0; i < nr; i++) {
     - 		opts->print_item(i, list[i], opts->print_item_data);
     + 	for (i = 0; i < list->nr; i++) {
     + 		opts->print_item(i, list->items + i, opts->print_item_data);
      @@
     - 	printf(" %2d: %s", i + 1, d->buf.buf);
     - }
     - 
     --static int run_status(struct repository *r, const struct pathspec *ps,
     -+static int run_status(struct add_i_state *s, const struct pathspec *ps,
     - 		      struct file_list *files, struct list_options *opts)
     - {
     - 	reset_file_list(files);
     - 
     --	if (get_modified_files(r, files, ps) < 0)
     -+	if (get_modified_files(s->r, files, ps) < 0)
     + 	if (get_modified_files(s->r, files, ps) < 0)
       		return -1;
       
     - 	if (files->nr)
     --		list((struct item **)files->file, files->nr, opts);
     -+		list((struct item **)files->file, files->nr, s, opts);
     +-	list(files, opts);
     ++	list(s, files, opts);
       	putchar('\n');
       
       	return 0;
     -@@
     - 
     - int run_add_i(struct repository *r, const struct pathspec *ps)
     - {
     -+	struct add_i_state s = { NULL };
     - 	struct print_file_item_data print_file_item_data = {
     - 		"%12s %12s %s", STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
     - 	};
     -@@
     - 	struct file_list files = { NULL };
     - 	int res = 0;
     - 
     -+	if (init_add_i_state(r, &s))
     -+		return error("could not parse `add -i` config");
     -+
     - 	strbuf_addstr(&header, "      ");
     - 	strbuf_addf(&header, print_file_item_data.modified_fmt,
     - 		    _("staged"), _("unstaged"), _("path"));
     - 	opts.header = header.buf;
     - 
     - 	repo_refresh_and_write_index(r, REFRESH_QUIET, 1);
     --	if (run_status(r, ps, &files, &opts) < 0)
     -+	if (run_status(&s, ps, &files, &opts) < 0)
     - 		res = -1;
     - 
     - 	release_file_list(&files);
  6:  ac67731cf1 !  5:  25590fbbbe built-in add -i: implement the main loop
     @@ -45,26 +45,26 @@
       --- a/add-interactive.c
       +++ b/add-interactive.c
      @@
     - };
     + }
       
       struct list_options {
      +	int columns;
       	const char *header;
     - 	void (*print_item)(int i, struct item *item, void *print_item_data);
     + 	void (*print_item)(int i, struct string_list_item *item, void *print_item_data);
       	void *print_item_data;
      @@
     - static void list(struct item **list, size_t nr,
     - 		 struct add_i_state *s, struct list_options *opts)
     + static void list(struct add_i_state *s, struct string_list *list,
     + 		 struct list_options *opts)
       {
      -	int i;
      +	int i, last_lf = 0;
       
     - 	if (!nr)
     + 	if (!list->nr)
       		return;
      @@
       
     - 	for (i = 0; i < nr; i++) {
     - 		opts->print_item(i, list[i], opts->print_item_data);
     + 	for (i = 0; i < list->nr; i++) {
     + 		opts->print_item(i, list->items + i, opts->print_item_data);
      +
      +		if ((opts->columns) && ((i + 1) % (opts->columns))) {
      +			putchar('\t');
     @@ -94,8 +94,7 @@
      + * If an error occurred, returns `LIST_AND_CHOOSE_ERROR`. Upon EOF,
      + * `LIST_AND_CHOOSE_QUIT` is returned.
      + */
     -+static ssize_t list_and_choose(struct item **items, size_t nr,
     -+			       struct add_i_state *s,
     ++static ssize_t list_and_choose(struct add_i_state *s, struct string_list *items,
      +			       struct list_and_choose_options *opts)
      +{
      +	struct strbuf input = STRBUF_INIT;
     @@ -106,7 +105,7 @@
      +
      +		strbuf_reset(&input);
      +
     -+		list(items, nr, s, &opts->list_opts);
     ++		list(s, items, &opts->list_opts);
      +
      +		printf("%s%s", opts->prompt, "> ");
      +		fflush(stdout);
     @@ -140,7 +139,7 @@
      +			}
      +
      +			p[sep] = '\0';
     -+			if (index < 0 || index >= nr)
     ++			if (index < 0 || index >= items->nr)
      +				printf(_("Huh (%s)?\n"), p);
      +			else {
      +				res = index;
     @@ -163,17 +162,15 @@
       	return 0;
       }
       
     -+static void print_command_item(int i, struct item *item,
     ++typedef int (*command_t)(struct add_i_state *s, const struct pathspec *ps,
     ++			 struct string_list *files,
     ++			 struct list_options *opts);
     ++
     ++static void print_command_item(int i, struct string_list_item *item,
      +			       void *print_command_item_data)
      +{
     -+	printf(" %2d: %s", i + 1, item->name);
     ++	printf(" %2d: %s", i + 1, item->string);
      +}
     -+
     -+struct command_item {
     -+	struct item item;
     -+	int (*command)(struct add_i_state *s, const struct pathspec *ps,
     -+		       struct file_list *files, struct list_options *opts);
     -+};
      +
       int run_add_i(struct repository *r, const struct pathspec *ps)
       {
     @@ -182,11 +179,13 @@
      +		{ 4, N_("*** Commands ***"), print_command_item, NULL },
      +		N_("What now")
      +	};
     -+	struct command_item
     -+		status = { { "status" }, run_status };
     -+	struct command_item *commands[] = {
     -+		&status
     ++	struct {
     ++		const char *string;
     ++		command_t command;
     ++	} command_list[] = {
     ++		{ "status", run_status },
      +	};
     ++	struct string_list commands = STRING_LIST_INIT_NODUP;
      +
       	struct print_file_item_data print_file_item_data = {
       		"%12s %12s %s", STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
     @@ -196,27 +195,42 @@
      +		0, NULL, print_file_item, &print_file_item_data
       	};
       	struct strbuf header = STRBUF_INIT;
     - 	struct file_list files = { NULL };
     + 	struct string_list files = STRING_LIST_INIT_DUP;
      +	ssize_t i;
       	int res = 0;
       
     - 	if (init_add_i_state(r, &s))
     ++	for (i = 0; i < ARRAY_SIZE(command_list); i++)
     ++		string_list_append(&commands, command_list[i].string)
     ++			->util = command_list[i].command;
     ++
     + 	init_add_i_state(&s, r);
     ++
     + 	strbuf_addstr(&header, "      ");
     + 	strbuf_addf(&header, print_file_item_data.modified_fmt,
     + 		    _("staged"), _("unstaged"), _("path"));
      @@
     - 	if (run_status(&s, ps, &files, &opts) < 0)
     - 		res = -1;
     + 
     + 	res = run_status(&s, ps, &files, &opts);
       
      +	for (;;) {
     -+		i = list_and_choose((struct item **)commands,
     -+				    ARRAY_SIZE(commands), &s, &main_loop_opts);
     ++		i = list_and_choose(&s, &commands, &main_loop_opts);
      +		if (i == LIST_AND_CHOOSE_QUIT) {
      +			printf(_("Bye.\n"));
      +			res = 0;
      +			break;
      +		}
     -+		if (i != LIST_AND_CHOOSE_ERROR)
     -+			res = commands[i]->command(&s, ps, &files, &opts);
     ++		if (i != LIST_AND_CHOOSE_ERROR) {
     ++			command_t command = commands.items[i].util;
     ++			res = command(&s, ps, &files, &opts);
     ++		}
      +	}
      +
     - 	release_file_list(&files);
     + 	string_list_clear(&files, 1);
       	strbuf_release(&print_file_item_data.buf);
       	strbuf_release(&print_file_item_data.index);
     + 	strbuf_release(&print_file_item_data.worktree);
     + 	strbuf_release(&header);
     ++	string_list_clear(&commands, 0);
     + 
     + 	return res;
     + }
  7:  c5a699b6b2 <  -:  ---------- Add a function to determine unique prefixes for a list of strings
  8:  bc7a74f697 !  6:  57fdc01463 built-in add -i: show unique prefixes of the commands
     @@ -1,4 +1,4 @@
     -Author: Slavica Djukic <[email protected]>
     +Author: Johannes Schindelin <[email protected]>
      
          built-in add -i: show unique prefixes of the commands
      
     @@ -7,97 +7,197 @@
          given parameters), and shown in the list, and accepted as a shortcut for
          the command.
      
     -    We use the prefix map implementation that we just added in the previous
     -    commit for that purpose.
     +    To determine the unique prefixes, as well as to look up the command in
     +    question, we use a copy of the list and sort it.
      
     -    Signed-off-by: Slavica Djukic <[email protected]>
     +    While this might seem like overkill for a single command, it will make
     +    much more sense when all the commands are implemented, and when we reuse
     +    the same logic to present a list of files to edit, with convenient
     +    unique prefixes.
     +
     +    At the start of the development of this patch series, a dedicated data
     +    structure was introduced that imitated the Trie that the Perl version
     +    implements. However, this was deemed overkill, and we now simply sort
     +    the list before determining the length of the unique prefixes by looking
     +    at each item's neighbor. As a bonus, we now use the same sorted list to
     +    perform a binary search using the user-provided prefix as search key.
     +
     +    Original-patch-by: Slavica Đukić <[email protected]>
     +    Helped-by: SZEDER Gábor <[email protected]>
          Signed-off-by: Johannes Schindelin <[email protected]>
      
       diff --git a/add-interactive.c b/add-interactive.c
       --- a/add-interactive.c
       +++ b/add-interactive.c
      @@
     - #include "diffcore.h"
     - #include "revision.h"
     - #include "refs.h"
     -+#include "prefix-map.h"
     - 
     - struct add_i_state {
     - 	struct repository *r;
     -@@
     - 	return 0;
     + 	init_color(r, s, "header", s->header_color, GIT_COLOR_BOLD);
       }
       
     --struct item {
     --	const char *name;
     --};
     -+static ssize_t find_unique(const char *string,
     -+			   struct prefix_item **list, size_t nr)
     ++/*
     ++ * A "prefix item list" is a list of items that are identified by a string, and
     ++ * a unique prefix (if any) is determined for each item.
     ++ *
     ++ * It is implemented in the form of a pair of `string_list`s, the first one
     ++ * duplicating the strings, with the `util` field pointing at a structure whose
     ++ * first field must be `size_t prefix_length`.
     ++ *
     ++ * That `prefix_length` field will be computed by `find_unique_prefixes()`; It
     ++ * will be set to zero if no valid, unique prefix could be found.
     ++ *
     ++ * The second `string_list` is called `sorted` and does _not_ duplicate the
     ++ * strings but simply reuses the first one's, with the `util` field pointing at
     ++ * the `string_item_list` of the first `string_list`. It  will be populated and
     ++ * sorted by `find_unique_prefixes()`.
     ++ */
     ++struct prefix_item_list {
     ++	struct string_list items;
     ++	struct string_list sorted;
     ++	size_t min_length, max_length;
     ++};
     ++#define PREFIX_ITEM_LIST_INIT \
     ++	{ STRING_LIST_INIT_DUP, STRING_LIST_INIT_NODUP, 1, 4 }
     ++
     ++static void prefix_item_list_clear(struct prefix_item_list *list)
     ++{
     ++	string_list_clear(&list->items, 1);
     ++	string_list_clear(&list->sorted, 0);
     ++}
     ++
     ++static void extend_prefix_length(struct string_list_item *p,
     ++				 const char *other_string, size_t max_length)
      +{
     -+	ssize_t found = -1, i;
     -+
     -+	for (i = 0; i < nr; i++) {
     -+		struct prefix_item *item = list[i];
     -+		if (!starts_with(item->name, string))
     -+			continue;
     -+		if (found >= 0)
     -+			return -1;
     -+		found = i;
     ++	size_t *len = p->util;
     ++
     ++	if (!*len || memcmp(p->string, other_string, *len))
     ++		return;
     ++
     ++	for (;;) {
     ++		char c = p->string[*len];
     ++
     ++		/*
     ++		 * Is `p` a strict prefix of `other`? Or have we exhausted the
     ++		 * maximal length of the prefix? Or is the current character a
     ++		 * multi-byte UTF-8 one? If so, there is no valid, unique
     ++		 * prefix.
     ++		 */
     ++		if (!c || ++*len > max_length || !isascii(c)) {
     ++			*len = 0;
     ++			break;
     ++		}
     ++
     ++		if (c != other_string[*len - 1])
     ++			break;
      +	}
     ++}
      +
     -+	return found;
     ++static void find_unique_prefixes(struct prefix_item_list *list)
     ++{
     ++	size_t i;
     ++
     ++	if (list->sorted.nr == list->items.nr)
     ++		return;
     ++
     ++	string_list_clear(&list->sorted, 0);
     ++	/* Avoid reallocating incrementally */
     ++	list->sorted.items = xmalloc(st_mult(sizeof(*list->sorted.items),
     ++					     list->items.nr));
     ++	list->sorted.nr = list->sorted.alloc = list->items.nr;
     ++
     ++	for (i = 0; i < list->items.nr; i++) {
     ++		list->sorted.items[i].string = list->items.items[i].string;
     ++		list->sorted.items[i].util = list->items.items + i;
     ++	}
     ++
     ++	string_list_sort(&list->sorted);
     ++
     ++	for (i = 0; i < list->sorted.nr; i++) {
     ++		struct string_list_item *sorted_item = list->sorted.items + i;
     ++		struct string_list_item *item = sorted_item->util;
     ++		size_t *len = item->util;
     ++
     ++		*len = 0;
     ++		while (*len < list->min_length) {
     ++			char c = item->string[(*len)++];
     ++
     ++			if (!c || !isascii(c)) {
     ++				*len = 0;
     ++				break;
     ++			}
     ++		}
     ++
     ++		if (i > 0)
     ++			extend_prefix_length(item, sorted_item[-1].string,
     ++					     list->max_length);
     ++		if (i + 1 < list->sorted.nr)
     ++			extend_prefix_length(item, sorted_item[1].string,
     ++					     list->max_length);
     ++	}
      +}
     - 
     ++
     ++static ssize_t find_unique(const char *string, struct prefix_item_list *list)
     ++{
     ++	int index = string_list_find_insert_index(&list->sorted, string, 1);
     ++	struct string_list_item *item;
     ++
     ++	if (list->items.nr != list->sorted.nr)
     ++		BUG("prefix_item_list in inconsistent state (%"PRIuMAX
     ++		    " vs %"PRIuMAX")",
     ++		    (uintmax_t)list->items.nr, (uintmax_t)list->sorted.nr);
     ++
     ++	if (index < 0)
     ++		item = list->sorted.items[-1 - index].util;
     ++	else if (index > 0 &&
     ++		 starts_with(list->sorted.items[index - 1].string, string))
     ++		return -1;
     ++	else if (index + 1 < list->sorted.nr &&
     ++		 starts_with(list->sorted.items[index + 1].string, string))
     ++		return -1;
     ++	else if (index < list->sorted.nr)
     ++		item = list->sorted.items[index].util;
     ++	else
     ++		return -1;
     ++	return item - list->items.items;
     ++}
     ++
       struct list_options {
       	int columns;
       	const char *header;
     --	void (*print_item)(int i, struct item *item, void *print_item_data);
     -+	void (*print_item)(int i, struct prefix_item *item,
     -+			   void *print_item_data);
     - 	void *print_item_data;
     - };
     - 
     --static void list(struct item **list, size_t nr,
     -+static void list(struct prefix_item **list, size_t nr,
     - 		 struct add_i_state *s, struct list_options *opts)
     - {
     - 	int i, last_lf = 0;
      @@
        * If an error occurred, returns `LIST_AND_CHOOSE_ERROR`. Upon EOF,
        * `LIST_AND_CHOOSE_QUIT` is returned.
        */
     --static ssize_t list_and_choose(struct item **items, size_t nr,
     -+static ssize_t list_and_choose(struct prefix_item **items, size_t nr,
     - 			       struct add_i_state *s,
     +-static ssize_t list_and_choose(struct add_i_state *s, struct string_list *items,
     ++static ssize_t list_and_choose(struct add_i_state *s,
     ++			       struct prefix_item_list *items,
       			       struct list_and_choose_options *opts)
       {
       	struct strbuf input = STRBUF_INIT;
       	ssize_t res = LIST_AND_CHOOSE_ERROR;
       
     -+	find_unique_prefixes(items, nr, 1, 4);
     ++	find_unique_prefixes(items);
      +
       	for (;;) {
       		char *p, *endp;
       
     + 		strbuf_reset(&input);
     + 
     +-		list(s, items, &opts->list_opts);
     ++		list(s, &items->items, &opts->list_opts);
     + 
     + 		printf("%s%s", opts->prompt, "> ");
     + 		fflush(stdout);
      @@
       			}
       
       			p[sep] = '\0';
     +-			if (index < 0 || index >= items->nr)
      +			if (index < 0)
     -+				index = find_unique(p, items, nr);
     ++				index = find_unique(p, items);
      +
     - 			if (index < 0 || index >= nr)
     ++			if (index < 0 || index >= items->items.nr)
       				printf(_("Huh (%s)?\n"), p);
       			else {
     -@@
     - 
     - struct file_list {
     - 	struct file_item {
     --		struct item item;
     -+		struct prefix_item item;
     - 		struct adddel index, worktree;
     - 	} **file;
     - 	size_t nr, alloc;
     + 				res = index;
      @@
       		strbuf_addstr(buf, no_changes);
       }
     @@ -122,50 +222,74 @@
       struct print_file_item_data {
       	const char *modified_fmt;
       	struct strbuf buf, index, worktree;
     - };
     - 
     --static void print_file_item(int i, struct item *item,
     -+static void print_file_item(int i, struct prefix_item *item,
     - 			    void *print_file_item_data)
     - {
     - 	struct file_item *c = (struct file_item *)item;
      @@
     - 		return -1;
     - 
     - 	if (files->nr)
     --		list((struct item **)files->file, files->nr, s, opts);
     -+		list((struct prefix_item **)files->file, files->nr, s, opts);
     - 	putchar('\n');
     + 			 struct string_list *files,
     + 			 struct list_options *opts);
       
     - 	return 0;
     - }
     - 
     --static void print_command_item(int i, struct item *item,
     -+static void print_command_item(int i, struct prefix_item *item,
     ++struct command_item {
     ++	size_t prefix_length;
     ++	command_t command;
     ++};
     ++
     + static void print_command_item(int i, struct string_list_item *item,
       			       void *print_command_item_data)
       {
     --	printf(" %2d: %s", i + 1, item->name);
     -+	if (!item->prefix_length ||
     -+	    !is_valid_prefix(item->name, item->prefix_length))
     -+		printf(" %2d: %s", i + 1, item->name);
     +-	printf(" %2d: %s", i + 1, item->string);
     ++	struct command_item *util = item->util;
     ++
     ++	if (!util->prefix_length ||
     ++	    !is_valid_prefix(item->string, util->prefix_length))
     ++		printf(" %2d: %s", i + 1, item->string);
      +	else
     -+		printf(" %3d: [%.*s]%s", i + 1,
     -+		       (int)item->prefix_length, item->name,
     -+		       item->name + item->prefix_length);
     ++		printf(" %2d: [%.*s]%s", i + 1,
     ++		       (int)util->prefix_length, item->string,
     ++		       item->string + util->prefix_length);
       }
       
     - struct command_item {
     --	struct item item;
     -+	struct prefix_item item;
     - 	int (*command)(struct add_i_state *s, const struct pathspec *ps,
     - 		       struct file_list *files, struct list_options *opts);
     - };
     + int run_add_i(struct repository *r, const struct pathspec *ps)
      @@
     - 		res = -1;
     + 	} command_list[] = {
     + 		{ "status", run_status },
     + 	};
     +-	struct string_list commands = STRING_LIST_INIT_NODUP;
     ++	struct prefix_item_list commands = PREFIX_ITEM_LIST_INIT;
       
     - 	for (;;) {
     --		i = list_and_choose((struct item **)commands,
     -+		i = list_and_choose((struct prefix_item **)commands,
     - 				    ARRAY_SIZE(commands), &s, &main_loop_opts);
     - 		if (i == LIST_AND_CHOOSE_QUIT) {
     - 			printf(_("Bye.\n"));
     + 	struct print_file_item_data print_file_item_data = {
     + 		"%12s %12s %s", STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
     +@@
     + 	ssize_t i;
     + 	int res = 0;
     + 
     +-	for (i = 0; i < ARRAY_SIZE(command_list); i++)
     +-		string_list_append(&commands, command_list[i].string)
     +-			->util = command_list[i].command;
     ++	for (i = 0; i < ARRAY_SIZE(command_list); i++) {
     ++		struct command_item *util = xcalloc(sizeof(*util), 1);
     ++		util->command = command_list[i].command;
     ++		string_list_append(&commands.items, command_list[i].string)
     ++			->util = util;
     ++	}
     + 
     + 	init_add_i_state(&s, r);
     + 
     +@@
     + 			break;
     + 		}
     + 		if (i != LIST_AND_CHOOSE_ERROR) {
     +-			command_t command = commands.items[i].util;
     +-			res = command(&s, ps, &files, &opts);
     ++			struct command_item *util =
     ++				commands.items.items[i].util;
     ++			res = util->command(&s, ps, &files, &opts);
     + 		}
     + 	}
     + 
     +@@
     + 	strbuf_release(&print_file_item_data.index);
     + 	strbuf_release(&print_file_item_data.worktree);
     + 	strbuf_release(&header);
     +-	string_list_clear(&commands, 0);
     ++	prefix_item_list_clear(&commands);
     + 
     + 	return res;
     + }
  9:  74f73e26b4 !  7:  77ad5f333a built-in add -i: support `?` (prompt help)
     @@ -24,9 +24,9 @@
       
       	init_color(r, s, "header", s->header_color, GIT_COLOR_BOLD);
      +	init_color(r, s, "help", s->help_color, GIT_COLOR_BOLD_RED);
     - 
     - 	return 0;
       }
     + 
     + /*
      @@
       	struct list_options list_opts;
       
     @@ -48,8 +48,8 @@
       		for (;;) {
       			size_t sep = strcspn(p, " \t\r\n,");
      @@
     - 		       struct file_list *files, struct list_options *opts);
     - };
     + 		       item->string + util->prefix_length);
     + }
       
      +static void command_prompt_help(struct add_i_state *s)
      +{
     @@ -71,5 +71,5 @@
      -		N_("What now")
      +		N_("What now"), command_prompt_help
       	};
     - 	struct command_item
     - 		status = { { "status" }, run_status };
     + 	struct {
     + 		const char *string;
 10:  88001009bc !  8:  3d0b172a7f built-in add -i: use color in the main loop
     @@ -1,11 +1,11 @@
     -Author: Slavica Djukic <[email protected]>
     +Author: Slavica Đukić <[email protected]>
      
          built-in add -i: use color in the main loop
      
          The error messages as well as the unique prefixes are colored in `git
          add -i` by default; We need to do the same in the built-in version.
      
     -    Signed-off-by: Slavica Djukic <[email protected]>
     +    Signed-off-by: Slavica Đukić <[email protected]>
          Signed-off-by: Johannes Schindelin <[email protected]>
      
       diff --git a/add-interactive.c b/add-interactive.c
     @@ -28,12 +28,12 @@
      +	init_color(r, s, "prompt", s->prompt_color, GIT_COLOR_BOLD_BLUE);
      +	init_color(r, s, "error", s->error_color, GIT_COLOR_BOLD_RED);
      +	init_color(r, s, "reset", s->reset_color, GIT_COLOR_RESET);
     - 
     - 	return 0;
       }
     + 
     + /*
      @@
       
     - 		list(items, nr, s, &opts->list_opts);
     + 		list(s, &items->items, &opts->list_opts);
       
      -		printf("%s%s", opts->prompt, "> ");
      +		color_fprintf(stdout, s->prompt_color, "%s", opts->prompt);
     @@ -42,9 +42,9 @@
       
       		if (strbuf_getline(&input, stdin) == EOF) {
      @@
     - 				index = find_unique(p, items, nr);
     + 				index = find_unique(p, items);
       
     - 			if (index < 0 || index >= nr)
     + 			if (index < 0 || index >= items->items.nr)
      -				printf(_("Huh (%s)?\n"), p);
      +				color_fprintf_ln(stdout, s->error_color,
      +						 _("Huh (%s)?"), p);
     @@ -52,43 +52,46 @@
       				res = index;
       				break;
      @@
     - 	return 0;
     - }
     + 	command_t command;
     + };
       
      +struct print_command_item_data {
      +	const char *color, *reset;
      +};
      +
     - static void print_command_item(int i, struct prefix_item *item,
     + static void print_command_item(int i, struct string_list_item *item,
       			       void *print_command_item_data)
       {
      +	struct print_command_item_data *d = print_command_item_data;
     -+
     - 	if (!item->prefix_length ||
     - 	    !is_valid_prefix(item->name, item->prefix_length))
     - 		printf(" %2d: %s", i + 1, item->name);
     + 	struct command_item *util = item->util;
     + 
     + 	if (!util->prefix_length ||
     + 	    !is_valid_prefix(item->string, util->prefix_length))
     + 		printf(" %2d: %s", i + 1, item->string);
       	else
     --		printf(" %3d: [%.*s]%s", i + 1,
     --		       (int)item->prefix_length, item->name,
     +-		printf(" %2d: [%.*s]%s", i + 1,
     +-		       (int)util->prefix_length, item->string,
     +-		       item->string + util->prefix_length);
      +		printf(" %2d: %s%.*s%s%s", i + 1,
     -+		       d->color, (int)item->prefix_length, item->name, d->reset,
     - 		       item->name + item->prefix_length);
     ++		       d->color, (int)util->prefix_length, item->string,
     ++		       d->reset, item->string + util->prefix_length);
       }
       
     + static void command_prompt_help(struct add_i_state *s)
      @@
       int run_add_i(struct repository *r, const struct pathspec *ps)
       {
       	struct add_i_state s = { NULL };
     -+	struct print_command_item_data data;
     ++	struct print_command_item_data data = { "[", "]" };
       	struct list_and_choose_options main_loop_opts = {
      -		{ 4, N_("*** Commands ***"), print_command_item, NULL },
      +		{ 4, N_("*** Commands ***"), print_command_item, &data },
       		N_("What now"), command_prompt_help
       	};
     - 	struct command_item
     + 	struct {
      @@
     - 	if (init_add_i_state(r, &s))
     - 		return error("could not parse `add -i` config");
     + 
     + 	init_add_i_state(&s, r);
       
      +	/*
      +	 * When color was asked for, use the prompt color for
     @@ -97,9 +100,6 @@
      +	if (s.use_color) {
      +		data.color = s.prompt_color;
      +		data.reset = s.reset_color;
     -+	} else {
     -+		data.color = "[";
     -+		data.reset = "]";
      +	}
      +
       	strbuf_addstr(&header, "      ");

11: b27fbe289f ! 9: 85e508ef11 built-in add -i: implement the help command
@@ -1,4 +1,4 @@
-Author: Johannes Schindelin <[email protected]> +Author: Slavica Đukić <[email protected]>

built-in add -i: implement the help command

@@ -8,7 +8,7 @@
To make sure that it renders exactly like the Perl version of git add -i, we also add a test case for that to t3701-add-interactive.sh.

- Signed-off-by: Slavica Djukic <[email protected]>
+ Signed-off-by: Slavica Đukić <[email protected]>

diff --git a/add-interactive.c b/add-interactive.c
@@ -19,7 +19,7 @@
}

+static int run_help(struct add_i_state *s, const struct pathspec *unused_ps,
-+ struct file_list *unused_files,
++ struct string_list *unused_files,
+ struct list_options *unused_opts)
+{
+ color_fprintf_ln(stdout, s->help_color, "status - %s",
@@ -38,23 +38,17 @@
+ return 0;
+}
+
- struct print_command_item_data {
- const char *color, *reset;
- };
+ typedef int (*command_t)(struct add_i_state *s, const struct pathspec *ps,
+ struct string_list *files,
+ struct list_options *opts);
@@
- N_("What now"), command_prompt_help
- };
- struct command_item
-- status = { { "status" }, run_status };
-+ status = { { "status" }, run_status },
-+ help = { { "help" }, run_help };
- struct command_item *commands[] = {
-- &status
-+ &status,
-+ &help
+ command_t command;
+ } command_list[] = {
+ { "status", run_status },
++ { "help", run_help },
};
+ struct prefix_item_list commands = PREFIX_ITEM_LIST_INIT;

- struct print_file_item_data print_file_item_data = {

diff --git a/t/t3701-add-interactive.sh b/t/t3701-add-interactive.sh --- a/t/t3701-add-interactive.sh

Signature · 1 line
gitgitgadget
#78 lore raw

PATCH v5 1/9 Start to implement a built-in version of `git add --interactive`

From: Johannes Schindelin <[email protected]>

This is hardly the first conversion of a Git command that is implemented as a script to a built-in. So far, the most successful strategy for such conversions has been to add a built-in helper and call that for more and more functionality from the script, as more and more parts are converted.

With the interactive add, we choose a different strategy. The sole reason for this is that on Windows (where such a conversion has the most benefits in terms of speed and robustness) we face the very specific problem that a system() call in Perl seems to close stdin in the parent process when the spawned process consumes even one character from stdin. And that just does not work for us here, as it would stop the main loop as soon as any interactive command was performed by the helper. Which is almost all of the commands in git add -i.

It is almost as if Perl told us once again that it does not want us to use it on Windows.

Instead, we follow the opposite route where we start with a bare-bones version of the built-in interactive add, guarded by the new add.interactive.useBuiltin config variable, and then add more and more functionality to it, until it is feature complete.

At this point, the built-in version of git add -i only states that it cannot do anything yet ;-)

---
 Documentation/config/add.txt |  5 +++++
 Makefile                     |  1 +
 add-interactive.c            |  7 +++++++
 add-interactive.h            |  8 ++++++++
 builtin/add.c                | 10 ++++++++++
 t/README                     |  4 ++++
 6 files changed, 35 insertions(+)
 create mode 100644 add-interactive.c
 create mode 100644 add-interactive.h
Documentation/config/add.txt +5 -0
index 4d753f006e..c9f748f81c 100644
@@ -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.
Makefile +1 -0
index 58b92af54b..6c4a1e0ee5 100644
@@ -823,6 +823,7 @@ LIB_H := $(sort $(patsubst ./%,%,$(shell git ls-files '*.h' ':!t/' ':!Documentat
 	-name '*.h' -print)))
 
 LIB_OBJS += abspath.o
+LIB_OBJS += add-interactive.o
 LIB_OBJS += advice.o
 LIB_OBJS += alias.o
 LIB_OBJS += alloc.o
add-interactive.c new file +7 -0
new file mode 100644
index 0000000000..482e458dc6
@@ -0,0 +1,7 @@
+#include "cache.h"
+#include "add-interactive.h"
+
+int run_add_i(struct repository *r, const struct pathspec *ps)
+{
+	die(_("No commands are available in the built-in `git add -i` yet!"));
+}
add-interactive.h new file +8 -0
new file mode 100644
index 0000000000..7043b8741d
@@ -0,0 +1,8 @@
+#ifndef ADD_INTERACTIVE_H
+#define ADD_INTERACTIVE_H
+
+struct repository;
+struct pathspec;
+int run_add_i(struct repository *r, const struct pathspec *ps);
+
+#endif
builtin/add.c +10 -0
index dd18e5c9b6..4f625691b5 100644
@@ -20,6 +20,7 @@
 #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>..."),
@@ -185,6 +186,14 @@ int run_add_interactive(const char *revision, const char *patch_mode,
 {
 	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 && !patch_mode)
+		return !!run_add_i(the_repository, pathspec);
 
 	argv_array_push(&argv, "add--interactive");
 	if (patch_mode)
@@ -319,6 +328,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);
 }
 
t/README +4 -0
index 60d5b77bcc..5132ec83f8 100644
@@ -397,6 +397,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
+built-in 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
Signature · 1 line
gitgitgadget
#79 lore raw

Re: [PATCH v5 1/9] Start to implement a built-in version of `git add --interactive`

"Johannes Schindelin via GitGitGadget" <[email protected]>

Quoted 9 lines · writes:

From: Johannes Schindelin <[email protected]>

This is hardly the first conversion of a Git command that is implemented as a script to a built-in. So far, the most successful strategy for such conversions has been to add a built-in helper and call that for more and more functionality from the script, as more and more parts are converted.

With the interactive add, we choose a different strategy....

This is hardly the first conversion that we took the "build the whole program piece by piece and flip the whole thing on with usebuiltin" conversion successfully. Pratik's rebase-in-c series comes to mind.

Personally, I do not think the first two paragraphs of the proposed log message do not belong here. Cover letter is a different story and it may make sense to explain why the approach was taken there, but here, I'd prefer to see it more succinctly tell what approach is taken and go directly to describe what this step in that approach does to the readers, which is more important.

Documentation/config/add.txt add.ignore-errors (deprecated):: · line 12

Quoted 7 lines
Documentation/config/add.txt +5 -0
 	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.

Good.

Makefile LIB_H := $(sort $(patsubst ./%,%,$(shell git ls-files '*.h' ':!t/' ':!Documentat · line 829

Quoted 9 lines
Makefile +1 -0
index 58b92af54b..6c4a1e0ee5 100644
 LIB_OBJS += abspath.o
+LIB_OBJS += add-interactive.o
 LIB_OBJS += advice.o
 LIB_OBJS += alias.o
 LIB_OBJS += alloc.o

OK.

add-interactive.c line 7

Quoted 13 lines
add-interactive.c new file +7 -0
new file mode 100644
index 0000000000..482e458dc6
@@ -0,0 +1,7 @@
+#include "cache.h"
+#include "add-interactive.h"
+
+int run_add_i(struct repository *r, const struct pathspec *ps)
+{
+	die(_("No commands are available in the built-in `git add -i` yet!"));
+}

OK, with or without s/commands/sub&/;

add-interactive.h

Quoted 5 lines
add-interactive.h new file +0 -0
new file mode 100644
index 0000000000..7043b8741d

OK.

Quoted 13 lines
builtin/add.c +1 -0
index dd18e5c9b6..4f625691b5 100644
@@ -20,6 +20,7 @@
 #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>..."),

builtin/add.c int run_add_interactive(const char *revision, const char *patch_mode, · line 190

Quoted 5 lines
builtin/add.c +2 -0
@@ -185,6 +186,14 @@ int run_add_interactive(const char *revision, const char *patch_mode,
 {
 	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);

Have blank here at the boundary between decl and stmt ...

builtin/add.c int run_add_interactive(const char *revision, const char *patch_mode, · line 194

Quoted 4 lines
builtin/add.c +4 -0
+	if (use_builtin_add_i < 0)
+		git_config_get_bool("add.interactive.usebuiltin",
+				    &use_builtin_add_i);
+

... and lose it here (optional).

builtin/add.c int run_add_interactive(const char *revision, const char *patch_mode, · line 197

Quoted 3 lines
builtin/add.c +2 -0
+	if (use_builtin_add_i == 1 && !patch_mode)
+		return !!run_add_i(the_repository, pathspec);
 

Strictly speaking, we can bypass the probing of environment and config when upon the entry of the function, where patch_mode is already known. I do not know offhand if rearranging the code to take advantage of that fact would result in a flow that is also easier to follow, but I suspect it would.

builtin/add.c int run_add_interactive(const char *revision, const char *patch_mode,

Quoted 3 lines
builtin/add.c +3 -0
+GIT_TEST_ADD_I_USE_BUILTIN=<boolean>, when true, enables the
+built-in version of git add -i. See 'add.interactive.useBuiltin' in
+git-config(1).

Makes sense.

Thanks.

#80 lore raw

Re: [PATCH v5 1/9] Start to implement a built-in version of `git add --interactive`

Hi Junio,

Quoted 18 lines · Junio C Hamano

"Johannes Schindelin via GitGitGadget" <[email protected]>

Quoted 9 lines · writes:

From: Johannes Schindelin <[email protected]>

This is hardly the first conversion of a Git command that is implemented as a script to a built-in. So far, the most successful strategy for such conversions has been to add a built-in helper and call that for more and more functionality from the script, as more and more parts are converted.

With the interactive add, we choose a different strategy....

This is hardly the first conversion that we took the "build the whole program piece by piece and flip the whole thing on with usebuiltin" conversion successfully. Pratik's rebase-in-c series comes to mind.

Quoted 6 lines

Personally, I do not think the first two paragraphs of the proposed log message do not belong here. Cover letter is a different story and it may make sense to explain why the approach was taken there, but here, I'd prefer to see it more succinctly tell what approach is taken and go directly to describe what this step in that approach does to the readers, which is more important.

I reworded the commit message:

Start to implement a built-in version of git add --interactive

To convert the interactive add to C, we start with a bare-bones version of the built-in interactive add, guarded by the new add.interactive.useBuiltin config variable, and then add more and more functionality to it, until it is feature complete.

This is in contrast to previous conversions to C, where we started with a built-in helper that spawns the script by default, but optionally executes the C code instead. The sole reason for this deviation from previous practice is that on Windows (where such a conversion has the most benefits in terms of speed and robustness) we face the very specific problem that a system() call in Perl seems to close stdin in the parent process when the spawned process consumes even one character from stdin. And that just does not work for us here, as it would stop the main loop as soon as any interactive command was performed by the helper. Which is almost all of the commands in git add -i.

It is almost as if Perl told us once again that it does not want us to use it on Windows.

At this point, the built-in version of git add -i only states that it cannot do anything yet ;-)

Hopefully you like this one better?

Quoted 21 lines
Quoted 13 lines
builtin/add.c +1 -0
index dd18e5c9b6..4f625691b5 100644
@@ -20,6 +20,7 @@
 #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>..."),

builtin/add.c int run_add_interactive(const char *revision, const char *patch_mode, · line 190

Quoted 5 lines
builtin/add.c +2 -0
@@ -185,6 +186,14 @@ int run_add_interactive(const char *revision, const char *patch_mode,
 {
 	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);

Have blank here at the boundary between decl and stmt ...

Quoted 6 lines

builtin/add.c int run_add_interactive(const char *revision, const char *patch_mode, · line 194

Quoted 4 lines
builtin/add.c +4 -0
+	if (use_builtin_add_i < 0)
+		git_config_get_bool("add.interactive.usebuiltin",
+				    &use_builtin_add_i);
+

... and lose it here (optional).

Done.

Quoted 10 lines

builtin/add.c int run_add_interactive(const char *revision, const char *patch_mode, · line 197

Quoted 4 lines
builtin/add.c +2 -0
+	if (use_builtin_add_i == 1 && !patch_mode)
+		return !!run_add_i(the_repository, pathspec);
 

Strictly speaking, we can bypass the probing of environment and config when upon the entry of the function, where patch_mode is already known. I do not know offhand if rearranging the code to take advantage of that fact would result in a flow that is also easier to follow, but I suspect it would.

Okay. I changed it to:

	if (!patch_mode) {
		if (use_builtin_add_i < 0)
			git_config_get_bool("add.interactive.usebuiltin",
					    &use_builtin_add_i);
		if (use_builtin_add_i == 1)
			return !!run_add_i(the_repository, pathspec);
	}

Thanks,
Dscho

#81 lore raw

Re: [PATCH v5 1/9] Start to implement a built-in version of `git add --interactive`

Quoted 21 lines · Johannes Schindelin

I reworded the commit message:

Start to implement a built-in version of git add --interactive

To convert the interactive add to C, we start with a bare-bones version of the built-in interactive add, guarded by the new add.interactive.useBuiltin config variable, and then add more and more functionality to it, until it is feature complete.

This is in contrast to previous conversions to C, where we started with a built-in helper that spawns the script by default, but optionally executes the C code instead. The sole reason for this deviation from previous practice is that on Windows (where such a conversion has the most benefits in terms of speed and robustness) we face the very specific problem that a system() call in Perl seems to close stdin in the parent process when the spawned process consumes even one character from stdin. And that just does not work for us here, as it would stop the main loop as soon as any interactive command was performed by the helper. Which is almost all of the commands in git add -i.

Quoted 7 lines

It is almost as if Perl told us once again that it does not want us to use it on Windows.

At this point, the built-in version of git add -i only states that it cannot do anything yet ;-)

Hopefully you like this one better?

Not really. I find the "we could do the other way but we don't, and I hate Perl" totally irrelevant and misleading.

Unless it is in GSoC or something that wants to avoid a total failure of nothing to show at the end of the period, in which case a piecemeal "we did not finish, but at least we have a handful of subcommands rewritten to show for the consolation prize" might be a way to have "something" that resembles "working". But if we value the quality of the final product over having to have something to show in a set term (like you as a paid programmer working as a professional), building piece by piece in the final framework (i.e. "in C as a builtin") and flipping the "useBuiltin" to turn on the whole thing at the end would be the preferrable way to do this kind of thing, I would think. Also, it is less wasteful, not having to worry about the inter-language glue code. Even if the original is in shell, not in Perl, we have quite a lot of glue code to throw values back and forth across the language boundary to imitate what used to be mere assignments to global shell variables between shell functions and their callers, and always somebody screws up quoting there ;-)

Quoted 12 lines
Quoted 9 lines

builtin/add.c int run_add_interactive(const char *revision, const char *patch_mode, · line 196

Quoted 3 lines
builtin/add.c +2 -0
+	if (use_builtin_add_i == 1 && !patch_mode)
+		return !!run_add_i(the_repository, pathspec);

Strictly speaking, we can bypass the probing of environment and config when upon the entry of the function, where patch_mode is already known. I do not know offhand if rearranging the code to take advantage of that fact would result in a flow that is also easier to follow, but I suspect it would.

Okay. I changed it to:

Quoted 7 lines
	if (!patch_mode) {
		if (use_builtin_add_i < 0)
			git_config_get_bool("add.interactive.usebuiltin",
					    &use_builtin_add_i);
		if (use_builtin_add_i == 1)
			return !!run_add_i(the_repository, pathspec);
	}

Doesn't look so bad as I feared.

Thanks.

#82 lore raw

Re: [PATCH v5 1/9] Start to implement a built-in version of `git add --interactive`

Hi Junio,

Quoted 26 lines · Junio C Hamano
Quoted 24 lines · Johannes Schindelin

I reworded the commit message:

Start to implement a built-in version of git add --interactive

To convert the interactive add to C, we start with a bare-bones version of the built-in interactive add, guarded by the new add.interactive.useBuiltin config variable, and then add more and more functionality to it, until it is feature complete.

This is in contrast to previous conversions to C, where we started with a built-in helper that spawns the script by default, but optionally executes the C code instead. The sole reason for this deviation from previous practice is that on Windows (where such a conversion has the most benefits in terms of speed and robustness) we face the very specific problem that a system() call in Perl seems to close stdin in the parent process when the spawned process consumes even one character from stdin. And that just does not work for us here, as it would stop the main loop as soon as any interactive command was performed by the helper. Which is almost all of the commands in git add -i.

It is almost as if Perl told us once again that it does not want us to use it on Windows.

Quoted 7 lines
Quoted 4 lines

At this point, the built-in version of git add -i only states that it cannot do anything yet ;-)

Hopefully you like this one better?

Not really. I find the "we could do the other way but we don't, and I hate Perl" totally irrelevant and misleading.

Okay, I understand that you take exception at my criticism of Git's use of Perl, and I fully understand that you think I blame you for it because you added most of it.

And I agree that this sidetrack is totally irrelevant for the patch under discussion.

I do think, however, that the discussion of "we wanted to do it the other way, but when we tried, it did not work" is relevant, even if I shortened it to "we use a different approach than previous conversions, because that previous approach would not work".

Truth be told: I would have _much rather_ stayed with the previous --helper approach, as that would have made it possible to have a passing test suite at every step, with and without GIT_TEST_ADD_I_USE_BUILTIN=true.

The "let GIT_TEST_ADD_I_USE_BUILTIN=true use the built-in even for functions we _know_ are not implemented" way only gives us the full comfort of a passing test suite at the very end of all six patch series, if which the patch series we are currently discussing is merely the first.

If I was a reviewer of this patch series rather than the sender, I would be a bit uncomfortable with the fact that GIT_TEST_ADD_I_USE_BUILTIN cannot be added to the CI/PR builds' linux-gcc over-job, not until much, much later. In fact, it can only be added as the very last patch in the very last of the six patch series.

And as I write this, I realize that I never spelled that out in the commit message, and it is a rather important point for reviewers to see addressed pre-emptively, in my opinion.

Therefore I revised the commit message again:

Start to implement a built-in version of git add --interactive

Unlike previous conversions to C, where we started with a built-in helper, we start this conversion by adding an interception in the run_add_interactive() function when the new opt-in add.interactive.useBuiltin config knob is turned on (or the corresponding environment variable GIT_TEST_ADD_I_USE_BUILTIN), and calling the new internal API function run_add_i() that is implemented directly in libgit.a.

At this point, the built-in version of git add -i only states that it cannot do anything yet. In subsequent patches/patch series, the run_add_i() function will gain more and more functionality, until it is feature complete. The whole arc of the conversion can be found in the PRs #170-175 at https://github.com/gitgitgadget/git.

The "--helper approach" can unfortunately not be used here: on Windows we face the very specific problem that a system() call in Perl seems to close stdin in the parent process when the spawned process consumes even one character from stdin. Which prevents us from implementing the main loop in C and still trying to hand off to the Perl script.

The very real downside of the approach we have to take here is that the test suite won't pass with GIT_TEST_ADD_I_USE_BUILTIN=true until the conversion is complete (the --helper approach would have let it pass, even at each of the incremental conversion steps).

Any suggestions how to improve that commit message?

Ciao,
Dscho

#83 lore raw

Re: [PATCH v5 1/9] Start to implement a built-in version of `git add --interactive`

Quoted 7 lines · Johannes Schindelin

And I agree that this sidetrack is totally irrelevant for the patch under discussion.

I do think, however, that the discussion of "we wanted to do it the other way, but when we tried, it did not work" is relevant, even if I shortened it to "we use a different approach than previous conversions, because that previous approach would not work".

Regardless of the language the scripted version was written in, I think the '--helper' approach is always the poorer choice between the two [*1*]. It limits the modular decomposition to what suits the original language, the impedance mismatch between the original and target language forces us to unnatural style of inter module communication, and the unnatural interface layer, which we know has to be discarded at the end, must be written [*2*].

So, I'd prefer to see "because this is a better way in the longer term" over "because the --helper approach would not work".

[Footnote]

*1* In only one case I would recommend using "--helper" approach,
though. When you are not expecting the developer to be able to come up with a better split of the program into modules than how the scripted version is, and you want to ensure that the developer have something to show when they faild to complete the project after N weeks. You are a more experienced developer than an average GSoC student, and there is no pencils-down time, so the exception would not apply.

*2* In "git submodule" for example it was quite natural for the
module that gives a list of submodules with its traits the program cares about to be written as a shell function that writes the data to its standard output. And consuming modules sit at the downstream of a pipe, accepting its output. When you are writing these modules both in C, you wouldn't connect them with pipe to carry the list of submodules, but a piecemeal conversion using the "--helper" approach meant that there always remained _some_ consumer that wants to read from the pipe, so long after the module lister was rewritten in C, it still needed to support a mode where it sends its output to the pipe, instead of just passing an array of structures.

#84 lore raw

Re: [PATCH v5 1/9] Start to implement a built-in version of `git add --interactive`

Hi Junio,

Quoted 18 lines · Junio C Hamano
Quoted 7 lines · Johannes Schindelin

And I agree that this sidetrack is totally irrelevant for the patch under discussion.

I do think, however, that the discussion of "we wanted to do it the other way, but when we tried, it did not work" is relevant, even if I shortened it to "we use a different approach than previous conversions, because that previous approach would not work".

Regardless of the language the scripted version was written in, I think the '--helper' approach is always the poorer choice between the two [*1*]. It limits the modular decomposition to what suits the original language, the impedance mismatch between the original and target language forces us to unnatural style of inter module communication, and the unnatural interface layer, which we know has to be discarded at the end, must be written [*2*].

Quoted 2 lines

So, I'd prefer to see "because this is a better way in the longer term" over "because the --helper approach would not work".

Hmm. I feel distinctly unheard.

It may appear compelling, conceptually, to shun the --helper approach, but the more important reality is that it is the only one that makes an incremental conversion possible at all.

It took an entire month of 60-hour weeks to complete the conversion of git add -i/git add -p to C, and only at the very end was I able to run the test suite with GIT_TEST_ADD_I_USE_BUILTIN=true and see it pass.

That is an awfully long time, and you know fully well that this amount of work equates to three to four Outreachy/GSoC seasons. That would be an insane amount of time to go without the confidence of a passing test suite.

In contrast, we were able to complete the conversions of the interactive rebase as well as of git stash within _a single season_. I attribute a large part of that success to the ability to keep the tests green during the incremental conversion _because of_ the --helper approach.

So no, I do not think that your suggestion to reword the commit message is something we want to do. Instead, I think the commit message needs to be rephrased until I get the point across clearly.

It is indeed _in spite of_ the success of the --helper approach that we cannot use it here.

Ciao,
Dscho

Quoted 25 lines

[Footnote]

*1* In only one case I would recommend using "--helper" approach,
though. When you are not expecting the developer to be able to come up with a better split of the program into modules than how the scripted version is, and you want to ensure that the developer have something to show when they faild to complete the project after N weeks. You are a more experienced developer than an average GSoC student, and there is no pencils-down time, so the exception would not apply.

*2* In "git submodule" for example it was quite natural for the
module that gives a list of submodules with its traits the program cares about to be written as a shell function that writes the data to its standard output. And consuming modules sit at the downstream of a pipe, accepting its output. When you are writing these modules both in C, you wouldn't connect them with pipe to carry the list of submodules, but a piecemeal conversion using the "--helper" approach meant that there always remained _some_ consumer that wants to read from the pipe, so long after the module lister was rewritten in C, it still needed to support a mode where it sends its output to the pipe, instead of just passing an array of structures.

#85 lore raw

Re: [PATCH v5 1/9] Start to implement a built-in version of `git add --interactive`

Quoted 8 lines · Johannes Schindelin
Quoted 8 lines

Regardless of the language the scripted version was written in, I think the '--helper' approach is always the poorer choice between the two [*1*]. It limits the modular decomposition to what suits the original language, the impedance mismatch between the original and target language forces us to unnatural style of inter module communication, and the unnatural interface layer, which we know has to be discarded at the end, must be written [*2*].

Quoted 4 lines
Quoted 2 lines

So, I'd prefer to see "because this is a better way in the longer term" over "because the --helper approach would not work".

Hmm. I feel distinctly unheard.

The feeling is mutual ;-)

Quoted 8 lines

It may appear compelling, conceptually, to shun the --helper approach, but the more important reality is that it is the only one that makes an incremental conversion possible at all.

It took an entire month of 60-hour weeks to complete the conversion of git add -i/git add -p to C, and only at the very end was I able to run the test suite with GIT_TEST_ADD_I_USE_BUILTIN=true and see it pass.

Yeah, that is developer comfort, and of course it is nice to have than not to have it.

But compared to the downside impact to the quality of end result that is inherent to the '--helper' approach, I'd prioritize the quality of the end result over developer comfort.

Quoted 2 lines

It is indeed _in spite of_ the success of the --helper approach that we cannot use it here.

As I do not see those past '--helper' ones necessarily successes, we must agree to disagree here.

In any case, the log message needs to express why _you_ ended up taking the non-helper approach. Even though it is far less relevant, compared to that, what other approach you instead wanted to take, I do not veto you from having your own opinion.

Quoted 24 lines
Quoted 24 lines

[Footnote]

*1* In only one case I would recommend using "--helper" approach,
though. When you are not expecting the developer to be able to come up with a better split of the program into modules than how the scripted version is, and you want to ensure that the developer have something to show when they faild to complete the project after N weeks. You are a more experienced developer than an average GSoC student, and there is no pencils-down time, so the exception would not apply.

*2* In "git submodule" for example it was quite natural for the
module that gives a list of submodules with its traits the program cares about to be written as a shell function that writes the data to its standard output. And consuming modules sit at the downstream of a pipe, accepting its output. When you are writing these modules both in C, you wouldn't connect them with pipe to carry the list of submodules, but a piecemeal conversion using the "--helper" approach meant that there always remained _some_ consumer that wants to read from the pipe, so long after the module lister was rewritten in C, it still needed to support a mode where it sends its output to the pipe, instead of just passing an array of structures.

#86 lore raw

Re: [PATCH v5 1/9] Start to implement a built-in version of `git add --interactive`

Hi Junio,

Quoted 10 lines · Junio C Hamano
Quoted 8 lines · Johannes Schindelin
Quoted 8 lines

Regardless of the language the scripted version was written in, I think the '--helper' approach is always the poorer choice between the two [*1*]. It limits the modular decomposition to what suits the original language, the impedance mismatch between the original and target language forces us to unnatural style of inter module communication, and the unnatural interface layer, which we know has to be discarded at the end, must be written [*2*].

Quoted 6 lines
Quoted 4 lines
Quoted 2 lines

So, I'd prefer to see "because this is a better way in the longer term" over "because the --helper approach would not work".

Hmm. I feel distinctly unheard.

The feeling is mutual ;-)

Maybe I could take your comments more seriously if you acknowledged the fact that I am _very_ familiar with the vagaries of converting scripts to C. Like, very, very, very familiar.

Quoted 11 lines
Quoted 8 lines

It may appear compelling, conceptually, to shun the --helper approach, but the more important reality is that it is the only one that makes an incremental conversion possible at all.

It took an entire month of 60-hour weeks to complete the conversion of git add -i/git add -p to C, and only at the very end was I able to run the test suite with GIT_TEST_ADD_I_USE_BUILTIN=true and see it pass.

Yeah, that is developer comfort, and of course it is nice to have than not to have it.

Comfort has little to do with it. Driving out bugs has a lot more to do with it. Which is the point I am trying to get across the entire time.

Quoted 9 lines

But compared to the downside impact to the quality of end result that is inherent to the '--helper' approach, I'd prioritize the quality of the end result over developer comfort.

Quoted 2 lines

It is indeed _in spite of_ the success of the --helper approach that we cannot use it here.

As I do not see those past '--helper' ones necessarily successes, we must agree to disagree here.

Right. But if I recall, you never even saw the need for the conversions in the first place. Maybe you still don't?

Quoted 4 lines

In any case, the log message needs to express why _you_ ended up taking the non-helper approach. Even though it is far less relevant, compared to that, what other approach you instead wanted to take, I do not veto you from having your own opinion.

Okay. I will take that as an indication that I can go forward with the latest proposal. After all, I described pretty well, I think, why _I_ ended up taking the non-helper approach.

Thanks,
Dscho

Quoted 25 lines
Quoted 24 lines
Quoted 24 lines

[Footnote]

*1* In only one case I would recommend using "--helper" approach,
though. When you are not expecting the developer to be able to come up with a better split of the program into modules than how the scripted version is, and you want to ensure that the developer have something to show when they faild to complete the project after N weeks. You are a more experienced developer than an average GSoC student, and there is no pencils-down time, so the exception would not apply.

*2* In "git submodule" for example it was quite natural for the
module that gives a list of submodules with its traits the program cares about to be written as a shell function that writes the data to its standard output. And consuming modules sit at the downstream of a pipe, accepting its output. When you are writing these modules both in C, you wouldn't connect them with pipe to carry the list of submodules, but a piecemeal conversion using the "--helper" approach meant that there always remained _some_ consumer that wants to read from the pipe, so long after the module lister was rewritten in C, it still needed to support a mode where it sends its output to the pipe, instead of just passing an array of structures.

#87 lore raw

Re: [PATCH v5 1/9] Start to implement a built-in version of `git add --interactive`

Quoted 5 lines · Johannes Schindelin
Quoted 2 lines

As I do not see those past '--helper' ones necessarily successes, we must agree to disagree here.

Right. But if I recall, you never even saw the need for the conversions in the first place. Maybe you still don't?

You probably are forgetting the fact that I was very supportive for the rewrite of checkout, commit and format-patch (the last one being my favorite) in C from scripted Porcelain.

None of these were '--helper' style conversion and I would consider them much more successful than the recent ones, some of which still suffer from impedance mismatch bugs (e.g. some parts of the C implementation work on the in-core index, while other parts working on the on the on-disk index, letting them become out of sync and introducing bugs).

#88 lore raw

PATCH v5 2/9 diff: export diffstat interface

In reply to Johannes Schindelin via GitGitGadget

From: Daniel Ferreira <[email protected]>

Make the diffstat interface (namely, the diffstat_t struct and compute_diffstat) no longer be internal to diff.c and allow it to be used by other parts of git.

This is helpful for code that may want to easily extract information from files using the diff machinery, while flushing it differently from how the show_* functions used by diff_flush() do it. One example is the builtin implementation of git-add--interactive's status.

---
 diff.c | 39 ++++++++++++++++-----------------------
 diff.h | 20 ++++++++++++++++++++
 2 files changed, 36 insertions(+), 23 deletions(-)
diff.c +16 -23
index afe4400a60..5703a9b78f 100644
@@ -2495,22 +2495,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)
@@ -3157,7 +3141,7 @@ static void show_dirstat_by_line(struct diffstat_t *data, struct diff_options *o
 	gather_dirstat(options, &dir, changed, "", 0);
 }
 
-static void free_diffstat_info(struct diffstat_t *diffstat)
+void free_diffstat_info(struct diffstat_t *diffstat)
 {
 	int i;
 	for (i = 0; i < diffstat->nr; i++) {
@@ -6283,12 +6267,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)
@@ -6621,6 +6600,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,
diff.h +20 -0
index 7f8f024feb..d986ddc3b5 100644
@@ -245,6 +245,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,
@@ -334,6 +350,10 @@ 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);
+void free_diffstat_info(struct diffstat_t *diffstat);
+
 #define DIFF_SETUP_REVERSE      	1
 #define DIFF_SETUP_USE_SIZE_CACHE	4
 
Signature · 1 line
gitgitgadget
#89 lore raw

Re: [PATCH v5 2/9] diff: export diffstat interface

Quoted 11 lines · Daniel Ferreira via GitGitGadget

From: Daniel Ferreira <[email protected]>

Make the diffstat interface (namely, the diffstat_t struct and compute_diffstat) no longer be internal to diff.c and allow it to be used by other parts of git.

This is helpful for code that may want to easily extract information from files using the diff machinery, while flushing it differently from how the show_* functions used by diff_flush() do it. One example is the builtin implementation of git-add--interactive's status.

Quoted 7 lines
---
 diff.c | 39 ++++++++++++++++-----------------------
 diff.h | 20 ++++++++++++++++++++
 2 files changed, 36 insertions(+), 23 deletions(-)

Looks good. The diffstat API seems to be quite well isolated to allow this with minimum (eh, rather no) changes.

Thanks.

Quoted 109 lines
diff.c +16 -23
index afe4400a60..5703a9b78f 100644
@@ -2495,22 +2495,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)
@@ -3157,7 +3141,7 @@ static void show_dirstat_by_line(struct diffstat_t *data, struct diff_options *o
 	gather_dirstat(options, &dir, changed, "", 0);
 }
 
-static void free_diffstat_info(struct diffstat_t *diffstat)
+void free_diffstat_info(struct diffstat_t *diffstat)
 {
 	int i;
 	for (i = 0; i < diffstat->nr; i++) {
@@ -6283,12 +6267,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)
@@ -6621,6 +6600,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,
diff.h +20 -0
index 7f8f024feb..d986ddc3b5 100644
@@ -245,6 +245,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,
@@ -334,6 +350,10 @@ 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);
+void free_diffstat_info(struct diffstat_t *diffstat);
+
 #define DIFF_SETUP_REVERSE      	1
 #define DIFF_SETUP_USE_SIZE_CACHE	4
#90 lore raw

PATCH v5 3/9 built-in add -i: implement the `status` command

In reply to Johannes Schindelin via GitGitGadget

From: Daniel Ferreira <[email protected]>

This implements the status command of git add -i. The data structures introduced in this commit will be extended later, as needed.

At this point, we re-implement only part of the list_and_choose() function of the Perl script git-add--interactive.perl and call it list(). It does not yet color anything, or do columns, or allow user input.

Over the course of the next commits, we will introduce a list_and_choose() function that uses list() to display the list of options and let the user choose one or more of the displayed items. This will be used to implement the main loop of the built-in git add -i, at which point the new status command can actually be used.

---
 add-interactive.c | 251 +++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 250 insertions(+), 1 deletion(-)
add-interactive.c +250 -1
index 482e458dc6..aa35184d87 100644
@@ -1,7 +1,256 @@
 #include "cache.h"
 #include "add-interactive.h"
+#include "diffcore.h"
+#include "revision.h"
+#include "refs.h"
+#include "string-list.h"
+
+struct add_i_state {
+	struct repository *r;
+};
+
+static void init_add_i_state(struct add_i_state *s, struct repository *r)
+{
+       s->r = r;
+}
+
+struct list_options {
+	const char *header;
+	void (*print_item)(int i, struct string_list_item *item, void *print_item_data);
+	void *print_item_data;
+};
+
+static void list(struct string_list *list, struct list_options *opts)
+{
+	int i;
+
+	if (!list->nr)
+		return;
+
+	if (opts->header)
+		printf("%s\n", opts->header);
+
+	for (i = 0; i < list->nr; i++) {
+		opts->print_item(i, list->items + i, opts->print_item_data);
+		putchar('\n');
+	}
+}
+
+struct adddel {
+	uintmax_t add, del;
+	unsigned seen:1, binary:1;
+};
+
+struct file_item {
+	struct adddel index, worktree;
+};
+
+static void add_file_item(struct string_list *files, const char *name)
+{
+	struct file_item *item = xcalloc(sizeof(*item), 1);
+
+	string_list_append(files, name)->util = item;
+}
+
+struct pathname_entry {
+	struct hashmap_entry ent;
+	const char *name;
+	struct file_item *item;
+};
+
+static int pathname_entry_cmp(const void *unused_cmp_data,
+			      const struct hashmap_entry *he1,
+			      const struct hashmap_entry *he2,
+			      const void *name)
+{
+	const struct pathname_entry *e1 =
+		container_of(he1, const struct pathname_entry, ent);
+	const struct pathname_entry *e2 =
+		container_of(he2, const struct pathname_entry, ent);
+
+	return strcmp(e1->name, name ? (const char *)name : e2->name);
+}
+
+struct collection_status {
+	enum { FROM_WORKTREE = 0, FROM_INDEX = 1 } phase;
+
+	const char *reference;
+
+	struct string_list *files;
+	struct hashmap file_map;
+};
+
+static void collect_changes_cb(struct diff_queue_struct *q,
+			       struct diff_options *options,
+			       void *data)
+{
+	struct collection_status *s = data;
+	struct diffstat_t stat = { 0 };
+	int i;
+
+	if (!q->nr)
+		return;
+
+	compute_diffstat(options, &stat, q);
+
+	for (i = 0; i < stat.nr; i++) {
+		const char *name = stat.files[i]->name;
+		int hash = strhash(name);
+		struct pathname_entry *entry;
+		struct file_item *file_item;
+		struct adddel *adddel;
+
+		entry = hashmap_get_entry_from_hash(&s->file_map, hash, name,
+						    struct pathname_entry, ent);
+		if (!entry) {
+			add_file_item(s->files, name);
+
+			entry = xcalloc(sizeof(*entry), 1);
+			hashmap_entry_init(&entry->ent, hash);
+			entry->name = s->files->items[s->files->nr - 1].string;
+			entry->item = s->files->items[s->files->nr - 1].util;
+			hashmap_add(&s->file_map, &entry->ent);
+		}
+
+		file_item = entry->item;
+		adddel = s->phase == FROM_INDEX ?
+			&file_item->index : &file_item->worktree;
+		adddel->seen = 1;
+		adddel->add = stat.files[i]->added;
+		adddel->del = stat.files[i]->deleted;
+		if (stat.files[i]->is_binary)
+			adddel->binary = 1;
+	}
+	free_diffstat_info(&stat);
+}
+
+static int get_modified_files(struct repository *r, struct string_list *files,
+			      const struct pathspec *ps)
+{
+	struct object_id head_oid;
+	int is_initial = !resolve_ref_unsafe("HEAD", RESOLVE_REF_READING,
+					     &head_oid, NULL);
+	struct collection_status s = { FROM_WORKTREE };
+
+	if (discard_index(r->index) < 0 ||
+	    repo_read_index_preload(r, ps, 0) < 0)
+		return error(_("could not read index"));
+
+	string_list_clear(files, 1);
+	s.files = files;
+	hashmap_init(&s.file_map, pathname_entry_cmp, NULL, 0);
+
+	for (s.phase = FROM_WORKTREE; s.phase <= FROM_INDEX; s.phase++) {
+		struct rev_info rev;
+		struct setup_revision_opt opt = { 0 };
+
+		opt.def = is_initial ?
+			empty_tree_oid_hex() : oid_to_hex(&head_oid);
+
+		init_revisions(&rev, NULL);
+		setup_revisions(0, NULL, &rev, &opt);
+
+		rev.diffopt.output_format = DIFF_FORMAT_CALLBACK;
+		rev.diffopt.format_callback = collect_changes_cb;
+		rev.diffopt.format_callback_data = &s;
+
+		if (ps)
+			copy_pathspec(&rev.prune_data, ps);
+
+		if (s.phase == FROM_INDEX)
+			run_diff_index(&rev, 1);
+		else {
+			rev.diffopt.flags.ignore_dirty_submodules = 1;
+			run_diff_files(&rev, 0);
+		}
+	}
+	hashmap_free_entries(&s.file_map, struct pathname_entry, ent);
+
+	/* While the diffs are ordered already, we ran *two* diffs... */
+	string_list_sort(files);
+
+	return 0;
+}
+
+static void render_adddel(struct strbuf *buf,
+				struct adddel *ad, const char *no_changes)
+{
+	if (ad->binary)
+		strbuf_addstr(buf, _("binary"));
+	else if (ad->seen)
+		strbuf_addf(buf, "+%"PRIuMAX"/-%"PRIuMAX,
+			    (uintmax_t)ad->add, (uintmax_t)ad->del);
+	else
+		strbuf_addstr(buf, no_changes);
+}
+
+struct print_file_item_data {
+	const char *modified_fmt;
+	struct strbuf buf, index, worktree;
+};
+
+static void print_file_item(int i, struct string_list_item *item,
+			    void *print_file_item_data)
+{
+	struct file_item *c = item->util;
+	struct print_file_item_data *d = print_file_item_data;
+
+	strbuf_reset(&d->index);
+	strbuf_reset(&d->worktree);
+	strbuf_reset(&d->buf);
+
+	render_adddel(&d->worktree, &c->worktree, _("nothing"));
+	render_adddel(&d->index, &c->index, _("unchanged"));
+	strbuf_addf(&d->buf, d->modified_fmt,
+		    d->index.buf, d->worktree.buf, item->string);
+
+	printf(" %2d: %s", i + 1, d->buf.buf);
+}
+
+static int run_status(struct add_i_state *s, const struct pathspec *ps,
+		      struct string_list *files, struct list_options *opts)
+{
+	if (get_modified_files(s->r, files, ps) < 0)
+		return -1;
+
+	list(files, opts);
+	putchar('\n');
+
+	return 0;
+}
 
 int run_add_i(struct repository *r, const struct pathspec *ps)
 {
-	die(_("No commands are available in the built-in `git add -i` yet!"));
+	struct add_i_state s = { NULL };
+	struct print_file_item_data print_file_item_data = {
+		"%12s %12s %s", STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
+	};
+	struct list_options opts = {
+		NULL, print_file_item, &print_file_item_data
+	};
+	struct strbuf header = STRBUF_INIT;
+	struct string_list files = STRING_LIST_INIT_DUP;
+	int res = 0;
+
+	init_add_i_state(&s, r);
+	strbuf_addstr(&header, "      ");
+	strbuf_addf(&header, print_file_item_data.modified_fmt,
+		    _("staged"), _("unstaged"), _("path"));
+	opts.header = header.buf;
+
+	if (discard_index(r->index) < 0 ||
+	    repo_read_index(r) < 0 ||
+	    repo_refresh_and_write_index(r, REFRESH_QUIET, 0, 1,
+					 NULL, NULL, NULL) < 0)
+		warning(_("could not refresh index"));
+
+	res = run_status(&s, ps, &files, &opts);
+
+	string_list_clear(&files, 1);
+	strbuf_release(&print_file_item_data.buf);
+	strbuf_release(&print_file_item_data.index);
+	strbuf_release(&print_file_item_data.worktree);
+	strbuf_release(&header);
+
+	return res;
 }
Signature · 1 line
gitgitgadget
#91 lore raw

Re: [PATCH v5 3/9] built-in add -i: implement the `status` command

Quoted 9 lines · Daniel Ferreira via GitGitGadget

From: Daniel Ferreira <[email protected]>

This implements the status command of git add -i. The data structures introduced in this commit will be extended later, as needed.

At this point, we re-implement only part of the list_and_choose() function of the Perl script git-add--interactive.perl and call it list(). It does not yet color anything, or do columns, or allow user input.

OK, so that is why we unconditionally do the status thing directly inside run_add_i() and nothing else. Makes sense.

Quoted 5 lines

Over the course of the next commits, we will introduce a list_and_choose() function that uses list() to display the list of options and let the user choose one or more of the displayed items. This will be used to implement the main loop of the built-in git add -i, at which point the new status command can actually be used.

Very well written.

#92 lore raw

PATCH v5 4/9 built-in add -i: color the header in the `status` command

In reply to Johannes Schindelin via GitGitGadget

From: =?UTF-8?q?Slavica=20=C4=90uki=C4=87?= <[email protected]>

For simplicity, we only implemented the status command without colors. This patch starts adding color, matching what the Perl script git-add--interactive.perl does.

---
 add-interactive.c | 41 +++++++++++++++++++++++++++++++++++++----
 1 file changed, 37 insertions(+), 4 deletions(-)
add-interactive.c +37 -4
index aa35184d87..174e07ce83 100644
@@ -1,5 +1,7 @@
 #include "cache.h"
 #include "add-interactive.h"
+#include "color.h"
+#include "config.h"
 #include "diffcore.h"
 #include "revision.h"
 #include "refs.h"
@@ -7,11 +9,40 @@
 
 struct add_i_state {
 	struct repository *r;
+	int use_color;
+	char header_color[COLOR_MAXLEN];
 };
 
+static void init_color(struct repository *r, struct add_i_state *s,
+		       const char *slot_name, char *dst,
+		       const char *default_color)
+{
+	char *key = xstrfmt("color.interactive.%s", slot_name);
+	const char *value;
+
+	if (!s->use_color)
+		dst[0] = '\0';
+	else if (repo_config_get_value(r, key, &value) ||
+		 color_parse(value, dst))
+		strlcpy(dst, default_color, COLOR_MAXLEN);
+
+	free(key);
+}
+
 static void init_add_i_state(struct add_i_state *s, struct repository *r)
 {
-       s->r = r;
+	const char *value;
+
+	s->r = r;
+
+	if (repo_config_get_value(r, "color.interactive", &value))
+		s->use_color = -1;
+	else
+		s->use_color =
+			git_config_colorbool("color.interactive", value);
+	s->use_color = want_color(s->use_color);
+
+	init_color(r, s, "header", s->header_color, GIT_COLOR_BOLD);
 }
 
 struct list_options {
@@ -20,7 +51,8 @@ struct list_options {
 	void *print_item_data;
 };
 
-static void list(struct string_list *list, struct list_options *opts)
+static void list(struct add_i_state *s, struct string_list *list,
+		 struct list_options *opts)
 {
 	int i;
 
@@ -28,7 +60,8 @@ static void list(struct string_list *list, struct list_options *opts)
 		return;
 
 	if (opts->header)
-		printf("%s\n", opts->header);
+		color_fprintf_ln(stdout, s->header_color,
+				 "%s", opts->header);
 
 	for (i = 0; i < list->nr; i++) {
 		opts->print_item(i, list->items + i, opts->print_item_data);
@@ -213,7 +246,7 @@ static int run_status(struct add_i_state *s, const struct pathspec *ps,
 	if (get_modified_files(s->r, files, ps) < 0)
 		return -1;
 
-	list(files, opts);
+	list(s, files, opts);
 	putchar('\n');
 
 	return 0;
Signature · 1 line
gitgitgadget
#93 lore raw

PATCH v5 5/9 built-in add -i: implement the main loop

In reply to Johannes Schindelin via GitGitGadget

From: Johannes Schindelin <[email protected]>

The reason why we did not start with the main loop to begin with is that it is the first user of list_and_choose(), which uses the list() function that we conveniently introduced for use by the status command.

Apart from the "and choose" part, there are more differences between the way the status command calls the list_and_choose() function in the Perl version of git add -i compared to the other callers of said function. The most important ones:

- The list is not only shown, but the user is also asked to make a choice, possibly selecting multiple entries.

- The list of items is prefixed with a marker indicating what items have been selected, if multi-selection is allowed.

- Initially, for each item a unique prefix (if there exists any within the given parameters) is determined, and shown in the list, and accepted as a shortcut for the selection.

These features will be implemented later, except the part where the user can choose a command. At this stage, though, the built-in git add -i still only supports the status command, with the remaining commands to follow over the course of the next commits.

In addition, we also modify list() to support displaying the commands in columns, even if there is currently only one.

The Perl script git-add--interactive.perl mixed the purposes of the "list" and the "and choose" part into the same function. In the C version, we will keep them separate instead, calling the list() function from the list_and_choose() function.

Note that we only have a prompt ending in a single ">" at this stage; later commits will add commands that display a double ">>" to indicate that the user is in a different loop than the main one.

---
 add-interactive.c | 135 +++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 133 insertions(+), 2 deletions(-)
add-interactive.c +133 -2
index 174e07ce83..c6f7fbad36 100644
@@ -46,6 +46,7 @@ static void init_add_i_state(struct add_i_state *s, struct repository *r)
 }
 
 struct list_options {
+	int columns;
 	const char *header;
 	void (*print_item)(int i, struct string_list_item *item, void *print_item_data);
 	void *print_item_data;
@@ -54,7 +55,7 @@ struct list_options {
 static void list(struct add_i_state *s, struct string_list *list,
 		 struct list_options *opts)
 {
-	int i;
+	int i, last_lf = 0;
 
 	if (!list->nr)
 		return;
@@ -65,8 +66,96 @@ static void list(struct add_i_state *s, struct string_list *list,
 
 	for (i = 0; i < list->nr; i++) {
 		opts->print_item(i, list->items + i, opts->print_item_data);
+
+		if ((opts->columns) && ((i + 1) % (opts->columns))) {
+			putchar('\t');
+			last_lf = 0;
+		}
+		else {
+			putchar('\n');
+			last_lf = 1;
+		}
+	}
+
+	if (!last_lf)
 		putchar('\n');
+}
+struct list_and_choose_options {
+	struct list_options list_opts;
+
+	const char *prompt;
+};
+
+#define LIST_AND_CHOOSE_ERROR (-1)
+#define LIST_AND_CHOOSE_QUIT  (-2)
+
+/*
+ * Returns the selected index.
+ *
+ * If an error occurred, returns `LIST_AND_CHOOSE_ERROR`. Upon EOF,
+ * `LIST_AND_CHOOSE_QUIT` is returned.
+ */
+static ssize_t list_and_choose(struct add_i_state *s, struct string_list *items,
+			       struct list_and_choose_options *opts)
+{
+	struct strbuf input = STRBUF_INIT;
+	ssize_t res = LIST_AND_CHOOSE_ERROR;
+
+	for (;;) {
+		char *p, *endp;
+
+		strbuf_reset(&input);
+
+		list(s, items, &opts->list_opts);
+
+		printf("%s%s", opts->prompt, "> ");
+		fflush(stdout);
+
+		if (strbuf_getline(&input, stdin) == EOF) {
+			putchar('\n');
+			res = LIST_AND_CHOOSE_QUIT;
+			break;
+		}
+		strbuf_trim(&input);
+
+		if (!input.len)
+			break;
+
+		p = input.buf;
+		for (;;) {
+			size_t sep = strcspn(p, " \t\r\n,");
+			ssize_t index = -1;
+
+			if (!sep) {
+				if (!*p)
+					break;
+				p++;
+				continue;
+			}
+
+			if (isdigit(*p)) {
+				index = strtoul(p, &endp, 10) - 1;
+				if (endp != p + sep)
+					index = -1;
+			}
+
+			p[sep] = '\0';
+			if (index < 0 || index >= items->nr)
+				printf(_("Huh (%s)?\n"), p);
+			else {
+				res = index;
+				break;
+			}
+
+			p += sep + 1;
+		}
+
+		if (res != LIST_AND_CHOOSE_ERROR)
+			break;
 	}
+
+	strbuf_release(&input);
+	return res;
 }
 
 struct adddel {
@@ -252,20 +341,48 @@ static int run_status(struct add_i_state *s, const struct pathspec *ps,
 	return 0;
 }
 
+typedef int (*command_t)(struct add_i_state *s, const struct pathspec *ps,
+			 struct string_list *files,
+			 struct list_options *opts);
+
+static void print_command_item(int i, struct string_list_item *item,
+			       void *print_command_item_data)
+{
+	printf(" %2d: %s", i + 1, item->string);
+}
+
 int run_add_i(struct repository *r, const struct pathspec *ps)
 {
 	struct add_i_state s = { NULL };
+	struct list_and_choose_options main_loop_opts = {
+		{ 4, N_("*** Commands ***"), print_command_item, NULL },
+		N_("What now")
+	};
+	struct {
+		const char *string;
+		command_t command;
+	} command_list[] = {
+		{ "status", run_status },
+	};
+	struct string_list commands = STRING_LIST_INIT_NODUP;
+
 	struct print_file_item_data print_file_item_data = {
 		"%12s %12s %s", STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
 	};
 	struct list_options opts = {
-		NULL, print_file_item, &print_file_item_data
+		0, NULL, print_file_item, &print_file_item_data
 	};
 	struct strbuf header = STRBUF_INIT;
 	struct string_list files = STRING_LIST_INIT_DUP;
+	ssize_t i;
 	int res = 0;
 
+	for (i = 0; i < ARRAY_SIZE(command_list); i++)
+		string_list_append(&commands, command_list[i].string)
+			->util = command_list[i].command;
+
 	init_add_i_state(&s, r);
+
 	strbuf_addstr(&header, "      ");
 	strbuf_addf(&header, print_file_item_data.modified_fmt,
 		    _("staged"), _("unstaged"), _("path"));
@@ -279,11 +396,25 @@ int run_add_i(struct repository *r, const struct pathspec *ps)
 
 	res = run_status(&s, ps, &files, &opts);
 
+	for (;;) {
+		i = list_and_choose(&s, &commands, &main_loop_opts);
+		if (i == LIST_AND_CHOOSE_QUIT) {
+			printf(_("Bye.\n"));
+			res = 0;
+			break;
+		}
+		if (i != LIST_AND_CHOOSE_ERROR) {
+			command_t command = commands.items[i].util;
+			res = command(&s, ps, &files, &opts);
+		}
+	}
+
 	string_list_clear(&files, 1);
 	strbuf_release(&print_file_item_data.buf);
 	strbuf_release(&print_file_item_data.index);
 	strbuf_release(&print_file_item_data.worktree);
 	strbuf_release(&header);
+	string_list_clear(&commands, 0);
 
 	return res;
 }
Signature · 1 line
gitgitgadget
#94 lore raw

Re: [PATCH v5 5/9] built-in add -i: implement the main loop

"Johannes Schindelin via GitGitGadget" <[email protected]>

Quoted 7 lines · writes:

From: Johannes Schindelin <[email protected]>

The reason why we did not start with the main loop to begin with is that it is the first user of list_and_choose(), which uses the list() function that we conveniently introduced for use by the status command.

Quoted 7 lines

Apart from the "and choose" part, there are more differences between the way the status command calls the list_and_choose() function in the Perl version of git add -i compared to the other callers of said function. The most important ones:

- The list is not only shown, but the user is also asked to make a choice, possibly selecting multiple entries.

The list_and_choose() we have here shows and lets users choose and returns the choice, but the above makes it sound as if it only shows and the caller is responsible for asking the end-user input. Is this description outdated or something?

Perl allows us to return multiple choices, where it is a bit hard to express it in C (perhaps because we are passing in an array of structs to be shown as choices, list_and_choose could set a bit in these structs to signal "this one, that one and that other one was chosen", returning how many are chosen in total, to extend the version here to bring it to feature-parity?). So at this step, it only lets the user one choice (or abort or ask for help). Isn't the lack of multiple choice the only difference this bullet item wants to highlight?

Quoted 4 lines

The Perl script git-add--interactive.perl mixed the purposes of the "list" and the "and choose" part into the same function. In the C version, we will keep them separate instead, calling the list() function from the list_and_choose() function.

That makes sense.

add-interactive.c static void list(struct add_i_state *s, struct string_list *list, · line 105

Quoted 8 lines
add-interactive.c +8 -0
+static ssize_t list_and_choose(struct add_i_state *s, struct string_list *items,
+			       struct list_and_choose_options *opts)
+{
+	struct strbuf input = STRBUF_INIT;
+	ssize_t res = LIST_AND_CHOOSE_ERROR;
+
+	for (;;) {
+		char *p, *endp;

The scope of endp looks way too wide in this function, isn't it? Even in the final state of the series, it only gets used to parse an integer input using strtoul, inside a block of three lines.

Other than that, the code at this step was a pleasant read overall.

Thanks.

#95 lore raw

Re: [PATCH v5 5/9] built-in add -i: implement the main loop

Hi Junio,

Quoted 23 lines · Junio C Hamano

"Johannes Schindelin via GitGitGadget" <[email protected]>

Quoted 7 lines · writes:

From: Johannes Schindelin <[email protected]>

The reason why we did not start with the main loop to begin with is that it is the first user of list_and_choose(), which uses the list() function that we conveniently introduced for use by the status command.

Quoted 7 lines

Apart from the "and choose" part, there are more differences between the way the status command calls the list_and_choose() function in the Perl version of git add -i compared to the other callers of said function. The most important ones:

- The list is not only shown, but the user is also asked to make a choice, possibly selecting multiple entries.

The list_and_choose() we have here shows and lets users choose and returns the choice, but the above makes it sound as if it only shows and the caller is responsible for asking the end-user input. Is this description outdated or something?

Quoted 9 lines

Perl allows us to return multiple choices, where it is a bit hard to express it in C (perhaps because we are passing in an array of structs to be shown as choices, list_and_choose could set a bit in these structs to signal "this one, that one and that other one was chosen", returning how many are chosen in total, to extend the version here to bring it to feature-parity?). So at this step, it only lets the user one choice (or abort or ask for help). Isn't the lack of multiple choice the only difference this bullet item wants to highlight?

I changed the commit message to:

built-in add -i: implement the main loop

The reason why we did not start with the main loop to begin with is that it is the first user of list_and_choose(), which uses the list() function that we conveniently introduced for use by the status command.

In contrast to the Perl version, in the built-in interactive add, we will keep the list() function (which only displays items) and the list_and_choose() function (which uses list() to display the items, and only takes care of the "and choose" part) separate.

The list_and_choose() function, as implemented in git-add--interactive.perl knows a few more tricks than the function we introduce in this patch:

- There is a flag to let the user select multiple items.

- In multi-select mode, the list of items is prefixed with a marker indicating what items have been selected.

- Initially, for each item a unique prefix is determined (if there exists any within the given parameters), and shown in the list, and accepted as a shortcut for the selection.

These features will be implemented in the C version later.

This patch does not add any new main loop command, of course, the built-in git add -i still only supports the status command. The remaining commands to follow over the course of the next commits.

To accommodate for listing the commands in columns, preparing for the commands that will be implemented over the course of the next patches/patch series, we teach the list() function to do precisely that.

Note that we only have a prompt ending in a single ">" at this stage; later commits will add commands that display a double ">>" to indicate that the user is in a different loop than the main one.

Quoted 7 lines
Quoted 4 lines

The Perl script git-add--interactive.perl mixed the purposes of the "list" and the "and choose" part into the same function. In the C version, we will keep them separate instead, calling the list() function from the list_and_choose() function.

That makes sense.

Quoted 12 lines

add-interactive.c static void list(struct add_i_state *s, struct string_list *list, · line 105

Quoted 8 lines
add-interactive.c +8 -0
+static ssize_t list_and_choose(struct add_i_state *s, struct string_list *items,
+			       struct list_and_choose_options *opts)
+{
+	struct strbuf input = STRBUF_INIT;
+	ssize_t res = LIST_AND_CHOOSE_ERROR;
+
+	for (;;) {
+		char *p, *endp;

The scope of endp looks way too wide in this function, isn't it? Even in the final state of the series, it only gets used to parse an integer input using strtoul, inside a block of three lines.

True. I moved the declaration of endp into that three-line (now four-line) scope.

Quoted 1 line

Other than that, the code at this step was a pleasant read overall.

Thank you for your review!
Dscho

#96 lore raw

PATCH v5 6/9 built-in add -i: show unique prefixes of the commands

In reply to Johannes Schindelin via GitGitGadget

From: Johannes Schindelin <[email protected]>

Just like in the Perl script git-add--interactive.perl, for each command a unique prefix is determined (if there exists any within the given parameters), and shown in the list, and accepted as a shortcut for the command.

To determine the unique prefixes, as well as to look up the command in question, we use a copy of the list and sort it.

While this might seem like overkill for a single command, it will make much more sense when all the commands are implemented, and when we reuse the same logic to present a list of files to edit, with convenient unique prefixes.

At the start of the development of this patch series, a dedicated data structure was introduced that imitated the Trie that the Perl version implements. However, this was deemed overkill, and we now simply sort the list before determining the length of the unique prefixes by looking at each item's neighbor. As a bonus, we now use the same sorted list to perform a binary search using the user-provided prefix as search key.

---
 add-interactive.c | 188 +++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 177 insertions(+), 11 deletions(-)
add-interactive.c +177 -11
index c6f7fbad36..eb559555ad 100644
@@ -45,6 +45,132 @@ static void init_add_i_state(struct add_i_state *s, struct repository *r)
 	init_color(r, s, "header", s->header_color, GIT_COLOR_BOLD);
 }
 
+/*
+ * A "prefix item list" is a list of items that are identified by a string, and
+ * a unique prefix (if any) is determined for each item.
+ *
+ * It is implemented in the form of a pair of `string_list`s, the first one
+ * duplicating the strings, with the `util` field pointing at a structure whose
+ * first field must be `size_t prefix_length`.
+ *
+ * That `prefix_length` field will be computed by `find_unique_prefixes()`; It
+ * will be set to zero if no valid, unique prefix could be found.
+ *
+ * The second `string_list` is called `sorted` and does _not_ duplicate the
+ * strings but simply reuses the first one's, with the `util` field pointing at
+ * the `string_item_list` of the first `string_list`. It  will be populated and
+ * sorted by `find_unique_prefixes()`.
+ */
+struct prefix_item_list {
+	struct string_list items;
+	struct string_list sorted;
+	size_t min_length, max_length;
+};
+#define PREFIX_ITEM_LIST_INIT \
+	{ STRING_LIST_INIT_DUP, STRING_LIST_INIT_NODUP, 1, 4 }
+
+static void prefix_item_list_clear(struct prefix_item_list *list)
+{
+	string_list_clear(&list->items, 1);
+	string_list_clear(&list->sorted, 0);
+}
+
+static void extend_prefix_length(struct string_list_item *p,
+				 const char *other_string, size_t max_length)
+{
+	size_t *len = p->util;
+
+	if (!*len || memcmp(p->string, other_string, *len))
+		return;
+
+	for (;;) {
+		char c = p->string[*len];
+
+		/*
+		 * Is `p` a strict prefix of `other`? Or have we exhausted the
+		 * maximal length of the prefix? Or is the current character a
+		 * multi-byte UTF-8 one? If so, there is no valid, unique
+		 * prefix.
+		 */
+		if (!c || ++*len > max_length || !isascii(c)) {
+			*len = 0;
+			break;
+		}
+
+		if (c != other_string[*len - 1])
+			break;
+	}
+}
+
+static void find_unique_prefixes(struct prefix_item_list *list)
+{
+	size_t i;
+
+	if (list->sorted.nr == list->items.nr)
+		return;
+
+	string_list_clear(&list->sorted, 0);
+	/* Avoid reallocating incrementally */
+	list->sorted.items = xmalloc(st_mult(sizeof(*list->sorted.items),
+					     list->items.nr));
+	list->sorted.nr = list->sorted.alloc = list->items.nr;
+
+	for (i = 0; i < list->items.nr; i++) {
+		list->sorted.items[i].string = list->items.items[i].string;
+		list->sorted.items[i].util = list->items.items + i;
+	}
+
+	string_list_sort(&list->sorted);
+
+	for (i = 0; i < list->sorted.nr; i++) {
+		struct string_list_item *sorted_item = list->sorted.items + i;
+		struct string_list_item *item = sorted_item->util;
+		size_t *len = item->util;
+
+		*len = 0;
+		while (*len < list->min_length) {
+			char c = item->string[(*len)++];
+
+			if (!c || !isascii(c)) {
+				*len = 0;
+				break;
+			}
+		}
+
+		if (i > 0)
+			extend_prefix_length(item, sorted_item[-1].string,
+					     list->max_length);
+		if (i + 1 < list->sorted.nr)
+			extend_prefix_length(item, sorted_item[1].string,
+					     list->max_length);
+	}
+}
+
+static ssize_t find_unique(const char *string, struct prefix_item_list *list)
+{
+	int index = string_list_find_insert_index(&list->sorted, string, 1);
+	struct string_list_item *item;
+
+	if (list->items.nr != list->sorted.nr)
+		BUG("prefix_item_list in inconsistent state (%"PRIuMAX
+		    " vs %"PRIuMAX")",
+		    (uintmax_t)list->items.nr, (uintmax_t)list->sorted.nr);
+
+	if (index < 0)
+		item = list->sorted.items[-1 - index].util;
+	else if (index > 0 &&
+		 starts_with(list->sorted.items[index - 1].string, string))
+		return -1;
+	else if (index + 1 < list->sorted.nr &&
+		 starts_with(list->sorted.items[index + 1].string, string))
+		return -1;
+	else if (index < list->sorted.nr)
+		item = list->sorted.items[index].util;
+	else
+		return -1;
+	return item - list->items.items;
+}
+
 struct list_options {
 	int columns;
 	const char *header;
@@ -95,18 +221,21 @@ struct list_and_choose_options {
  * If an error occurred, returns `LIST_AND_CHOOSE_ERROR`. Upon EOF,
  * `LIST_AND_CHOOSE_QUIT` is returned.
  */
-static ssize_t list_and_choose(struct add_i_state *s, struct string_list *items,
+static ssize_t list_and_choose(struct add_i_state *s,
+			       struct prefix_item_list *items,
 			       struct list_and_choose_options *opts)
 {
 	struct strbuf input = STRBUF_INIT;
 	ssize_t res = LIST_AND_CHOOSE_ERROR;
 
+	find_unique_prefixes(items);
+
 	for (;;) {
 		char *p, *endp;
 
 		strbuf_reset(&input);
 
-		list(s, items, &opts->list_opts);
+		list(s, &items->items, &opts->list_opts);
 
 		printf("%s%s", opts->prompt, "> ");
 		fflush(stdout);
@@ -140,7 +269,10 @@ static ssize_t list_and_choose(struct add_i_state *s, struct string_list *items,
 			}
 
 			p[sep] = '\0';
-			if (index < 0 || index >= items->nr)
+			if (index < 0)
+				index = find_unique(p, items);
+
+			if (index < 0 || index >= items->items.nr)
 				printf(_("Huh (%s)?\n"), p);
 			else {
 				res = index;
@@ -306,6 +438,23 @@ static void render_adddel(struct strbuf *buf,
 		strbuf_addstr(buf, no_changes);
 }
 
+/* filters out prefixes which have special meaning to list_and_choose() */
+static int is_valid_prefix(const char *prefix, size_t prefix_len)
+{
+	return prefix_len && prefix &&
+		/*
+		 * We expect `prefix` to be NUL terminated, therefore this
+		 * `strcspn()` call is okay, even if it might do much more
+		 * work than strictly necessary.
+		 */
+		strcspn(prefix, " \t\r\n,") >= prefix_len &&	/* separators */
+		*prefix != '-' &&				/* deselection */
+		!isdigit(*prefix) &&				/* selection */
+		(prefix_len != 1 ||
+		 (*prefix != '*' &&				/* "all" wildcard */
+		  *prefix != '?'));				/* prompt help */
+}
+
 struct print_file_item_data {
 	const char *modified_fmt;
 	struct strbuf buf, index, worktree;
@@ -345,10 +494,23 @@ typedef int (*command_t)(struct add_i_state *s, const struct pathspec *ps,
 			 struct string_list *files,
 			 struct list_options *opts);
 
+struct command_item {
+	size_t prefix_length;
+	command_t command;
+};
+
 static void print_command_item(int i, struct string_list_item *item,
 			       void *print_command_item_data)
 {
-	printf(" %2d: %s", i + 1, item->string);
+	struct command_item *util = item->util;
+
+	if (!util->prefix_length ||
+	    !is_valid_prefix(item->string, util->prefix_length))
+		printf(" %2d: %s", i + 1, item->string);
+	else
+		printf(" %2d: [%.*s]%s", i + 1,
+		       (int)util->prefix_length, item->string,
+		       item->string + util->prefix_length);
 }
 
 int run_add_i(struct repository *r, const struct pathspec *ps)
@@ -364,7 +526,7 @@ int run_add_i(struct repository *r, const struct pathspec *ps)
 	} command_list[] = {
 		{ "status", run_status },
 	};
-	struct string_list commands = STRING_LIST_INIT_NODUP;
+	struct prefix_item_list commands = PREFIX_ITEM_LIST_INIT;
 
 	struct print_file_item_data print_file_item_data = {
 		"%12s %12s %s", STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
@@ -377,9 +539,12 @@ int run_add_i(struct repository *r, const struct pathspec *ps)
 	ssize_t i;
 	int res = 0;
 
-	for (i = 0; i < ARRAY_SIZE(command_list); i++)
-		string_list_append(&commands, command_list[i].string)
-			->util = command_list[i].command;
+	for (i = 0; i < ARRAY_SIZE(command_list); i++) {
+		struct command_item *util = xcalloc(sizeof(*util), 1);
+		util->command = command_list[i].command;
+		string_list_append(&commands.items, command_list[i].string)
+			->util = util;
+	}
 
 	init_add_i_state(&s, r);
 
@@ -404,8 +569,9 @@ int run_add_i(struct repository *r, const struct pathspec *ps)
 			break;
 		}
 		if (i != LIST_AND_CHOOSE_ERROR) {
-			command_t command = commands.items[i].util;
-			res = command(&s, ps, &files, &opts);
+			struct command_item *util =
+				commands.items.items[i].util;
+			res = util->command(&s, ps, &files, &opts);
 		}
 	}
 
@@ -414,7 +580,7 @@ int run_add_i(struct repository *r, const struct pathspec *ps)
 	strbuf_release(&print_file_item_data.index);
 	strbuf_release(&print_file_item_data.worktree);
 	strbuf_release(&header);
-	string_list_clear(&commands, 0);
+	prefix_item_list_clear(&commands);
 
 	return res;
 }
Signature · 1 line
gitgitgadget
#97 lore raw

PATCH v5 7/9 built-in add -i: support `?` (prompt help)

In reply to Johannes Schindelin via GitGitGadget

From: Johannes Schindelin <[email protected]>

With this change, we print out the same colored help text that the Perl-based git add -i prints in the main loop when question mark is entered.

---
 add-interactive.c | 22 +++++++++++++++++++++-
 1 file changed, 21 insertions(+), 1 deletion(-)
add-interactive.c +21 -1
index eb559555ad..d96e18fce5 100644
@@ -11,6 +11,7 @@ struct add_i_state {
 	struct repository *r;
 	int use_color;
 	char header_color[COLOR_MAXLEN];
+	char help_color[COLOR_MAXLEN];
 };
 
 static void init_color(struct repository *r, struct add_i_state *s,
@@ -43,6 +44,7 @@ static void init_add_i_state(struct add_i_state *s, struct repository *r)
 	s->use_color = want_color(s->use_color);
 
 	init_color(r, s, "header", s->header_color, GIT_COLOR_BOLD);
+	init_color(r, s, "help", s->help_color, GIT_COLOR_BOLD_RED);
 }
 
 /*
@@ -210,6 +212,7 @@ struct list_and_choose_options {
 	struct list_options list_opts;
 
 	const char *prompt;
+	void (*print_help)(struct add_i_state *s);
 };
 
 #define LIST_AND_CHOOSE_ERROR (-1)
@@ -250,6 +253,11 @@ static ssize_t list_and_choose(struct add_i_state *s,
 		if (!input.len)
 			break;
 
+		if (!strcmp(input.buf, "?")) {
+			opts->print_help(s);
+			continue;
+		}
+
 		p = input.buf;
 		for (;;) {
 			size_t sep = strcspn(p, " \t\r\n,");
@@ -513,12 +521,24 @@ static void print_command_item(int i, struct string_list_item *item,
 		       item->string + util->prefix_length);
 }
 
+static void command_prompt_help(struct add_i_state *s)
+{
+	const char *help_color = s->help_color;
+	color_fprintf_ln(stdout, help_color, "%s", _("Prompt help:"));
+	color_fprintf_ln(stdout, help_color, "1          - %s",
+			 _("select a numbered item"));
+	color_fprintf_ln(stdout, help_color, "foo        - %s",
+			 _("select item based on unique prefix"));
+	color_fprintf_ln(stdout, help_color, "           - %s",
+			 _("(empty) select nothing"));
+}
+
 int run_add_i(struct repository *r, const struct pathspec *ps)
 {
 	struct add_i_state s = { NULL };
 	struct list_and_choose_options main_loop_opts = {
 		{ 4, N_("*** Commands ***"), print_command_item, NULL },
-		N_("What now")
+		N_("What now"), command_prompt_help
 	};
 	struct {
 		const char *string;
Signature · 1 line
gitgitgadget
#98 lore raw

PATCH v5 8/9 built-in add -i: use color in the main loop

In reply to Johannes Schindelin via GitGitGadget

From: =?UTF-8?q?Slavica=20=C4=90uki=C4=87?= <[email protected]>

The error messages as well as the unique prefixes are colored in git add -i by default; We need to do the same in the built-in version.

---
 add-interactive.c | 35 +++++++++++++++++++++++++++++------
 1 file changed, 29 insertions(+), 6 deletions(-)
add-interactive.c +29 -6
index d96e18fce5..64d84ba1dc 100644
@@ -12,6 +12,9 @@ struct add_i_state {
 	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];
 };
 
 static void init_color(struct repository *r, struct add_i_state *s,
@@ -45,6 +48,9 @@ static void init_add_i_state(struct add_i_state *s, struct repository *r)
 
 	init_color(r, s, "header", s->header_color, GIT_COLOR_BOLD);
 	init_color(r, s, "help", s->help_color, GIT_COLOR_BOLD_RED);
+	init_color(r, s, "prompt", s->prompt_color, GIT_COLOR_BOLD_BLUE);
+	init_color(r, s, "error", s->error_color, GIT_COLOR_BOLD_RED);
+	init_color(r, s, "reset", s->reset_color, GIT_COLOR_RESET);
 }
 
 /*
@@ -240,7 +246,8 @@ static ssize_t list_and_choose(struct add_i_state *s,
 
 		list(s, &items->items, &opts->list_opts);
 
-		printf("%s%s", opts->prompt, "> ");
+		color_fprintf(stdout, s->prompt_color, "%s", opts->prompt);
+		fputs("> ", stdout);
 		fflush(stdout);
 
 		if (strbuf_getline(&input, stdin) == EOF) {
@@ -281,7 +288,8 @@ static ssize_t list_and_choose(struct add_i_state *s,
 				index = find_unique(p, items);
 
 			if (index < 0 || index >= items->items.nr)
-				printf(_("Huh (%s)?\n"), p);
+				color_fprintf_ln(stdout, s->error_color,
+						 _("Huh (%s)?"), p);
 			else {
 				res = index;
 				break;
@@ -507,18 +515,23 @@ struct command_item {
 	command_t command;
 };
 
+struct print_command_item_data {
+	const char *color, *reset;
+};
+
 static void print_command_item(int i, struct string_list_item *item,
 			       void *print_command_item_data)
 {
+	struct print_command_item_data *d = print_command_item_data;
 	struct command_item *util = item->util;
 
 	if (!util->prefix_length ||
 	    !is_valid_prefix(item->string, util->prefix_length))
 		printf(" %2d: %s", i + 1, item->string);
 	else
-		printf(" %2d: [%.*s]%s", i + 1,
-		       (int)util->prefix_length, item->string,
-		       item->string + util->prefix_length);
+		printf(" %2d: %s%.*s%s%s", i + 1,
+		       d->color, (int)util->prefix_length, item->string,
+		       d->reset, item->string + util->prefix_length);
 }
 
 static void command_prompt_help(struct add_i_state *s)
@@ -536,8 +549,9 @@ static void command_prompt_help(struct add_i_state *s)
 int run_add_i(struct repository *r, const struct pathspec *ps)
 {
 	struct add_i_state s = { NULL };
+	struct print_command_item_data data = { "[", "]" };
 	struct list_and_choose_options main_loop_opts = {
-		{ 4, N_("*** Commands ***"), print_command_item, NULL },
+		{ 4, N_("*** Commands ***"), print_command_item, &data },
 		N_("What now"), command_prompt_help
 	};
 	struct {
@@ -568,6 +582,15 @@ int run_add_i(struct repository *r, const struct pathspec *ps)
 
 	init_add_i_state(&s, r);
 
+	/*
+	 * When color was asked for, use the prompt color for
+	 * highlighting, otherwise use square brackets.
+	 */
+	if (s.use_color) {
+		data.color = s.prompt_color;
+		data.reset = s.reset_color;
+	}
+
 	strbuf_addstr(&header, "      ");
 	strbuf_addf(&header, print_file_item_data.modified_fmt,
 		    _("staged"), _("unstaged"), _("path"));
Signature · 1 line
gitgitgadget
#99 lore raw

PATCH v5 9/9 built-in add -i: implement the `help` command

In reply to Johannes Schindelin via GitGitGadget

From: =?UTF-8?q?Slavica=20=C4=90uki=C4=87?= <[email protected]>

This imitates the code to show the help text from the Perl script git-add--interactive.perl in the built-in version.

To make sure that it renders exactly like the Perl version of git add -i, we also add a test case for that to t3701-add-interactive.sh.

---
 add-interactive.c          | 21 +++++++++++++++++++++
 t/t3701-add-interactive.sh | 25 +++++++++++++++++++++++++
 2 files changed, 46 insertions(+)
add-interactive.c +21 -0
index 64d84ba1dc..4e5241e865 100644
@@ -506,6 +506,26 @@ static int run_status(struct add_i_state *s, const struct pathspec *ps,
 	return 0;
 }
 
+static int run_help(struct add_i_state *s, const struct pathspec *unused_ps,
+		    struct string_list *unused_files,
+		    struct list_options *unused_opts)
+{
+	color_fprintf_ln(stdout, s->help_color, "status        - %s",
+			 _("show paths with changes"));
+	color_fprintf_ln(stdout, s->help_color, "update        - %s",
+			 _("add working tree state to the staged set of changes"));
+	color_fprintf_ln(stdout, s->help_color, "revert        - %s",
+			 _("revert staged set of changes back to the HEAD version"));
+	color_fprintf_ln(stdout, s->help_color, "patch         - %s",
+			 _("pick hunks and update selectively"));
+	color_fprintf_ln(stdout, s->help_color, "diff          - %s",
+			 _("view diff between HEAD and index"));
+	color_fprintf_ln(stdout, s->help_color, "add untracked - %s",
+			 _("add contents of untracked files to the staged set of changes"));
+
+	return 0;
+}
+
 typedef int (*command_t)(struct add_i_state *s, const struct pathspec *ps,
 			 struct string_list *files,
 			 struct list_options *opts);
@@ -559,6 +579,7 @@ int run_add_i(struct repository *r, const struct pathspec *ps)
 		command_t command;
 	} command_list[] = {
 		{ "status", run_status },
+		{ "help", run_help },
 	};
 	struct prefix_item_list commands = PREFIX_ITEM_LIST_INIT;
 
t/t3701-add-interactive.sh +25 -0
index d50e165ca8..d4f9386621 100755
@@ -647,4 +647,29 @@ test_expect_success 'checkout -p works with pathological context lines' '
 	test_write_lines a b a b a a b a b a >expect &&
 	test_cmp expect a
 '
+
+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 | GIT_PAGER_IN_USE=true TERM=vt100 git add -i >actual.colored &&
+	test_decode_color <actual.colored >actual &&
+	test_i18ncmp expect actual
+'
+
 test_done
Signature · 1 line
gitgitgadget