Subject: [PATCH v2 2/4] commit: introduce a config key to allow as-is commit with i-t-a entries

From: Nguyễn Thái Ngọc Duy <[email-masked]>
Date: Tue, 7 Feb 2012 19:46:42 +0700

"git add -N" is introduced some time ago to help prevent users's
forgetting to add new files when committing. "git add -N" sets
CE_INTENT_TO_ADD bit to index, hence i-t-a entries from now on. For
safety reasons, as-is commits in the existence of i-t-a entries are
rejected ("I intend to add these files. If I type "git commit -m foo", it
may be a mistake if those files's contents are not added yet. Stop me in
that case").

However unless you are doing "commit -a" or "commit pathspec", you are
responsible for adding all contents you want to have in the commit
before you run the "git commit" command (and for the purpose of this
statement, "add -N" to tell Git to keey an eye on a path does _not_ add
contents).

A change to the file in the working tree that is left unadded is what
you decided to deliberately leave out of the commit, be it a change to a
path already in HEAD, or a path marked with "add -N". Forgetting to add
modified file and forgetting to add a file you earlier used "add -N"
amount to the same kind of risk, and "git status" is the way to make
sure your partial commit has exactly what you want. If you are not
worried about partial commit, you would be doing "commit -a", so the
"safety" is a moot point.

This patch is the beginning of the deprecation process to remove this
safety check, allowing "git commit" to proceed anyway. The process
consists of three steps to help users get used to new behavior:

1. Introduce a temporary config var, commit.ignoreIntentToAdd to help
the migration process, default to false. If the var is true, the remove
safety check.

Users who encounter the safety check are pointed to
commit.ignoreIntentToAdd documentation and advised to set the variable
to true.

2. A few releases after step 1 is out in the field, turn
commit.ignoreIntentToAdd default value to true (affecting mostly new
users).

Those who decided to stick to "false" from step 1 are warned the "false"
support will soon be gone and encouraged to move to "true" (or simply
remove the config variable).

Those who set the config to "true" is advised to remove it to keep
config file clean.

Those who encountered the safety check and did not bother to set this
config var is left out in the cold.

3. A few more releases after step 2, commit.ignoreIntentToAdd is
removed. There's no way to bring back the safety check.

This patch implements step 1.

Signed-off-by: Nguyễn Thái Ngọc Duy <[email-masked]>
---
 Documentation/config.txt |   16 ++++++++++++++++
 builtin/commit.c         |   29 ++++++++++++++++++++++++++---
 cache-tree.c             |    8 +++++---
 cache-tree.h             |    1 +
 t/t2203-add-intent.sh    |   21 ++++++++++++++++++++-
 5 files changed, 68 insertions(+), 7 deletions(-)

diff --git a/Documentation/config.txt b/Documentation/config.txt
index abeb82b..6839e44 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -831,6 +831,22 @@ commit.template::
 	"{tilde}/" is expanded to the value of `$HOME` and "{tilde}user/" to the
 	specified user's home directory.
 
+commit.ignoreIntentToAdd::
+	When set to `false`, prevent `git commit` from creating a
+	commit from an index that has entries that were added with
+	`git add -N` but have not been updated with real contents, as
+	the user may have forgotten to tell the final contents for
+	these entries.  Setting this to `true` makes `git commit`
+	pretend as if these entries do not exist in the index.
++
+The default for this variable is `false`, but it will change to `true`
+in future releases of git. To ease the transition, you may want to set
+it to `true` now and get used to the new behaviour early, or you may
+want to set it to `false` to keep the old behaviour a bit longer. We
+however expect to support setting this to `false` (to keep the current
+behaviour) only for a limited time after the default is changed to
+`true`.
+
 credential.helper::
 	Specify an external helper to be called when a username or
 	password credential is needed; the helper may consult external
diff --git a/builtin/commit.c b/builtin/commit.c
index bf42bb3..da67653 100644
--- a/builtin/commit.c
+++ b/builtin/commit.c
@@ -86,6 +86,7 @@ static int all, also, interactive, patch_interactive, only, amend, signoff;
 static int edit_flag = -1; /* unspecified */
 static int quiet, verbose, no_verify, allow_empty, dry_run, renew_authorship;
 static int no_post_rewrite, allow_empty_message;
+static int cache_tree_flags;
 static char *untracked_files_arg, *force_date, *ignore_submodule_arg;
 static char *sign_commit;
 
@@ -117,6 +118,8 @@ static enum {
 } status_format = STATUS_FORMAT_LONG;
 static int status_show_branch;
 
+static int set_commit_ignoreintenttoadd;
+
 static int opt_parse_m(const struct option *opt, const char *arg, int unset)
 {
 	struct strbuf *buf = opt->value;
@@ -400,7 +403,7 @@ static char *prepare_index(int argc, const char **argv, const char *prefix,
 		fd = hold_locked_index(&index_lock, 1);
 		add_files_to_cache(also ? prefix : NULL, pathspec, 0);
 		refresh_cache_or_die(refresh_flags);
-		update_main_cache_tree(WRITE_TREE_SILENT);
+		update_main_cache_tree(cache_tree_flags | WRITE_TREE_SILENT);
 		if (write_cache(fd, active_cache, active_nr) ||
 		    close_lock_file(&index_lock))
 			die(_("unable to write new_index file"));
@@ -420,8 +423,21 @@ static char *prepare_index(int argc, const char **argv, const char *prefix,
 	if (!pathspec || !*pathspec) {
 		fd = hold_locked_index(&index_lock, 1);
 		refresh_cache_or_die(refresh_flags);
+		if (!set_commit_ignoreintenttoadd) {
+			int i;
+			for (i = 0; i < active_nr; i++)
+				if (active_cache[i]->ce_flags & CE_INTENT_TO_ADD)
+					break;
+			if (i < active_nr) {
+				error(_("you intended to add \"%s\" but did not add it; not committing\n"
+					"hint: to commit all changes to tracked files, use \"commit -a\"\n"
+					"hint: to commit anyway without adding, set commit.ignoreIntentToAdd to true"),
+				      active_cache[i]->name);
+				exit(128); /* die() */
+			}
+		}
 		if (active_cache_changed) {
-			update_main_cache_tree(WRITE_TREE_SILENT);
+			update_main_cache_tree(cache_tree_flags | WRITE_TREE_SILENT);
 			if (write_cache(fd, active_cache, active_nr) ||
 			    commit_locked_index(&index_lock))
 				die(_("unable to write new_index file"));
@@ -870,7 +886,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,
 	 */
 	discard_cache();
 	read_cache_from(index_file);
-	if (update_main_cache_tree(0)) {
+	if (update_main_cache_tree(cache_tree_flags)) {
 		error(_("Error building trees"));
 		return 0;
 	}
@@ -1338,6 +1354,13 @@ static int git_commit_config(const char *k, const char *v, void *cb)
 		include_status = git_config_bool(k, v);
 		return 0;
 	}
+	if (!strcmp(k, "commit.ignoreintenttoadd")) {
+		set_commit_ignoreintenttoadd = 1;
+		if (git_config_bool(k, v))
+			cache_tree_flags |= WRITE_TREE_IGNORE_INTENT_TO_ADD;
+		else
+			cache_tree_flags &= ~WRITE_TREE_IGNORE_INTENT_TO_ADD;
+	}
 
 	status = git_gpg_config(k, v, NULL);
 	if (status)
diff --git a/cache-tree.c b/cache-tree.c
index 16355d6..d0be159 100644
--- a/cache-tree.c
+++ b/cache-tree.c
@@ -159,7 +159,9 @@ static int verify_cache(struct cache_entry **cache,
 	funny = 0;
 	for (i = 0; i < entries; i++) {
 		struct cache_entry *ce = cache[i];
-		if (ce_stage(ce) || (ce->ce_flags & CE_INTENT_TO_ADD)) {
+		if (ce_stage(ce) ||
+		    ((flags & WRITE_TREE_IGNORE_INTENT_TO_ADD) == 0 &&
+		     (ce->ce_flags & CE_INTENT_TO_ADD))) {
 			if (silent)
 				return -1;
 			if (10 < ++funny) {
@@ -339,8 +341,8 @@ static int update_one(struct cache_tree *it,
 				mode, sha1_to_hex(sha1), entlen+baselen, path);
 		}
 
-		if (ce->ce_flags & CE_REMOVE)
-			continue; /* entry being removed */
+		if (ce->ce_flags & (CE_REMOVE | CE_INTENT_TO_ADD))
+			continue; /* entry being removed or placeholder */
 
 		strbuf_grow(&buffer, entlen + 100);
 		strbuf_addf(&buffer, "%o %.*s%c", mode, entlen, path + baselen, '\0');
diff --git a/cache-tree.h b/cache-tree.h
index d8cb2e9..af3b917 100644
--- a/cache-tree.h
+++ b/cache-tree.h
@@ -38,6 +38,7 @@ int update_main_cache_tree(int);
 #define WRITE_TREE_IGNORE_CACHE_TREE 2
 #define WRITE_TREE_DRY_RUN 4
 #define WRITE_TREE_SILENT 8
+#define WRITE_TREE_IGNORE_INTENT_TO_ADD 16
 
 /* error return codes */
 #define WRITE_TREE_UNREADABLE_INDEX (-1)
diff --git a/t/t2203-add-intent.sh b/t/t2203-add-intent.sh
index 58a3299..88a508e 100755
--- a/t/t2203-add-intent.sh
+++ b/t/t2203-add-intent.sh
@@ -41,7 +41,26 @@ test_expect_success 'cannot commit with i-t-a entry' '
 	echo frotz >nitfol &&
 	git add rezrov &&
 	git add -N nitfol &&
-	test_must_fail git commit
+	test_must_fail git commit -minitial
+'
+
+test_expect_success 'can commit tree with i-t-a entry' '
+	git reset --hard &&
+	echo xyzzy >rezrov &&
+	echo frotz >nitfol &&
+	git add rezrov &&
+	git add -N nitfol &&
+	git config commit.ignoreIntentToAdd true &&
+	git commit -m initial &&
+	git ls-tree -r HEAD >actual &&
+	cat >expected <<EOF &&
+100644 blob ce013625030ba8dba906f756967f9e9ca394464a	elif
+100644 blob ce013625030ba8dba906f756967f9e9ca394464a	file
+100644 blob cf7711b63209d0dbc2d030f7fe3513745a9880e4	rezrov
+EOF
+	test_cmp expected actual &&
+	git config commit.ignoreIntentToAdd false &&
+	git reset HEAD^
 '
 
 test_expect_success 'can commit with an unrelated i-t-a entry in index' '
-- 
1.7.8.36.g69ee2