git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH v3 14/15] git config --unset: remove empty sections (in the common case)

From
Johannes Schindelin <johannes.schindelin@gmx.de>
Date
Apr 9, 2018, 08:32 UTC
Message-ID
<7f3d1b7520622801ccdf1bc9898eb7e9b4e78c8e.1523262450.git.johannes.schindelin@gmx.de>
In-Reply-To
<cover.1523262449.git.johannes.schindelin@gmx.de>

The original reasoning for not removing section headers upon removal of the last entry went like this: the user could have added comments about the section, or about the entries therein, and if there were other comments there, we would not know whether we should remove them.

In particular, a concocted example was presented that looked like this (and was added to t1300):

	# some generic comment on the configuration file itself
	# a comment specific to this "section" section.
	[section]
	# some intervening lines
	# that should also be dropped
	key = value
	# please be careful when you update the above variable

The ideal thing for `git config --unset section.key` in this case would be to leave only the first line behind, because all the other comments are now obsolete.

However, this is unfeasible, short of adding a complete Natural Language Processing module to Git, which seems not only a lot of work, but a totally unreasonable feature (for little benefit to most users).

Now, the real kicker about this problem is: most users do not edit their config files at all! In their use case, the config looks like this instead:

	[section]
		key = value

... and it is totally obvious what should happen if the entry is removed: the entire section should vanish.

Let's generalize this observation to this conservative strategy: if we are removing the last entry from a section, and there are no comments inside that section nor surrounding it, then remove the entire section. Otherwise behave as before: leave the now-empty section (including those comments, even ones about the now-deleted entry).

We have to be extra careful to handle the case where more than one entry is removed: any subset of them might be the last entries of their respective sections (and if there are no comments in or around that section, the section should be removed, too).

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 config.c          | 93 ++++++++++++++++++++++++++++++++++++++++++++++-
 t/t1300-config.sh |  4 +-
 2 files changed, 93 insertions(+), 4 deletions(-)
diff --git a/config.c b/config.c
index 72d71fc9a4e..2c7a10acdaa 100644
--- a/config.c
+++ b/config.c
@@ -2305,6 +2305,7 @@ struct config_store_data {
 	struct {
 		size_t begin, end;
 		enum config_event_t type;
+		int is_keys_section;
 	} *parsed;
 	unsigned int parsed_nr, parsed_alloc, *seen, seen_nr, seen_alloc;
 	unsigned int key_seen:1, section_seen:1, is_keys_section:1;
@@ -2333,17 +2334,20 @@ static int store_aux_event(enum config_event_t type,
 	store->parsed[store->parsed_nr].begin = begin;
 	store->parsed[store->parsed_nr].end = end;
 	store->parsed[store->parsed_nr].type = type;
-	store->parsed_nr++;
 
 	if (type == CONFIG_EVENT_SECTION) {
 		if (cf->var.len < 2 || cf->var.buf[cf->var.len - 1] != '.')
 			BUG("Invalid section name '%s'", cf->var.buf);
 
 		/* Is this the section we were looking for? */
-		store->is_keys_section = cf->var.len - 1 == store->baselen &&
+		store->is_keys_section =
+			store->parsed[store->parsed_nr].is_keys_section =
+			cf->var.len - 1 == store->baselen &&
 			!strncasecmp(cf->var.buf, store->key, store->baselen);
 	}
 
+	store->parsed_nr++;
+
 	return 0;
 }
 
@@ -2475,6 +2479,87 @@ static ssize_t write_pair(int fd, const char *key, const char *value,
 	return ret;
 }
 
+/*
+ * If we are about to unset the last key(s) in a section, and if there are
+ * no comments surrounding (or included in) the section, we will want to
+ * extend begin/end to remove the entire section.
+ *
+ * Note: the parameter `seen_ptr` points to the index into the store.seen
+ * array.  * This index may be incremented if a section has more than one
+ * entry (which all are to be removed).
+ */
+static void maybe_remove_section(struct config_store_data *store,
+				 const char *contents,
+				 size_t *begin_offset, size_t *end_offset,
+				 int *seen_ptr)
+{
+	size_t begin;
+	int i, seen, section_seen = 0;
+
+	/*
+	 * First, ensure that this is the first key, and that there are no
+	 * comments before the entry nor before the section header.
+	 */
+	seen = *seen_ptr;
+	for (i = store->seen[seen]; i > 0; i--) {
+		enum config_event_t type = store->parsed[i - 1].type;
+
+		if (type == CONFIG_EVENT_COMMENT)
+			/* There is a comment before this entry or section */
+			return;
+		if (type == CONFIG_EVENT_ENTRY) {
+			if (!section_seen)
+				/* This is not the section's first entry. */
+				return;
+			/* We encountered no comment before the section. */
+			break;
+		}
+		if (type == CONFIG_EVENT_SECTION) {
+			if (!store->parsed[i - 1].is_keys_section)
+				break;
+			section_seen = 1;
+		}
+	}
+	begin = store->parsed[i].begin;
+
+	/*
+	 * Next, make sure that we are removing he last key(s) in the section,
+	 * and that there are no comments that are possibly about the current
+	 * section.
+	 */
+	for (i = store->seen[seen] + 1; i < store->parsed_nr; i++) {
+		enum config_event_t type = store->parsed[i].type;
+
+		if (type == CONFIG_EVENT_COMMENT)
+			return;
+		if (type == CONFIG_EVENT_SECTION) {
+			if (store->parsed[i].is_keys_section)
+				continue;
+			break;
+		}
+		if (type == CONFIG_EVENT_ENTRY) {
+			if (++seen < store->seen_nr &&
+			    i == store->seen[seen])
+				/* We want to remove this entry, too */
+				continue;
+			/* There is another entry in this section. */
+			return;
+		}
+	}
+
+	/*
+	 * We are really removing the last entry/entries from this section, and
+	 * there are no enclosed or surrounding comments. Remove the entire,
+	 * now-empty section.
+	 */
+	*seen_ptr = seen;
+	*begin_offset = begin;
+	if (i < store->parsed_nr)
+		*end_offset = store->parsed[i].begin;
+	else
+		*end_offset = store->parsed[store->parsed_nr - 1].end;
+}
+
 int git_config_set_in_file_gently(const char *config_filename,
 				  const char *key, const char *value)
 {
@@ -2697,6 +2782,10 @@ int git_config_set_multivar_in_file_gently(const char *config_filename,
 			} else {
 				replace_end = store.parsed[j].end;
 				copy_end = store.parsed[j].begin;
+				if (!value)
+					maybe_remove_section(&store, contents,
+							     &copy_end,
+							     &replace_end, &i);
 				/*
 				 * Swallow preceding white-space on the same
 				 * line.
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index 9d23a8ca972..d973fd53398 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -1413,7 +1413,7 @@ test_expect_success 'urlmatch with wildcard' '
 '
 
 # good section hygiene
-test_expect_failure '--unset last key removes section (except if commented)' '
+test_expect_success '--unset last key removes section (except if commented)' '
 	cat >.git/config <<-\EOF &&
 	# some generic comment on the configuration file itself
 	# a comment specific to this "section" section.
@@ -1495,7 +1495,7 @@ test_expect_failure '--unset last key removes section (except if commented)' '
 	test_line_count = 3 .git/config
 '
 
-test_expect_failure '--unset-all removes section if empty & uncommented' '
+test_expect_success '--unset-all removes section if empty & uncommented' '
 	cat >.git/config <<-\EOF &&
 	[section]
 	key = value1
-- 
2.17.0.windows.1.4.g7e4058d72e3
Previous: Jeff KingNext: Johannes Schindelin
Message 102 of 103 in “Assorted fixes for `git config` (including the "empty sections" bug)”
  1. 0/9 Assorted fixes for `git config` (including the "empty sections" bug)Johannes Schindelin, Mar 29, 2018
  2. 1/9 git_config_set: fix off-by-twoJohannes Schindelin, Mar 29, 2018
  3. Stefan BellerMar 29, 2018
  4. Jeff KingMar 29, 2018
  5. Johannes SchindelinMar 30, 2018
  6. Ævar Arnfjörð BjarmasonMar 30, 2018
  7. Junio C HamanoMar 30, 2018
  8. Johannes SchindelinMar 30, 2018
  9. Junio C HamanoMar 30, 2018
  10. Johannes SchindelinApr 3, 2018
  11. Duy NguyenApr 3, 2018
  12. Johannes SchindelinApr 3, 2018
  13. Junio C HamanoApr 8, 2018
  14. Duy NguyenMar 30, 2018
  15. Johannes SchindelinMar 30, 2018
  16. Duy NguyenMar 30, 2018
  17. A potential approach to making tests faster on WindowsÆvar Arnfjörð Bjarmason, Mar 30, 2018
  18. Junio C HamanoMar 30, 2018
  19. Jeff KingMar 30, 2018
  20. Johannes SchindelinApr 3, 2018
  21. Ævar Arnfjörð BjarmasonApr 3, 2018
  22. Johannes SchindelinApr 3, 2018
  23. Eric SunshineApr 3, 2018
  24. Johannes SchindelinApr 3, 2018
  25. Jeff KingApr 3, 2018
  26. Johannes SchindelinApr 3, 2018
  27. Jeff KingApr 6, 2018
  28. Stefan BellerApr 6, 2018
  29. 2/9 t1300: rename it to reflect that `repo-config` was deprecatedJohannes Schindelin, Mar 29, 2018
  30. Jeff KingMar 29, 2018
  31. Johannes SchindelinMar 30, 2018
  32. 3/9 t1300: avoid relying on a bugJohannes Schindelin, Mar 29, 2018
  33. Jeff KingMar 29, 2018
  34. Johannes SchindelinMar 30, 2018
  35. 5/9 t1300: `--unset-all` can leave an empty section behind (bug)Johannes Schindelin, Mar 29, 2018
  36. Jeff KingMar 29, 2018
  37. 4/9 t1300: remove unreasonable expectation from TODOJohannes Schindelin, Mar 29, 2018
  38. Jeff KingMar 29, 2018
  39. Junio C HamanoMar 29, 2018
  40. Johannes SchindelinMar 30, 2018
  41. 6/9 git_config_set: simplify the way the section name is rememberedJohannes Schindelin, Mar 29, 2018
  42. 7/9 git config --unset: remove empty sections (in normal situations)Johannes Schindelin, Mar 29, 2018
  43. Jeff KingMar 29, 2018
  44. Johannes SchindelinMar 30, 2018
  45. Jeff KingMar 30, 2018
  46. 8/9 git_config_set: use do_config_from_file() directlyJohannes Schindelin, Mar 29, 2018
  47. Jeff KingMar 29, 2018
  48. Johannes SchindelinMar 30, 2018
  49. Jeff KingMar 30, 2018
  50. Johannes SchindelinMar 30, 2018
  51. Jeff KingMar 30, 2018
  52. Johannes SchindelinMar 30, 2018
  53. 9/9 git_config_set: reuse empty sectionsJohannes Schindelin, Mar 29, 2018
  54. Jeff KingMar 29, 2018
  55. Johannes SchindelinMar 30, 2018
  56. Stefan BellerMar 29, 2018
  57. Johannes SchindelinMar 30, 2018
  58. Jeff KingMar 29, 2018
  59. Johannes SchindelinMar 30, 2018
  60. Ævar Arnfjörð BjarmasonMar 30, 2018
  61. Johannes SchindelinMar 30, 2018
  62. 00/15 Assorted fixes for `git config` (including the "empty sections" bug)Johannes Schindelin, Apr 3, 2018
  63. 01/15 git_config_set: fix off-by-twoJohannes Schindelin, Apr 3, 2018
  64. 02/15 t1300: rename it to reflect that `repo-config` was deprecatedJohannes Schindelin, Apr 3, 2018
  65. 03/15 t1300: demonstrate that --replace-all can "invent" newlinesJohannes Schindelin, Apr 3, 2018
  66. 04/15 config --replace-all: avoid extra line breaksJohannes Schindelin, Apr 3, 2018
  67. 05/15 t1300: avoid relying on a bugJohannes Schindelin, Apr 3, 2018
  68. 06/15 t1300: remove unreasonable expectation from TODOJohannes Schindelin, Apr 3, 2018
  69. 07/15 t1300: `--unset-all` can leave an empty section behind (bug)Johannes Schindelin, Apr 3, 2018
  70. 08/15 config: introduce an optional event stream while parsingJohannes Schindelin, Apr 3, 2018
  71. Jeff KingApr 6, 2018
  72. Johannes SchindelinApr 9, 2018
  73. 09/15 config: avoid using the global variable `store`Johannes Schindelin, Apr 3, 2018
  74. Jeff KingApr 6, 2018
  75. Johannes SchindelinApr 9, 2018
  76. 10/15 config_set_store: rename some fields for consistencyJohannes Schindelin, Apr 3, 2018
  77. 11/15 git_config_set: do not use a state machineJohannes Schindelin, Apr 3, 2018
  78. Jeff KingApr 6, 2018
  79. Johannes SchindelinApr 9, 2018
  80. 12/15 git_config_set: make use of the config parser's event streamJohannes Schindelin, Apr 3, 2018
  81. 13/15 git config --unset: remove empty sections (in the common case)Johannes Schindelin, Apr 3, 2018
  82. 14/15 git_config_set: reuse empty sectionsJohannes Schindelin, Apr 3, 2018
  83. Johannes SchindelinApr 3, 2018
  84. Jeff KingApr 6, 2018
  85. Johannes SchindelinApr 9, 2018
  86. 00/15 Assorted fixes for `git config` (including the "empty sections" bug)Johannes Schindelin, Apr 9, 2018
  87. 01/15 git_config_set: fix off-by-twoJohannes Schindelin, Apr 9, 2018
  88. 03/15 t1300: demonstrate that --replace-all can "invent" newlinesJohannes Schindelin, Apr 9, 2018
  89. 04/15 config --replace-all: avoid extra line breaksJohannes Schindelin, Apr 9, 2018
  90. 02/15 t1300: rename it to reflect that `repo-config` was deprecatedJohannes Schindelin, Apr 9, 2018
  91. 05/15 t1300: avoid relying on a bugJohannes Schindelin, Apr 9, 2018
  92. 06/15 t1300: remove unreasonable expectation from TODOJohannes Schindelin, Apr 9, 2018
  93. 07/15 t1300: add a few more hairy examples of sections becoming emptyJohannes Schindelin, Apr 9, 2018
  94. 08/15 t1300: `--unset-all` can leave an empty section behind (bug)Johannes Schindelin, Apr 9, 2018
  95. 09/15 config: introduce an optional event stream while parsingJohannes Schindelin, Apr 9, 2018
  96. 10/15 config: avoid using the global variable `store`Johannes Schindelin, Apr 9, 2018
  97. 11/15 config_set_store: rename some fields for consistencyJohannes Schindelin, Apr 9, 2018
  98. 12/15 git_config_set: do not use a state machineJohannes Schindelin, Apr 9, 2018
  99. 13/15 git_config_set: make use of the config parser's event streamJohannes Schindelin, Apr 9, 2018
  100. Jeff KingMay 8, 2018
  101. Jeff KingMay 8, 2018
  102. 14/15 git config --unset: remove empty sections (in the common case)Johannes Schindelin, Apr 9, 2018
  103. 15/15 git_config_set: reuse empty sectionsJohannes Schindelin, Apr 9, 2018

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.