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

[PATCH v2 1/2] diff: add an API for deferred freeing

From
Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Date
Feb 11, 2021, 10:45 UTC
Message-ID
<20210211104535.16076-2-avarab@gmail.com>
In-Reply-To
<YCUFNVj7qlt9wzlX@coredump.intra.peff.net>

Add a diff_free() function to free anything we may have allocated in the "diff_options" struct, and the ability to make calling it a noop by setting "no_free" in "diff_options".

This is required because when e.g. "git diff" is run we'll allocate things in that struct, use the diff machinery once, and then exit.

But if we run e.g. "git log -p" we're going to re-use what we allocated across multiple diff_flush() calls, and only want to free things at the end.

We've thus ended up with features like the recently added "diff -I"[1] where we'll leak memory. As it turns out it could have simply used the pattern established in 6ea57703f6 (log: prepare log/log-tree to reuse the diffopt.close_file attribute, 2016-06-22).

Manually adding more such flags to things log_tree_commit() every time we need to allocate something would be tedious. Let's instead move that fclose() code it to a new diff_free(), in anticipation of freeing more things in that function in follow-up commits.

Some functions such as log_tree_commit() need an idiom of optionally retaining a previous "no_free", as they may either free the memory themselves, or their caller may do so. I'm keeping that idiom in log_show_early() for good measure, even though I don't think it's currently called in this manner. It also gets passed an existing "struct rev_info", so future callers may want to set the "no_free" flag.

This change is a bit hard to read because while the freeing pattern we're introducing isn't unusual, the "file" member is a special snowflake. We usually don't want to fclose() it. This is because "file" is usually stdout, in which case we don't want to fclose() it. We only want to opt-in to closing it when we e.g. open a file on the filesystem. Thus the opt-in "close_file" flag.

So the API in general just needs a "no_free" flag to defer freeing, but the "file" member still needs its "close_file" flag. This is made more confusing because while refactoring this code we could replace some "close_file=0" with "no_free=1", whereas others need to set both flags.

This is because there were some cases where an existing "close_file=0" meant "let's defer deallocation", and others where it meant "we don't want to close this file handle at all".

1. 296d4a94e7 (diff: add -I<regex> that ignores matching changes,
   2020-10-20)
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/log.c | 23 ++++++++++++-----------
 diff.c        | 20 ++++++++++++++++----
 diff.h        | 15 ++++++++++++++-
 log-tree.c    | 10 ++++++----
 4 files changed, 48 insertions(+), 20 deletions(-)
diff --git a/builtin/log.c b/builtin/log.c
index d0cbaaf68a0..fffaf51970d 100644
--- a/builtin/log.c
+++ b/builtin/log.c
@@ -307,10 +307,11 @@ static struct itimerval early_output_timer;
 
 static void log_show_early(struct rev_info *revs, struct commit_list *list)
 {
-	int i = revs->early_output, close_file = revs->diffopt.close_file;
+	int i = revs->early_output;
 	int show_header = 1;
+	int no_free = revs->diffopt.no_free;
 
-	revs->diffopt.close_file = 0;
+	revs->diffopt.no_free = 0;
 	sort_in_topological_order(&list, revs->sort_order);
 	while (list && i) {
 		struct commit *commit = list->item;
@@ -327,8 +328,8 @@ static void log_show_early(struct rev_info *revs, struct commit_list *list)
 		case commit_ignore:
 			break;
 		case commit_error:
-			if (close_file)
-				fclose(revs->diffopt.file);
+			revs->diffopt.no_free = no_free;
+			diff_free(&revs->diffopt);
 			return;
 		}
 		list = list->next;
@@ -336,8 +337,8 @@ static void log_show_early(struct rev_info *revs, struct commit_list *list)
 
 	/* Did we already get enough commits for the early output? */
 	if (!i) {
-		if (close_file)
-			fclose(revs->diffopt.file);
+		revs->diffopt.no_free = 0;
+		diff_free(&revs->diffopt);
 		return;
 	}
 
@@ -401,7 +402,7 @@ static int cmd_log_walk(struct rev_info *rev)
 {
 	struct commit *commit;
 	int saved_nrl = 0;
-	int saved_dcctc = 0, close_file = rev->diffopt.close_file;
+	int saved_dcctc = 0;
 
 	if (rev->early_output)
 		setup_early_output();
@@ -417,7 +418,7 @@ static int cmd_log_walk(struct rev_info *rev)
 	 * and HAS_CHANGES being accumulated in rev->diffopt, so be careful to
 	 * retain that state information if replacing rev->diffopt in this loop
 	 */
-	rev->diffopt.close_file = 0;
+	rev->diffopt.no_free = 1;
 	while ((commit = get_revision(rev)) != NULL) {
 		if (!log_tree_commit(rev, commit) && rev->max_count >= 0)
 			/*
@@ -442,8 +443,8 @@ static int cmd_log_walk(struct rev_info *rev)
 	}
 	rev->diffopt.degraded_cc_to_c = saved_dcctc;
 	rev->diffopt.needed_rename_limit = saved_nrl;
-	if (close_file)
-		fclose(rev->diffopt.file);
+	rev->diffopt.no_free = 0;
+	diff_free(&rev->diffopt);
 
 	if (rev->diffopt.output_format & DIFF_FORMAT_CHECKDIFF &&
 	    rev->diffopt.flags.check_failed) {
@@ -1955,7 +1956,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)
 		 * file, but but we must instruct it not to close after each
 		 * diff.
 		 */
-		rev.diffopt.close_file = 0;
+		rev.diffopt.no_free = 1;
 	} else {
 		int saved;
 
diff --git a/diff.c b/diff.c
index 69e3bc00ed8..a63c9ecae79 100644
--- a/diff.c
+++ b/diff.c
@@ -6336,6 +6336,20 @@ static void diff_flush_patch_all_file_pairs(struct diff_options *o)
 	}
 }
 
+static void diff_free_file(struct diff_options *options)
+{
+	if (options->close_file)
+		fclose(options->file);
+}
+
+void diff_free(struct diff_options *options)
+{
+	if (options->no_free)
+		return;
+
+	diff_free_file(options);
+}
+
 void diff_flush(struct diff_options *options)
 {
 	struct diff_queue_struct *q = &diff_queued_diff;
@@ -6399,8 +6413,7 @@ void diff_flush(struct diff_options *options)
 		 * options->file to /dev/null should be safe, because we
 		 * aren't supposed to produce any output anyway.
 		 */
-		if (options->close_file)
-			fclose(options->file);
+		diff_free_file(options);
 		options->file = xfopen("/dev/null", "w");
 		options->close_file = 1;
 		options->color_moved = 0;
@@ -6433,8 +6446,7 @@ void diff_flush(struct diff_options *options)
 free_queue:
 	free(q->queue);
 	DIFF_QUEUE_CLEAR(q);
-	if (options->close_file)
-		fclose(options->file);
+	diff_free(options);
 
 	/*
 	 * Report the content-level differences with HAS_CHANGES;
diff --git a/diff.h b/diff.h
index 2ff2b1c7f2c..527fb56d851 100644
--- a/diff.h
+++ b/diff.h
@@ -49,7 +49,17 @@
  * - Once you finish feeding the pairs of files, call `diffcore_std()`.
  * This will tell the diffcore library to go ahead and do its work.
  *
- * - Calling `diff_flush()` will produce the output.
+ * - Calling `diff_flush()` will produce the output, it will call
+ *   `diff_free()` to free any resources, e.g. those allocated in
+ *   `diff_opt_parse()`.
+ *
+ * - Set `.no_free = 1` before calling `diff_flush()` to defer the
+ *   freeing of allocated memory in diff_options. This is useful when
+ *   `diff_flush()` is being called in a loop, rather than as a
+ *   one-off. When setting `.no_free = 1` you must ensure that
+ *   `diff_free()` is called at the end, either by flipping the flag
+ *   before the last `diff_flush()` call, or by flipping it before
+ *   calling `diff_free()` yourself.
  */
 
 struct combine_diff_path;
@@ -365,6 +375,8 @@ struct diff_options {
 
 	struct repository *repo;
 	struct option *parseopts;
+
+	int no_free;
 };
 
 unsigned diff_filter_bit(char status);
@@ -559,6 +571,7 @@ void diffcore_fix_diff_index(void);
 
 int diff_queue_is_empty(void);
 void diff_flush(struct diff_options*);
+void diff_free(struct diff_options*);
 void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc);
 
 /* diff-raw status letters */
diff --git a/log-tree.c b/log-tree.c
index e0484676507..e7fcd70ba17 100644
--- a/log-tree.c
+++ b/log-tree.c
@@ -958,12 +958,14 @@ static int log_tree_diff(struct rev_info *opt, struct commit *commit, struct log
 int log_tree_commit(struct rev_info *opt, struct commit *commit)
 {
 	struct log_info log;
-	int shown, close_file = opt->diffopt.close_file;
+	int shown;
+	/* maybe called by e.g. cmd_log_walk(), maybe stand-alone */
+	int no_free = opt->diffopt.no_free;
 
 	log.commit = commit;
 	log.parent = NULL;
 	opt->loginfo = &log;
-	opt->diffopt.close_file = 0;
+	opt->diffopt.no_free = 1;
 
 	if (opt->line_level_traverse)
 		return line_log_print(opt, commit);
@@ -980,7 +982,7 @@ int log_tree_commit(struct rev_info *opt, struct commit *commit)
 		fprintf(opt->diffopt.file, "\n%s\n", opt->break_bar);
 	opt->loginfo = NULL;
 	maybe_flush_or_die(opt->diffopt.file, "stdout");
-	if (close_file)
-		fclose(opt->diffopt.file);
+	opt->diffopt.no_free = no_free;
+	diff_free(&opt->diffopt);
 	return shown;
 }
-- 
2.30.0.284.gd98b1dd5eaa7
Previous: Ævar Arnfjörð BjarmasonNext: Ævar Arnfjörð Bjarmason
Message 56 of 57 in “diff: add -I<regex> that ignores matching changes”
  1. 0/2 diff: add -I<regex> that ignores matching changesMichał Kępień, Oct 1, 2020
  2. 2/2 t: add -I<regex> testsMichał Kępień, Oct 1, 2020
  3. 1/2 diff: add -I<regex> that ignores matching changesMichał Kępień, Oct 1, 2020
  4. Junio C HamanoOct 1, 2020
  5. Michał KępieńOct 7, 2020
  6. Junio C HamanoOct 7, 2020
  7. Junio C HamanoOct 1, 2020
  8. 0/3 diff: add -I<regex> that ignores matching changesMichał Kępień, Oct 12, 2020
  9. 1/3 merge-base, xdiff: zero out xpparam_t structuresMichał Kępień, Oct 12, 2020
  10. Johannes SchindelinOct 12, 2020
  11. Junio C HamanoOct 12, 2020
  12. Junio C HamanoOct 12, 2020
  13. Michał KępieńOct 13, 2020
  14. 2/3 diff: add -I<regex> that ignores matching changesMichał Kępień, Oct 12, 2020
  15. Johannes SchindelinOct 12, 2020
  16. Junio C HamanoOct 12, 2020
  17. Johannes SchindelinOct 12, 2020
  18. Junio C HamanoOct 12, 2020
  19. Michał KępieńOct 13, 2020
  20. Junio C HamanoOct 13, 2020
  21. Michał KępieńOct 13, 2020
  22. Johannes SchindelinOct 13, 2020
  23. Junio C HamanoOct 13, 2020
  24. Michał KępieńOct 13, 2020
  25. Junio C HamanoOct 12, 2020
  26. Michał KępieńOct 13, 2020
  27. Junio C HamanoOct 12, 2020
  28. Michał KępieńOct 13, 2020
  29. 3/3 t: add -I<regex> testsMichał Kępień, Oct 12, 2020
  30. Johannes SchindelinOct 12, 2020
  31. Michał KępieńOct 13, 2020
  32. Johannes SchindelinOct 13, 2020
  33. Junio C HamanoOct 13, 2020
  34. Michał KępieńOct 13, 2020
  35. Johannes SchindelinOct 15, 2020
  36. 0/2 diff: add -I<regex> that ignores matching changesMichał Kępień, Oct 15, 2020
  37. 1/2 merge-base, xdiff: zero out xpparam_t structuresMichał Kępień, Oct 15, 2020
  38. 2/2 diff: add -I<regex> that ignores matching changesMichał Kępień, Oct 15, 2020
  39. Phillip WoodOct 16, 2020
  40. Junio C HamanoOct 16, 2020
  41. Michał KępieńOct 19, 2020
  42. Junio C HamanoOct 16, 2020
  43. Michał KępieńOct 19, 2020
  44. Junio C HamanoOct 19, 2020
  45. Johannes SchindelinOct 16, 2020
  46. 0/2 diff: add -I<regex> that ignores matching changesMichał Kępień, Oct 20, 2020
  47. 1/2 merge-base, xdiff: zero out xpparam_t structuresMichał Kępień, Oct 20, 2020
  48. 2/2 diff: add -I<regex> that ignores matching changesMichał Kępień, Oct 20, 2020
  49. 1/2 diff: add an API for deferred freeingÆvar Arnfjörð Bjarmason, Feb 5, 2021
  50. Johannes SchindelinFeb 10, 2021
  51. Ævar Arnfjörð BjarmasonFeb 11, 2021
  52. Johannes SchindelinFeb 11, 2021
  53. Jeff KingFeb 11, 2021
  54. 2/2 diff: plug memory leak from regcomp() on {log,diff} -IÆvar Arnfjörð Bjarmason, Feb 11, 2021
  55. 0/2 diff: add an API for deferred freeingÆvar Arnfjörð Bjarmason, Feb 11, 2021
  56. 1/2 diff: add an API for deferred freeingÆvar Arnfjörð Bjarmason, Feb 11, 2021
  57. 2/2 diff: plug memory leak from regcomp() on {log,diff} -IÆvar Arnfjörð Bjarmason, Feb 5, 2021

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.