{"thread":{"id":"61535","subject":"[PATCH 00/20] Various memory leak fixes","startedAt":"2024-05-23T12:25:15Z","lastAt":"2024-05-31T14:56:34Z","messageCount":115,"participants":["Patrick Steinhardt","Junio C Hamano","Eric Sunshine","Karthik Nayak","Jeff King"],"isPatch":true,"patchVersion":1,"patchTotal":20},"messages":[{"id":"495383","messageId":"cover.1716465556.git.ps@pks.im","threadId":"61535","inReplyTo":null,"subject":"[PATCH 00/20] Various memory leak fixes","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-23T12:25:09Z","receivedAt":"2024-05-23T12:25:15Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nmy mind had a couple of minutes where it was roaming, and of course it\nimmediately searched for and chased down the next rabbit hole. The\nresult is this patch series which fixes a bunch of leaks all over the\nplace. There isn't really any structure to the leaks that I did fix --\nit's mostly things that I stumbled over. In the end, this series makes\nanother 56 test suites pass with leak checking enabled, 13 of which have\nalready been passing without any changes.\n\nWhile most things are unstructured, there are two topics that stand out:\n\n  - Patches 5 to 12 address a shortcoming of our config API. Both\n    `git_config_string()` and `git_config_pathname()` have a `const char\n    **` out parameter, but they do in fact transfer memory ownership to\n    the caller. This resulted in a bunch of memory leaks all over the\n    place.\n\n    These patches thus refactor a bunch of code and then ultimately\n    switch the out parameter to become a `char *`\n\n  - Patches 16 to 20 have the goal of making git-mv(1) memory leak free.\n    I had a very hard time understanding how it tracks memory. I think\n    this wasn't only me, or otherwise there wouldn't be calls to\n    `UNLEAK()` in there. In any case, I decided to rewrite the arrays to\n    use a `struct strvec`, which makes tracking and releasing of memory\n    a ton easier.\n\n    It does come at the cost of more allocations because we may now\n    duplicate strings that we didn't before. But I think the tradeoff is\n    worth it because the number of strings we may now duplicate is\n    bounded by the number of command line arguments anyway.\n\nThanks!\n\nPatrick\n\nPatrick Steinhardt (20):\n  t: mark a bunch of tests as leak-free\n  transport-helper: fix leaking helper name\n  strbuf: fix leak when `appendwholeline()` fails with EOF\n  checkout: clarify memory ownership in `unique_tracking_name()`\n  http: refactor code to clarify memory ownership\n  config: clarify memory ownership in `git_config_pathname()`\n  diff: refactor code to clarify memory ownership of prefixes\n  convert: refactor code to clarify ownership of\n    check_roundtrip_encoding\n  builtin/log: stop using globals for log config\n  builtin/log: stop using globals for format config\n  config: clarify memory ownership in `git_config_string()`\n  config: plug various memory leaks\n  builtin/credential: clear credential before exit\n  commit-reach: fix memory leak in `ahead_behind()`\n  submodule: fix leaking memory for submodule entries\n  strvec: add functions to replace and remove strings\n  builtin/mv: refactor `add_slash()` to always return allocated strings\n  builtin/mv duplicate string list memory\n  builtin/mv: refactor to use `struct strvec`\n  builtin/mv: fix leaks for submodule gitfile paths\n\n Makefile                                      |   1 +\n alias.c                                       |   6 +-\n attr.c                                        |   2 +-\n attr.h                                        |   2 +-\n builtin/blame.c                               |   2 +-\n builtin/checkout.c                            |  14 +-\n builtin/commit.c                              |   4 +-\n builtin/config.c                              |   2 +-\n builtin/credential.c                          |   2 +\n builtin/log.c                                 | 708 ++++++++++--------\n builtin/merge.c                               |   4 +-\n builtin/mv.c                                  | 222 +++---\n builtin/rebase.c                              |   2 +-\n builtin/receive-pack.c                        |   6 +-\n builtin/repack.c                              |   8 +-\n builtin/worktree.c                            |  20 +-\n checkout.c                                    |   4 +-\n checkout.h                                    |   6 +-\n commit-reach.c                                |   4 +\n config.c                                      |  52 +-\n config.h                                      |  10 +-\n convert.c                                     |  30 +-\n convert.h                                     |   2 +-\n delta-islands.c                               |   2 +-\n diff.c                                        |  20 +-\n environment.c                                 |  16 +-\n environment.h                                 |  14 +-\n fetch-pack.c                                  |   4 +-\n fsck.c                                        |   4 +-\n fsmonitor-settings.c                          |   5 +-\n gpg-interface.c                               |   6 +-\n http.c                                        |  50 +-\n imap-send.c                                   |  12 +-\n mailmap.c                                     |   4 +-\n mailmap.h                                     |   4 +-\n merge-ll.c                                    |   6 +-\n pager.c                                       |   2 +-\n pretty.c                                      |  14 +-\n promisor-remote.h                             |   2 +-\n remote.c                                      |  20 +-\n remote.h                                      |   8 +-\n sequencer.c                                   |   2 +-\n setup.c                                       |   6 +-\n strbuf.c                                      |   4 +-\n strvec.c                                      |  20 +\n strvec.h                                      |  13 +\n submodule-config.c                            |   2 +\n t/t0300-credentials.sh                        |   2 +\n t/t0411-clone-from-partial.sh                 |   1 +\n t/t0610-reftable-basics.sh                    |   1 +\n t/t0611-reftable-httpd.sh                     |   1 +\n t/t1013-read-tree-submodule.sh                |   1 +\n t/t1306-xdg-files.sh                          |   1 +\n t/t1350-config-hooks-path.sh                  |   1 +\n t/t1400-update-ref.sh                         |   2 +\n t/t2013-checkout-submodule.sh                 |   1 +\n t/t2024-checkout-dwim.sh                      |   1 +\n t/t2060-switch.sh                             |   1 +\n t/t2405-worktree-submodule.sh                 |   1 +\n t/t3007-ls-files-recurse-submodules.sh        |   1 +\n t/t3203-branch-output.sh                      |   2 +\n t/t3415-rebase-autosquash.sh                  |   1 +\n t/t3426-rebase-submodule.sh                   |   1 +\n t/t3512-cherry-pick-submodule.sh              |   1 +\n t/t3513-revert-submodule.sh                   |   1 +\n t/t3600-rm.sh                                 |   1 +\n t/t3906-stash-submodule.sh                    |   1 +\n t/t4001-diff-rename.sh                        |   4 +-\n t/t4041-diff-submodule-option.sh              |   1 +\n t/t4043-diff-rename-binary.sh                 |   1 +\n t/t4059-diff-submodule-not-initialized.sh     |   1 +\n t/t4060-diff-submodule-option-diff-format.sh  |   1 +\n t/t4120-apply-popt.sh                         |   1 +\n t/t4137-apply-submodule.sh                    |   1 +\n t/t4153-am-resume-override-opts.sh            |   1 +\n t/t4210-log-i18n.sh                           |   2 +\n t/t5563-simple-http-auth.sh                   |   1 +\n t/t5564-http-proxy.sh                         |   1 +\n t/t5581-http-curl-verbose.sh                  |   1 +\n t/t6006-rev-list-format.sh                    |   1 +\n t/t6041-bisect-submodule.sh                   |   1 +\n t/t6400-merge-df.sh                           |   1 +\n t/t6412-merge-large-rename.sh                 |   1 +\n t/t6426-merge-skip-unneeded-updates.sh        |   1 +\n t/t6429-merge-sequence-rename-caching.sh      |   1 +\n t/t6438-submodule-directory-file-conflicts.sh |   1 +\n t/t7001-mv.sh                                 |   2 +\n t/t7005-editor.sh                             |   1 +\n t/t7006-pager.sh                              |   1 +\n t/t7102-reset.sh                              |   1 +\n t/t7112-reset-submodule.sh                    |   1 +\n t/t7417-submodule-path-url.sh                 |   1 +\n t/t7421-submodule-summary-add.sh              |   1 +\n t/t7423-submodule-symlinks.sh                 |   1 +\n t/t9129-git-svn-i18n-commitencoding.sh        |   1 -\n t/t9139-git-svn-non-utf8-commitencoding.sh    |   1 -\n t/t9200-git-cvsexportcommit.sh                |   1 +\n t/t9401-git-cvsserver-crlf.sh                 |   1 +\n t/t9600-cvsimport.sh                          |   1 +\n t/t9601-cvsimport-vendor-branch.sh            |   1 +\n t/t9602-cvsimport-branches-tags.sh            |   1 +\n t/t9603-cvsimport-patchsets.sh                |   2 +\n t/t9604-cvsimport-timestamps.sh               |   2 +\n t/unit-tests/t-strvec.c                       | 259 +++++++\n t/unit-tests/test-lib.c                       |  13 +\n t/unit-tests/test-lib.h                       |  13 +\n transport-helper.c                            |   6 +-\n transport.c                                   |   1 +\n upload-pack.c                                 |   2 +-\n userdiff.h                                    |  12 +-\n 110 files changed, 1141 insertions(+), 584 deletions(-)\n create mode 100644 t/unit-tests/t-strvec.c\n\n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495384","messageId":"0e9fa9ca7386f527903887242008b5b0443ada69.1716465556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716465556.git.ps@pks.im","subject":"[PATCH 01/20] t: mark a bunch of tests as leak-free","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-23T12:25:15Z","receivedAt":"2024-05-23T12:25:19Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"There are a bunch of tests which do not have any leaks:\n\n  - t0411: Introduced via 5c5a4a1c05 (t0411: add tests for cloning from\n    partial repo, 2024-01-28), passes since its inception.\n\n  - t0610: Introduced via 57db2a094d (refs: introduce reftable backend,\n    2024-02-07), passes since its inception.\n\n  - t2405: Passes since 6741e917de (repository: avoid leaking\n    `fsmonitor` data, 2024-04-12).\n\n  - t4153: Passes since 71c7916053 (apply: plug a leak in apply_data,\n    2024-04-23).\n\n  - t7006: Passes since at least Git v2.40. I did not care to go back\n    any further than that.\n\n  - t7423: Introduced via b20c10fd9b (t7423: add tests for symlinked\n    submodule directories, 2024-01-28), passes since e8d0608944\n    (submodule: require the submodule path to contain directories only,\n    2024-03-26). The fix is not ovbiously related, but probably works\n    because we now die early in many code paths.\n\n  - t9xxx: All of these are exercising CVS-related tooling and pass\n    since at least Git v2.40. It's likely that these pass for a long\n    time already, but nobody ever noticed because noone has CVS on their\n    machine.\n\nMark all of these tests as passing.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t0411-clone-from-partial.sh      | 1 +\n t/t0610-reftable-basics.sh         | 1 +\n t/t2405-worktree-submodule.sh      | 1 +\n t/t4153-am-resume-override-opts.sh | 1 +\n t/t7006-pager.sh                   | 1 +\n t/t7423-submodule-symlinks.sh      | 1 +\n t/t9200-git-cvsexportcommit.sh     | 1 +\n t/t9401-git-cvsserver-crlf.sh      | 1 +\n t/t9600-cvsimport.sh               | 1 +\n t/t9601-cvsimport-vendor-branch.sh | 1 +\n t/t9602-cvsimport-branches-tags.sh | 1 +\n t/t9603-cvsimport-patchsets.sh     | 2 ++\n t/t9604-cvsimport-timestamps.sh    | 2 ++\n 13 files changed, 15 insertions(+)\n\ndiff --git a/t/t0411-clone-from-partial.sh b/t/t0411-clone-from-partial.sh\nindex c98d501869..932bf2067d 100755\n--- a/t/t0411-clone-from-partial.sh\n+++ b/t/t0411-clone-from-partial.sh\n@@ -2,6 +2,7 @@\n \n test_description='check that local clone does not fetch from promisor remotes'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n test_expect_success 'create evil repo' '\ndiff --git a/t/t0610-reftable-basics.sh b/t/t0610-reftable-basics.sh\nindex cc5bbfd732..b06c46999d 100755\n--- a/t/t0610-reftable-basics.sh\n+++ b/t/t0610-reftable-basics.sh\n@@ -10,6 +10,7 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n GIT_TEST_DEFAULT_REF_FORMAT=reftable\n export GIT_TEST_DEFAULT_REF_FORMAT\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n INVALID_OID=$(test_oid 001)\ndiff --git a/t/t2405-worktree-submodule.sh b/t/t2405-worktree-submodule.sh\nindex 11018f37c7..1d7f605633 100755\n--- a/t/t2405-worktree-submodule.sh\n+++ b/t/t2405-worktree-submodule.sh\n@@ -5,6 +5,7 @@ test_description='Combination of submodules and multiple worktrees'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n base_path=$(pwd -P)\ndiff --git a/t/t4153-am-resume-override-opts.sh b/t/t4153-am-resume-override-opts.sh\nindex 4add7c7757..6bc377b917 100755\n--- a/t/t4153-am-resume-override-opts.sh\n+++ b/t/t4153-am-resume-override-opts.sh\n@@ -2,6 +2,7 @@\n \n test_description='git-am command-line options override saved options'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-terminal.sh\n \ndiff --git a/t/t7006-pager.sh b/t/t7006-pager.sh\nindex e56ca5b0fa..60e4c90de1 100755\n--- a/t/t7006-pager.sh\n+++ b/t/t7006-pager.sh\n@@ -2,6 +2,7 @@\n \n test_description='Test automatic use of a pager.'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-pager.sh\n . \"$TEST_DIRECTORY\"/lib-terminal.sh\ndiff --git a/t/t7423-submodule-symlinks.sh b/t/t7423-submodule-symlinks.sh\nindex 3d3c7af3ce..f45d806201 100755\n--- a/t/t7423-submodule-symlinks.sh\n+++ b/t/t7423-submodule-symlinks.sh\n@@ -2,6 +2,7 @@\n \n test_description='check that submodule operations do not follow symlinks'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n test_expect_success 'prepare' '\ndiff --git a/t/t9200-git-cvsexportcommit.sh b/t/t9200-git-cvsexportcommit.sh\nindex a44eabf0d8..3d4842164c 100755\n--- a/t/t9200-git-cvsexportcommit.sh\n+++ b/t/t9200-git-cvsexportcommit.sh\n@@ -4,6 +4,7 @@\n #\n test_description='Test export of commits to CVS'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n if ! test_have_prereq PERL; then\ndiff --git a/t/t9401-git-cvsserver-crlf.sh b/t/t9401-git-cvsserver-crlf.sh\nindex a34805acdc..a67e6abd49 100755\n--- a/t/t9401-git-cvsserver-crlf.sh\n+++ b/t/t9401-git-cvsserver-crlf.sh\n@@ -12,6 +12,7 @@ repository using cvs CLI client via git-cvsserver server'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n marked_as () {\ndiff --git a/t/t9600-cvsimport.sh b/t/t9600-cvsimport.sh\nindex 5680849218..41fcf3606b 100755\n--- a/t/t9600-cvsimport.sh\n+++ b/t/t9600-cvsimport.sh\n@@ -4,6 +4,7 @@ test_description='git cvsimport basic tests'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./lib-cvs.sh\n \n if ! test_have_prereq NOT_ROOT; then\ndiff --git a/t/t9601-cvsimport-vendor-branch.sh b/t/t9601-cvsimport-vendor-branch.sh\nindex 116cddba3a..e007669495 100755\n--- a/t/t9601-cvsimport-vendor-branch.sh\n+++ b/t/t9601-cvsimport-vendor-branch.sh\n@@ -35,6 +35,7 @@ test_description='git cvsimport handling of vendor branches'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./lib-cvs.sh\n \n setup_cvs_test_repository t9601\ndiff --git a/t/t9602-cvsimport-branches-tags.sh b/t/t9602-cvsimport-branches-tags.sh\nindex e5266c9a87..3768e3bd8c 100755\n--- a/t/t9602-cvsimport-branches-tags.sh\n+++ b/t/t9602-cvsimport-branches-tags.sh\n@@ -7,6 +7,7 @@ test_description='git cvsimport handling of branches and tags'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./lib-cvs.sh\n \n setup_cvs_test_repository t9602\ndiff --git a/t/t9603-cvsimport-patchsets.sh b/t/t9603-cvsimport-patchsets.sh\nindex 19f38f78f2..2a387fdbaa 100755\n--- a/t/t9603-cvsimport-patchsets.sh\n+++ b/t/t9603-cvsimport-patchsets.sh\n@@ -12,6 +12,8 @@\n # bug.\n \n test_description='git cvsimport testing for correct patchset estimation'\n+\n+TEST_PASSES_SANITIZE_LEAK=true\n . ./lib-cvs.sh\n \n setup_cvs_test_repository t9603\ndiff --git a/t/t9604-cvsimport-timestamps.sh b/t/t9604-cvsimport-timestamps.sh\nindex 2d03259729..9cf0685d56 100755\n--- a/t/t9604-cvsimport-timestamps.sh\n+++ b/t/t9604-cvsimport-timestamps.sh\n@@ -1,6 +1,8 @@\n #!/bin/sh\n \n test_description='git cvsimport timestamps'\n+\n+TEST_PASSES_SANITIZE_LEAK=true\n . ./lib-cvs.sh\n \n test_lazy_prereq POSIX_TIMEZONE '\n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495385","messageId":"05fbadbae2184479c87c37675dde7bd79b3e32ab.1716465556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716465556.git.ps@pks.im","subject":"[PATCH 02/20] transport-helper: fix leaking helper name","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-23T12:25:20Z","receivedAt":"2024-05-23T12:25:24Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When initializing the transport helper in `transport_get()`, we\nallocate the name of the helper. We neither end up transferring\nownership of the name, nor do we free it. The associated memory thus\nleaks.\n\nFix this memory leak and mark now-passing tests as leak free.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t0611-reftable-httpd.sh    | 1 +\n t/t5563-simple-http-auth.sh  | 1 +\n t/t5564-http-proxy.sh        | 1 +\n t/t5581-http-curl-verbose.sh | 1 +\n transport-helper.c           | 6 ++++--\n transport.c                  | 1 +\n 6 files changed, 9 insertions(+), 2 deletions(-)\n\ndiff --git a/t/t0611-reftable-httpd.sh b/t/t0611-reftable-httpd.sh\nindex 5e05b9c1f2..2805995cc8 100755\n--- a/t/t0611-reftable-httpd.sh\n+++ b/t/t0611-reftable-httpd.sh\n@@ -2,6 +2,7 @@\n \n test_description='reftable HTTPD tests'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-httpd.sh\n \ndiff --git a/t/t5563-simple-http-auth.sh b/t/t5563-simple-http-auth.sh\nindex 5d5caa3f58..4af796de67 100755\n--- a/t/t5563-simple-http-auth.sh\n+++ b/t/t5563-simple-http-auth.sh\n@@ -2,6 +2,7 @@\n \n test_description='test http auth header and credential helper interop'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-httpd.sh\n \ndiff --git a/t/t5564-http-proxy.sh b/t/t5564-http-proxy.sh\nindex 9da5134614..bb35b87071 100755\n--- a/t/t5564-http-proxy.sh\n+++ b/t/t5564-http-proxy.sh\n@@ -2,6 +2,7 @@\n \n test_description=\"test fetching through http proxy\"\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-httpd.sh\n \ndiff --git a/t/t5581-http-curl-verbose.sh b/t/t5581-http-curl-verbose.sh\nindex cded79c16b..724f610054 100755\n--- a/t/t5581-http-curl-verbose.sh\n+++ b/t/t5581-http-curl-verbose.sh\n@@ -4,6 +4,7 @@ test_description='test GIT_CURL_VERBOSE'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-httpd.sh\n start_httpd\ndiff --git a/transport-helper.c b/transport-helper.c\nindex 780fcaf529..9820947ab2 100644\n--- a/transport-helper.c\n+++ b/transport-helper.c\n@@ -22,7 +22,7 @@\n static int debug;\n \n struct helper_data {\n-\tconst char *name;\n+\tchar *name;\n \tstruct child_process *helper;\n \tFILE *out;\n \tunsigned fetch : 1,\n@@ -111,6 +111,7 @@ static void do_take_over(struct transport *transport)\n \tdata = (struct helper_data *)transport->data;\n \ttransport_take_over(transport, data->helper);\n \tfclose(data->out);\n+\tfree(data->name);\n \tfree(data);\n }\n \n@@ -253,6 +254,7 @@ static int disconnect_helper(struct transport *transport)\n \t\tclose(data->helper->out);\n \t\tfclose(data->out);\n \t\tres = finish_command(data->helper);\n+\t\tFREE_AND_NULL(data->name);\n \t\tFREE_AND_NULL(data->helper);\n \t}\n \treturn res;\n@@ -1297,7 +1299,7 @@ static struct transport_vtable vtable = {\n int transport_helper_init(struct transport *transport, const char *name)\n {\n \tstruct helper_data *data = xcalloc(1, sizeof(*data));\n-\tdata->name = name;\n+\tdata->name = xstrdup(name);\n \n \ttransport_check_allowed(name);\n \ndiff --git a/transport.c b/transport.c\nindex 0ad04b77fd..83ddea8fbc 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -1176,6 +1176,7 @@ struct transport *transport_get(struct remote *remote, const char *url)\n \t\tint len = external_specification_len(url);\n \t\tchar *handler = xmemdupz(url, len);\n \t\ttransport_helper_init(ret, handler);\n+\t\tfree(handler);\n \t}\n \n \tif (ret->smart_options) {\n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495386","messageId":"d76079797f3a4ab55ee1dc6e6ee761dea021d70a.1716465556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716465556.git.ps@pks.im","subject":"[PATCH 03/20] strbuf: fix leak when `appendwholeline()` fails with EOF","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-23T12:25:25Z","receivedAt":"2024-05-23T12:25:29Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"In `strbuf_appendwholeline()` we call `strbuf_getwholeline()` with a\ntemporary buffer. In case the call returns an error we indicate this by\nreturning EOF, but never release the temporary buffer. This can lead to\na memory leak when the line has been partially filled. Fix this.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n strbuf.c              | 4 +++-\n t/t1400-update-ref.sh | 2 ++\n 2 files changed, 5 insertions(+), 1 deletion(-)\n\ndiff --git a/strbuf.c b/strbuf.c\nindex 0d929e4e19..e1076c9891 100644\n--- a/strbuf.c\n+++ b/strbuf.c\n@@ -691,8 +691,10 @@ int strbuf_getwholeline(struct strbuf *sb, FILE *fp, int term)\n int strbuf_appendwholeline(struct strbuf *sb, FILE *fp, int term)\n {\n \tstruct strbuf line = STRBUF_INIT;\n-\tif (strbuf_getwholeline(&line, fp, term))\n+\tif (strbuf_getwholeline(&line, fp, term)) {\n+\t\tstrbuf_release(&line);\n \t\treturn EOF;\n+\t}\n \tstrbuf_addbuf(sb, &line);\n \tstrbuf_release(&line);\n \treturn 0;\ndiff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh\nindex ec3443cc87..bbee2783ab 100755\n--- a/t/t1400-update-ref.sh\n+++ b/t/t1400-update-ref.sh\n@@ -4,6 +4,8 @@\n #\n \n test_description='Test git update-ref and basic ref logging'\n+\n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n Z=$ZERO_OID\n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495387","messageId":"ffd9eb795f315b2c03da5bcec573938e89148a95.1716465556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716465556.git.ps@pks.im","subject":"[PATCH 04/20] checkout: clarify memory ownership in `unique_tracking_name()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-23T12:25:31Z","receivedAt":"2024-05-23T12:25:34Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The function `unique_tracking_name()` returns an allocated string, but\ndoes not clearly indicate this because its return type is `const char *`\ninstead of `char *`. This has led to various callsites where we never\nfree its returned memory at all, which causes memory leaks.\n\nPlug those leaks and mark now-passing tests as leak free.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/checkout.c                            | 14 +++++++------\n builtin/worktree.c                            | 20 ++++++++++---------\n checkout.c                                    |  4 ++--\n checkout.h                                    |  6 +++---\n t/t2024-checkout-dwim.sh                      |  1 +\n t/t2060-switch.sh                             |  1 +\n t/t3426-rebase-submodule.sh                   |  1 +\n t/t3512-cherry-pick-submodule.sh              |  1 +\n t/t3513-revert-submodule.sh                   |  1 +\n t/t3600-rm.sh                                 |  1 +\n t/t3906-stash-submodule.sh                    |  1 +\n t/t4137-apply-submodule.sh                    |  1 +\n t/t6041-bisect-submodule.sh                   |  1 +\n t/t6438-submodule-directory-file-conflicts.sh |  1 +\n 14 files changed, 34 insertions(+), 20 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex f90a4ca4b7..3cf44b4683 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -1275,12 +1275,12 @@ static void setup_new_branch_info_and_source_tree(\n \t}\n }\n \n-static const char *parse_remote_branch(const char *arg,\n-\t\t\t\t       struct object_id *rev,\n-\t\t\t\t       int could_be_checkout_paths)\n+static char *parse_remote_branch(const char *arg,\n+\t\t\t\t struct object_id *rev,\n+\t\t\t\t int could_be_checkout_paths)\n {\n \tint num_matches = 0;\n-\tconst char *remote = unique_tracking_name(arg, rev, &num_matches);\n+\tchar *remote = unique_tracking_name(arg, rev, &num_matches);\n \n \tif (remote && could_be_checkout_paths) {\n \t\tdie(_(\"'%s' could be both a local file and a tracking branch.\\n\"\n@@ -1316,6 +1316,7 @@ static int parse_branchname_arg(int argc, const char **argv,\n \tconst char **new_branch = &opts->new_branch;\n \tint argcount = 0;\n \tconst char *arg;\n+\tchar *remote = NULL;\n \tint dash_dash_pos;\n \tint has_dash_dash = 0;\n \tint i;\n@@ -1416,8 +1417,8 @@ static int parse_branchname_arg(int argc, const char **argv,\n \t\t\trecover_with_dwim = 0;\n \n \t\tif (recover_with_dwim) {\n-\t\t\tconst char *remote = parse_remote_branch(arg, rev,\n-\t\t\t\t\t\t\t\t could_be_checkout_paths);\n+\t\t\tremote = parse_remote_branch(arg, rev,\n+\t\t\t\t\t\t     could_be_checkout_paths);\n \t\t\tif (remote) {\n \t\t\t\t*new_branch = arg;\n \t\t\t\targ = remote;\n@@ -1459,6 +1460,7 @@ static int parse_branchname_arg(int argc, const char **argv,\n \t\targc--;\n \t}\n \n+\tfree(remote);\n \treturn argcount;\n }\n \ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 7e0868df72..937da6c0ee 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -736,16 +736,14 @@ static int dwim_orphan(const struct add_opts *opts, int opt_track, int remote)\n \treturn 1;\n }\n \n-static const char *dwim_branch(const char *path, const char **new_branch)\n+static char *dwim_branch(const char *path, char **new_branch)\n {\n \tint n;\n \tint branch_exists;\n \tconst char *s = worktree_basename(path, &n);\n-\tconst char *branchname = xstrndup(s, n);\n+\tchar *branchname = xstrndup(s, n);\n \tstruct strbuf ref = STRBUF_INIT;\n \n-\tUNLEAK(branchname);\n-\n \tbranch_exists = !strbuf_check_branch_ref(&ref, branchname) &&\n \t\t\trefs_ref_exists(get_main_ref_store(the_repository),\n \t\t\t\t\tref.buf);\n@@ -756,8 +754,7 @@ static const char *dwim_branch(const char *path, const char **new_branch)\n \t*new_branch = branchname;\n \tif (guess_remote) {\n \t\tstruct object_id oid;\n-\t\tconst char *remote =\n-\t\t\tunique_tracking_name(*new_branch, &oid, NULL);\n+\t\tchar *remote = unique_tracking_name(*new_branch, &oid, NULL);\n \t\treturn remote;\n \t}\n \treturn NULL;\n@@ -769,6 +766,8 @@ static int add(int ac, const char **av, const char *prefix)\n \tconst char *new_branch_force = NULL;\n \tchar *path;\n \tconst char *branch;\n+\tchar *branch_to_free = NULL;\n+\tchar *new_branch_to_free = NULL;\n \tconst char *new_branch = NULL;\n \tconst char *opt_track = NULL;\n \tconst char *lock_reason = NULL;\n@@ -859,16 +858,17 @@ static int add(int ac, const char **av, const char *prefix)\n \t\topts.orphan = dwim_orphan(&opts, !!opt_track, 0);\n \t} else if (ac < 2) {\n \t\t/* DWIM: Guess branch name from path. */\n-\t\tconst char *s = dwim_branch(path, &new_branch);\n+\t\tchar *s = dwim_branch(path, &new_branch_to_free);\n \t\tif (s)\n-\t\t\tbranch = s;\n+\t\t\tbranch = branch_to_free = s;\n+\t\tnew_branch = new_branch_to_free;\n \n \t\t/* DWIM: Infer --orphan when repo has no refs. */\n \t\topts.orphan = (!s) && dwim_orphan(&opts, !!opt_track, 1);\n \t} else if (ac == 2) {\n \t\tstruct object_id oid;\n \t\tstruct commit *commit;\n-\t\tconst char *remote;\n+\t\tchar *remote;\n \n \t\tcommit = lookup_commit_reference_by_name(branch);\n \t\tif (!commit) {\n@@ -923,6 +923,8 @@ static int add(int ac, const char **av, const char *prefix)\n \n \tret = add_worktree(path, branch, &opts);\n \tfree(path);\n+\tfree(branch_to_free);\n+\tfree(new_branch_to_free);\n \treturn ret;\n }\n \ndiff --git a/checkout.c b/checkout.c\nindex 4256e71a7c..cfaea4bd10 100644\n--- a/checkout.c\n+++ b/checkout.c\n@@ -45,8 +45,8 @@ static int check_tracking_name(struct remote *remote, void *cb_data)\n \treturn 0;\n }\n \n-const char *unique_tracking_name(const char *name, struct object_id *oid,\n-\t\t\t\t int *dwim_remotes_matched)\n+char *unique_tracking_name(const char *name, struct object_id *oid,\n+\t\t\t   int *dwim_remotes_matched)\n {\n \tstruct tracking_name_data cb_data = TRACKING_NAME_DATA_INIT;\n \tconst char *default_remote = NULL;\ndiff --git a/checkout.h b/checkout.h\nindex 3c514a5ab4..ba15a13fb3 100644\n--- a/checkout.h\n+++ b/checkout.h\n@@ -8,8 +8,8 @@\n  * tracking branch.  Return the name of the remote if such a branch\n  * exists, NULL otherwise.\n  */\n-const char *unique_tracking_name(const char *name,\n-\t\t\t\t struct object_id *oid,\n-\t\t\t\t int *dwim_remotes_matched);\n+char *unique_tracking_name(const char *name,\n+\t\t\t   struct object_id *oid,\n+\t\t\t   int *dwim_remotes_matched);\n \n #endif /* CHECKOUT_H */\ndiff --git a/t/t2024-checkout-dwim.sh b/t/t2024-checkout-dwim.sh\nindex a3b1449ef1..2caada3d83 100755\n--- a/t/t2024-checkout-dwim.sh\n+++ b/t/t2024-checkout-dwim.sh\n@@ -4,6 +4,7 @@ test_description='checkout <branch>\n \n Ensures that checkout on an unborn branch does what the user expects'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n # Is the current branch \"refs/heads/$1\"?\ndiff --git a/t/t2060-switch.sh b/t/t2060-switch.sh\nindex c91c4db936..77b2346291 100755\n--- a/t/t2060-switch.sh\n+++ b/t/t2060-switch.sh\n@@ -5,6 +5,7 @@ test_description='switch basic functionality'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n test_expect_success 'setup' '\ndiff --git a/t/t3426-rebase-submodule.sh b/t/t3426-rebase-submodule.sh\nindex ba069dccbd..94ea88e384 100755\n--- a/t/t3426-rebase-submodule.sh\n+++ b/t/t3426-rebase-submodule.sh\n@@ -2,6 +2,7 @@\n \n test_description='rebase can handle submodules'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-submodule-update.sh\n . \"$TEST_DIRECTORY\"/lib-rebase.sh\ndiff --git a/t/t3512-cherry-pick-submodule.sh b/t/t3512-cherry-pick-submodule.sh\nindex f22d1ddead..9387a22a9e 100755\n--- a/t/t3512-cherry-pick-submodule.sh\n+++ b/t/t3512-cherry-pick-submodule.sh\n@@ -5,6 +5,7 @@ test_description='cherry-pick can handle submodules'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-submodule-update.sh\n \ndiff --git a/t/t3513-revert-submodule.sh b/t/t3513-revert-submodule.sh\nindex 8bfe3ed246..e178968b40 100755\n--- a/t/t3513-revert-submodule.sh\n+++ b/t/t3513-revert-submodule.sh\n@@ -2,6 +2,7 @@\n \n test_description='revert can handle submodules'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-submodule-update.sh\n \ndiff --git a/t/t3600-rm.sh b/t/t3600-rm.sh\nindex 98259e2ada..31ac31d4bc 100755\n--- a/t/t3600-rm.sh\n+++ b/t/t3600-rm.sh\n@@ -8,6 +8,7 @@ test_description='Test of the various options to git rm.'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n # Setup some files to be removed, some with funny characters\ndiff --git a/t/t3906-stash-submodule.sh b/t/t3906-stash-submodule.sh\nindex 0f7348ec21..0f61f01ef4 100755\n--- a/t/t3906-stash-submodule.sh\n+++ b/t/t3906-stash-submodule.sh\n@@ -2,6 +2,7 @@\n \n test_description='stash can handle submodules'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-submodule-update.sh\n \ndiff --git a/t/t4137-apply-submodule.sh b/t/t4137-apply-submodule.sh\nindex 07d5262537..ebd0d4ad17 100755\n--- a/t/t4137-apply-submodule.sh\n+++ b/t/t4137-apply-submodule.sh\n@@ -2,6 +2,7 @@\n \n test_description='git apply handling submodules'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-submodule-update.sh\n \ndiff --git a/t/t6041-bisect-submodule.sh b/t/t6041-bisect-submodule.sh\nindex 82013fc903..3946e18089 100755\n--- a/t/t6041-bisect-submodule.sh\n+++ b/t/t6041-bisect-submodule.sh\n@@ -2,6 +2,7 @@\n \n test_description='bisect can handle submodules'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-submodule-update.sh\n \ndiff --git a/t/t6438-submodule-directory-file-conflicts.sh b/t/t6438-submodule-directory-file-conflicts.sh\nindex 8df67a0ef9..3594190af8 100755\n--- a/t/t6438-submodule-directory-file-conflicts.sh\n+++ b/t/t6438-submodule-directory-file-conflicts.sh\n@@ -2,6 +2,7 @@\n \n test_description='merge can handle submodules'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-submodule-update.sh\n \n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495388","messageId":"d4bf3c1f9566f34751eb5669e66e9c100ece02b3.1716465556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716465556.git.ps@pks.im","subject":"[PATCH 05/20] http: refactor code to clarify memory ownership","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-23T12:25:35Z","receivedAt":"2024-05-23T12:25:39Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"There are various variables assigned via `git_config_string()` and\n`git_config_pathname()` which are never free'd. This bug is relatable\nbecause the out parameter of those functions are a `const char **`, even\nthough memory ownership is transferred to the caller.\n\nWe're about to adapt the functions to instead use `char **`. Prepare the\ncode accordingly. Note that the `(const char **)` casts will go away\nonce we have adapted the functions.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n http.c | 62 ++++++++++++++++++++++++++++++----------------------------\n 1 file changed, 32 insertions(+), 30 deletions(-)\n\ndiff --git a/http.c b/http.c\nindex 752c879c1f..db2e2f1d39 100644\n--- a/http.c\n+++ b/http.c\n@@ -39,8 +39,8 @@ char curl_errorstr[CURL_ERROR_SIZE];\n static int curl_ssl_verify = -1;\n static int curl_ssl_try;\n static const char *curl_http_version = NULL;\n-static const char *ssl_cert;\n-static const char *ssl_cert_type;\n+static char *ssl_cert;\n+static char *ssl_cert_type;\n static const char *ssl_cipherlist;\n static const char *ssl_version;\n static struct {\n@@ -59,23 +59,23 @@ static struct {\n \t{ \"tlsv1.3\", CURL_SSLVERSION_TLSv1_3 },\n #endif\n };\n-static const char *ssl_key;\n-static const char *ssl_key_type;\n-static const char *ssl_capath;\n-static const char *curl_no_proxy;\n+static char *ssl_key;\n+static char *ssl_key_type;\n+static char *ssl_capath;\n+static char *curl_no_proxy;\n #ifdef GIT_CURL_HAVE_CURLOPT_PINNEDPUBLICKEY\n static const char *ssl_pinnedkey;\n #endif\n-static const char *ssl_cainfo;\n+static char *ssl_cainfo;\n static long curl_low_speed_limit = -1;\n static long curl_low_speed_time = -1;\n static int curl_ftp_no_epsv;\n-static const char *curl_http_proxy;\n-static const char *http_proxy_authmethod;\n+static char *curl_http_proxy;\n+static char *http_proxy_authmethod;\n \n-static const char *http_proxy_ssl_cert;\n-static const char *http_proxy_ssl_key;\n-static const char *http_proxy_ssl_ca_info;\n+static char *http_proxy_ssl_cert;\n+static char *http_proxy_ssl_key;\n+static char *http_proxy_ssl_ca_info;\n static struct credential proxy_cert_auth = CREDENTIAL_INIT;\n static int proxy_ssl_cert_password_required;\n \n@@ -112,7 +112,7 @@ static const char *curl_cookie_file;\n static int curl_save_cookies;\n struct credential http_auth = CREDENTIAL_INIT;\n static int http_proactive_auth;\n-static const char *user_agent;\n+static char *user_agent;\n static int curl_empty_auth = -1;\n \n enum http_follow_config http_follow_config = HTTP_FOLLOW_INITIAL;\n@@ -381,17 +381,17 @@ static int http_options(const char *var, const char *value,\n \tif (!strcmp(\"http.sslversion\", var))\n \t\treturn git_config_string(&ssl_version, var, value);\n \tif (!strcmp(\"http.sslcert\", var))\n-\t\treturn git_config_pathname(&ssl_cert, var, value);\n+\t\treturn git_config_pathname((const char **)&ssl_cert, var, value);\n \tif (!strcmp(\"http.sslcerttype\", var))\n-\t\treturn git_config_string(&ssl_cert_type, var, value);\n+\t\treturn git_config_string((const char **)&ssl_cert_type, var, value);\n \tif (!strcmp(\"http.sslkey\", var))\n-\t\treturn git_config_pathname(&ssl_key, var, value);\n+\t\treturn git_config_pathname((const char **)&ssl_key, var, value);\n \tif (!strcmp(\"http.sslkeytype\", var))\n-\t\treturn git_config_string(&ssl_key_type, var, value);\n+\t\treturn git_config_string((const char **)&ssl_key_type, var, value);\n \tif (!strcmp(\"http.sslcapath\", var))\n-\t\treturn git_config_pathname(&ssl_capath, var, value);\n+\t\treturn git_config_pathname((const char **)&ssl_capath, var, value);\n \tif (!strcmp(\"http.sslcainfo\", var))\n-\t\treturn git_config_pathname(&ssl_cainfo, var, value);\n+\t\treturn git_config_pathname((const char **)&ssl_cainfo, var, value);\n \tif (!strcmp(\"http.sslcertpasswordprotected\", var)) {\n \t\tssl_cert_password_required = git_config_bool(var, value);\n \t\treturn 0;\n@@ -440,19 +440,19 @@ static int http_options(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \tif (!strcmp(\"http.proxy\", var))\n-\t\treturn git_config_string(&curl_http_proxy, var, value);\n+\t\treturn git_config_string((const char **)&curl_http_proxy, var, value);\n \n \tif (!strcmp(\"http.proxyauthmethod\", var))\n-\t\treturn git_config_string(&http_proxy_authmethod, var, value);\n+\t\treturn git_config_string((const char **)&http_proxy_authmethod, var, value);\n \n \tif (!strcmp(\"http.proxysslcert\", var))\n-\t\treturn git_config_string(&http_proxy_ssl_cert, var, value);\n+\t\treturn git_config_string((const char **)&http_proxy_ssl_cert, var, value);\n \n \tif (!strcmp(\"http.proxysslkey\", var))\n-\t\treturn git_config_string(&http_proxy_ssl_key, var, value);\n+\t\treturn git_config_string((const char **)&http_proxy_ssl_key, var, value);\n \n \tif (!strcmp(\"http.proxysslcainfo\", var))\n-\t\treturn git_config_string(&http_proxy_ssl_ca_info, var, value);\n+\t\treturn git_config_string((const char **)&http_proxy_ssl_ca_info, var, value);\n \n \tif (!strcmp(\"http.proxysslcertpasswordprotected\", var)) {\n \t\tproxy_ssl_cert_password_required = git_config_bool(var, value);\n@@ -476,7 +476,7 @@ static int http_options(const char *var, const char *value,\n \t}\n \n \tif (!strcmp(\"http.useragent\", var))\n-\t\treturn git_config_string(&user_agent, var, value);\n+\t\treturn git_config_string((const char **)&user_agent, var, value);\n \n \tif (!strcmp(\"http.emptyauth\", var)) {\n \t\tif (value && !strcmp(\"auto\", value))\n@@ -592,10 +592,10 @@ static void init_curl_http_auth(CURL *result)\n }\n \n /* *var must be free-able */\n-static void var_override(const char **var, char *value)\n+static void var_override(char **var, char *value)\n {\n \tif (value) {\n-\t\tfree((void *)*var);\n+\t\tfree(*var);\n \t\t*var = xstrdup(value);\n \t}\n }\n@@ -1233,11 +1233,13 @@ static CURL *get_curl_handle(void)\n \treturn result;\n }\n \n-static void set_from_env(const char **var, const char *envname)\n+static void set_from_env(char **var, const char *envname)\n {\n \tconst char *val = getenv(envname);\n-\tif (val)\n-\t\t*var = val;\n+\tif (val) {\n+\t\tFREE_AND_NULL(*var);\n+\t\t*var = xstrdup(val);\n+\t}\n }\n \n void http_init(struct remote *remote, const char *url, int proactive_auth)\n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495389","messageId":"170276277217a3582f343dad9b429936682664f4.1716465556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716465556.git.ps@pks.im","subject":"[PATCH 06/20] config: clarify memory ownership in `git_config_pathname()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-23T12:25:40Z","receivedAt":"2024-05-23T12:25:44Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The out parameter of `git_config_pathname()` is a `const char **` even\nthough we transfer ownership of memory to the caller. This is quite\nmisleading and has led to many memory leaks all over the place. Adapt\nthe parameter to instead be `char **`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/blame.c        |  2 +-\n builtin/commit.c       |  2 +-\n builtin/config.c       |  2 +-\n builtin/log.c          |  2 +-\n builtin/receive-pack.c |  4 ++--\n config.c               | 10 +++++-----\n config.h               |  8 ++++----\n diff.c                 |  2 +-\n environment.c          |  6 +++---\n environment.h          |  6 +++---\n fetch-pack.c           |  4 ++--\n fsck.c                 |  4 ++--\n fsmonitor-settings.c   |  5 ++++-\n gpg-interface.c        |  4 +++-\n http.c                 | 12 ++++++------\n mailmap.c              |  2 +-\n mailmap.h              |  2 +-\n setup.c                |  6 +++---\n 18 files changed, 44 insertions(+), 39 deletions(-)\n\ndiff --git a/builtin/blame.c b/builtin/blame.c\nindex 6bc7aa6085..838cd476be 100644\n--- a/builtin/blame.c\n+++ b/builtin/blame.c\n@@ -718,7 +718,7 @@ static int git_blame_config(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"blame.ignorerevsfile\")) {\n-\t\tconst char *str;\n+\t\tchar *str;\n \t\tint ret;\n \n \t\tret = git_config_pathname(&str, var, value);\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 78bfae2164..1cc88e92bf 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -107,7 +107,7 @@ static enum {\n } commit_style;\n \n static const char *logfile, *force_author;\n-static const char *template_file;\n+static char *template_file;\n /*\n  * The _message variables are commit names from which to take\n  * the commit message and/or authorship.\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 80aa9d8a66..cc343f55ca 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -277,7 +277,7 @@ static int format_config(struct strbuf *buf, const char *key_,\n \t\t\telse\n \t\t\t\tstrbuf_addstr(buf, v ? \"true\" : \"false\");\n \t\t} else if (type == TYPE_PATH) {\n-\t\t\tconst char *v;\n+\t\t\tchar *v;\n \t\t\tif (git_config_pathname(&v, key_, value_) < 0)\n \t\t\t\treturn -1;\n \t\t\tstrbuf_addstr(buf, v);\ndiff --git a/builtin/log.c b/builtin/log.c\nindex b17dd8b40a..a2f5845556 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -957,7 +957,7 @@ static int do_signoff;\n static enum auto_base_setting auto_base;\n static char *from;\n static const char *signature = git_version_string;\n-static const char *signature_file;\n+static char *signature_file;\n static enum cover_setting config_cover_letter;\n static const char *config_output_directory;\n static enum cover_from_description cover_from_description_mode = COVER_FROM_MESSAGE;\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex be8969a84a..56228ad314 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -168,13 +168,13 @@ static int receive_pack_config(const char *var, const char *value,\n \t}\n \n \tif (strcmp(var, \"receive.fsck.skiplist\") == 0) {\n-\t\tconst char *path;\n+\t\tchar *path;\n \n \t\tif (git_config_pathname(&path, var, value))\n \t\t\treturn 1;\n \t\tstrbuf_addf(&fsck_msg_types, \"%cskiplist=%s\",\n \t\t\tfsck_msg_types.len ? ',' : '=', path);\n-\t\tfree((char *)path);\n+\t\tfree(path);\n \t\treturn 0;\n \t}\n \ndiff --git a/config.c b/config.c\nindex d57996240b..fb56e11276 100644\n--- a/config.c\n+++ b/config.c\n@@ -1346,7 +1346,7 @@ int git_config_string(const char **dest, const char *var, const char *value)\n \treturn 0;\n }\n \n-int git_config_pathname(const char **dest, const char *var, const char *value)\n+int git_config_pathname(char **dest, const char *var, const char *value)\n {\n \tif (!value)\n \t\treturn config_error_nonbool(var);\n@@ -1597,7 +1597,7 @@ static int git_default_core_config(const char *var, const char *value,\n \t\treturn git_config_string(&askpass_program, var, value);\n \n \tif (!strcmp(var, \"core.excludesfile\")) {\n-\t\tfree((char *)excludes_file);\n+\t\tfree(excludes_file);\n \t\treturn git_config_pathname(&excludes_file, var, value);\n \t}\n \n@@ -2494,7 +2494,7 @@ int git_configset_get_maybe_bool(struct config_set *set, const char *key, int *d\n \t\treturn 1;\n }\n \n-int git_configset_get_pathname(struct config_set *set, const char *key, const char **dest)\n+int git_configset_get_pathname(struct config_set *set, const char *key, char **dest)\n {\n \tconst char *value;\n \tif (!git_configset_get_value(set, key, &value, NULL))\n@@ -2639,7 +2639,7 @@ int repo_config_get_maybe_bool(struct repository *repo,\n }\n \n int repo_config_get_pathname(struct repository *repo,\n-\t\t\t     const char *key, const char **dest)\n+\t\t\t     const char *key, char **dest)\n {\n \tint ret;\n \tgit_config_check_init(repo);\n@@ -2738,7 +2738,7 @@ int git_config_get_maybe_bool(const char *key, int *dest)\n \treturn repo_config_get_maybe_bool(the_repository, key, dest);\n }\n \n-int git_config_get_pathname(const char *key, const char **dest)\n+int git_config_get_pathname(const char *key, char **dest)\n {\n \treturn repo_config_get_pathname(the_repository, key, dest);\n }\ndiff --git a/config.h b/config.h\nindex db8b608064..b3103bba94 100644\n--- a/config.h\n+++ b/config.h\n@@ -286,7 +286,7 @@ int git_config_string(const char **, const char *, const char *);\n  * Similar to `git_config_string`, but expands `~` or `~user` into the\n  * user's home directory when found at the beginning of the path.\n  */\n-int git_config_pathname(const char **, const char *, const char *);\n+int git_config_pathname(char **, const char *, const char *);\n \n int git_config_expiry_date(timestamp_t *, const char *, const char *);\n int git_config_color(char *, const char *, const char *);\n@@ -541,7 +541,7 @@ int git_configset_get_ulong(struct config_set *cs, const char *key, unsigned lon\n int git_configset_get_bool(struct config_set *cs, const char *key, int *dest);\n int git_configset_get_bool_or_int(struct config_set *cs, const char *key, int *is_bool, int *dest);\n int git_configset_get_maybe_bool(struct config_set *cs, const char *key, int *dest);\n-int git_configset_get_pathname(struct config_set *cs, const char *key, const char **dest);\n+int git_configset_get_pathname(struct config_set *cs, const char *key, char **dest);\n \n /* Functions for reading a repository's config */\n struct repository;\n@@ -577,7 +577,7 @@ int repo_config_get_bool_or_int(struct repository *repo,\n int repo_config_get_maybe_bool(struct repository *repo,\n \t\t\t       const char *key, int *dest);\n int repo_config_get_pathname(struct repository *repo,\n-\t\t\t     const char *key, const char **dest);\n+\t\t\t     const char *key, char **dest);\n \n /*\n  * Functions for reading protected config. By definition, protected\n@@ -687,7 +687,7 @@ int git_config_get_maybe_bool(const char *key, int *dest);\n  * Similar to `git_config_get_string`, but expands `~` or `~user` into\n  * the user's home directory when found at the beginning of the path.\n  */\n-int git_config_get_pathname(const char *key, const char **dest);\n+int git_config_get_pathname(const char *key, char **dest);\n \n int git_config_get_index_threads(int *dest);\n int git_config_get_split_index(void);\ndiff --git a/diff.c b/diff.c\nindex ded9ac70df..902df9286a 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -58,7 +58,7 @@ static int diff_context_default = 3;\n static int diff_interhunk_context_default;\n static const char *diff_word_regex_cfg;\n static const char *external_diff_cmd_cfg;\n-static const char *diff_order_file_cfg;\n+static char *diff_order_file_cfg;\n int diff_auto_refresh_index = 1;\n static int diff_mnemonic_prefix;\n static int diff_no_prefix;\ndiff --git a/environment.c b/environment.c\nindex a73ba9c12c..279ea3fd5e 100644\n--- a/environment.c\n+++ b/environment.c\n@@ -46,8 +46,8 @@ const char *git_commit_encoding;\n const char *git_log_output_encoding;\n char *apply_default_whitespace;\n char *apply_default_ignorewhitespace;\n-const char *git_attributes_file;\n-const char *git_hooks_path;\n+char *git_attributes_file;\n+char *git_hooks_path;\n int zlib_compression_level = Z_BEST_SPEED;\n int pack_compression_level = Z_DEFAULT_COMPRESSION;\n int fsync_object_files = -1;\n@@ -60,7 +60,7 @@ size_t delta_base_cache_limit = 96 * 1024 * 1024;\n unsigned long big_file_threshold = 512 * 1024 * 1024;\n const char *editor_program;\n const char *askpass_program;\n-const char *excludes_file;\n+char *excludes_file;\n enum auto_crlf auto_crlf = AUTO_CRLF_FALSE;\n enum eol core_eol = EOL_UNSET;\n int global_conv_flags_eol = CONV_EOL_RNDTRP_WARN;\ndiff --git a/environment.h b/environment.h\nindex 0b2d457f07..be1b88ad6f 100644\n--- a/environment.h\n+++ b/environment.h\n@@ -131,8 +131,8 @@ extern int warn_ambiguous_refs;\n extern int warn_on_object_refname_ambiguity;\n extern char *apply_default_whitespace;\n extern char *apply_default_ignorewhitespace;\n-extern const char *git_attributes_file;\n-extern const char *git_hooks_path;\n+extern char *git_attributes_file;\n+extern char *git_hooks_path;\n extern int zlib_compression_level;\n extern int pack_compression_level;\n extern size_t packed_git_window_size;\n@@ -229,7 +229,7 @@ extern const char *git_log_output_encoding;\n \n extern const char *editor_program;\n extern const char *askpass_program;\n-extern const char *excludes_file;\n+extern char *excludes_file;\n \n /*\n  * Should we print an ellipsis after an abbreviated SHA-1 value\ndiff --git a/fetch-pack.c b/fetch-pack.c\nindex 8e8f3bba32..d80e9c92dd 100644\n--- a/fetch-pack.c\n+++ b/fetch-pack.c\n@@ -1865,13 +1865,13 @@ static int fetch_pack_config_cb(const char *var, const char *value,\n \tconst char *msg_id;\n \n \tif (strcmp(var, \"fetch.fsck.skiplist\") == 0) {\n-\t\tconst char *path;\n+\t\tchar *path ;\n \n \t\tif (git_config_pathname(&path, var, value))\n \t\t\treturn 1;\n \t\tstrbuf_addf(&fsck_msg_types, \"%cskiplist=%s\",\n \t\t\tfsck_msg_types.len ? ',' : '=', path);\n-\t\tfree((char *)path);\n+\t\tfree(path);\n \t\treturn 0;\n \t}\n \ndiff --git a/fsck.c b/fsck.c\nindex 8ef962199f..7dff41413e 100644\n--- a/fsck.c\n+++ b/fsck.c\n@@ -1330,13 +1330,13 @@ int git_fsck_config(const char *var, const char *value,\n \tconst char *msg_id;\n \n \tif (strcmp(var, \"fsck.skiplist\") == 0) {\n-\t\tconst char *path;\n+\t\tchar *path;\n \t\tstruct strbuf sb = STRBUF_INIT;\n \n \t\tif (git_config_pathname(&path, var, value))\n \t\t\treturn 1;\n \t\tstrbuf_addf(&sb, \"skiplist=%s\", path);\n-\t\tfree((char *)path);\n+\t\tfree(path);\n \t\tfsck_set_msg_types(options, sb.buf);\n \t\tstrbuf_release(&sb);\n \t\treturn 0;\ndiff --git a/fsmonitor-settings.c b/fsmonitor-settings.c\nindex a6a9e6bc19..e818583420 100644\n--- a/fsmonitor-settings.c\n+++ b/fsmonitor-settings.c\n@@ -103,6 +103,7 @@ static struct fsmonitor_settings *alloc_settings(void)\n static void lookup_fsmonitor_settings(struct repository *r)\n {\n \tconst char *const_str;\n+\tchar *to_free = NULL;\n \tint bool_value;\n \n \tif (r->settings.fsmonitor)\n@@ -129,8 +130,9 @@ static void lookup_fsmonitor_settings(struct repository *r)\n \t\tbreak;\n \n \tcase -1: /* config value set to an arbitrary string */\n-\t\tif (repo_config_get_pathname(r, \"core.fsmonitor\", &const_str))\n+\t\tif (repo_config_get_pathname(r, \"core.fsmonitor\", &to_free))\n \t\t\treturn; /* should not happen */\n+\t\tconst_str = to_free;\n \t\tbreak;\n \n \tdefault: /* should not happen */\n@@ -141,6 +143,7 @@ static void lookup_fsmonitor_settings(struct repository *r)\n \t\tfsm_settings__set_hook(r, const_str);\n \telse\n \t\tfsm_settings__set_disabled(r);\n+\tfree(to_free);\n }\n \n enum fsmonitor_mode fsm_settings__get_mode(struct repository *r)\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 1ff94266d2..2b50ed0fa0 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -27,7 +27,9 @@ static void gpg_interface_lazy_init(void)\n }\n \n static char *configured_signing_key;\n-static const char *ssh_default_key_command, *ssh_allowed_signers, *ssh_revocation_file;\n+static const char *ssh_default_key_command;\n+static char *ssh_allowed_signers;\n+static char *ssh_revocation_file;\n static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;\n \n struct gpg_format {\ndiff --git a/http.c b/http.c\nindex db2e2f1d39..fa3ea87451 100644\n--- a/http.c\n+++ b/http.c\n@@ -64,7 +64,7 @@ static char *ssl_key_type;\n static char *ssl_capath;\n static char *curl_no_proxy;\n #ifdef GIT_CURL_HAVE_CURLOPT_PINNEDPUBLICKEY\n-static const char *ssl_pinnedkey;\n+static char *ssl_pinnedkey;\n #endif\n static char *ssl_cainfo;\n static long curl_low_speed_limit = -1;\n@@ -108,7 +108,7 @@ static struct {\n \n static struct credential proxy_auth = CREDENTIAL_INIT;\n static const char *curl_proxyuserpwd;\n-static const char *curl_cookie_file;\n+static char *curl_cookie_file;\n static int curl_save_cookies;\n struct credential http_auth = CREDENTIAL_INIT;\n static int http_proactive_auth;\n@@ -381,17 +381,17 @@ static int http_options(const char *var, const char *value,\n \tif (!strcmp(\"http.sslversion\", var))\n \t\treturn git_config_string(&ssl_version, var, value);\n \tif (!strcmp(\"http.sslcert\", var))\n-\t\treturn git_config_pathname((const char **)&ssl_cert, var, value);\n+\t\treturn git_config_pathname(&ssl_cert, var, value);\n \tif (!strcmp(\"http.sslcerttype\", var))\n \t\treturn git_config_string((const char **)&ssl_cert_type, var, value);\n \tif (!strcmp(\"http.sslkey\", var))\n-\t\treturn git_config_pathname((const char **)&ssl_key, var, value);\n+\t\treturn git_config_pathname(&ssl_key, var, value);\n \tif (!strcmp(\"http.sslkeytype\", var))\n \t\treturn git_config_string((const char **)&ssl_key_type, var, value);\n \tif (!strcmp(\"http.sslcapath\", var))\n-\t\treturn git_config_pathname((const char **)&ssl_capath, var, value);\n+\t\treturn git_config_pathname(&ssl_capath, var, value);\n \tif (!strcmp(\"http.sslcainfo\", var))\n-\t\treturn git_config_pathname((const char **)&ssl_cainfo, var, value);\n+\t\treturn git_config_pathname(&ssl_cainfo, var, value);\n \tif (!strcmp(\"http.sslcertpasswordprotected\", var)) {\n \t\tssl_cert_password_required = git_config_bool(var, value);\n \t\treturn 0;\ndiff --git a/mailmap.c b/mailmap.c\nindex 3d6a5e9400..044466b043 100644\n--- a/mailmap.c\n+++ b/mailmap.c\n@@ -6,7 +6,7 @@\n #include \"object-store-ll.h\"\n #include \"setup.h\"\n \n-const char *git_mailmap_file;\n+char *git_mailmap_file;\n const char *git_mailmap_blob;\n \n struct mailmap_info {\ndiff --git a/mailmap.h b/mailmap.h\nindex 0f8fd2c586..429a760945 100644\n--- a/mailmap.h\n+++ b/mailmap.h\n@@ -3,7 +3,7 @@\n \n struct string_list;\n \n-extern const char *git_mailmap_file;\n+extern char *git_mailmap_file;\n extern const char *git_mailmap_blob;\n \n int read_mailmap(struct string_list *map);\ndiff --git a/setup.c b/setup.c\nindex 9247cded6a..59ff3a19eb 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -1177,13 +1177,13 @@ static int safe_directory_cb(const char *key, const char *value,\n \t} else if (!strcmp(value, \"*\")) {\n \t\tdata->is_safe = 1;\n \t} else {\n-\t\tconst char *interpolated = NULL;\n+\t\tchar *interpolated = NULL;\n \n \t\tif (!git_config_pathname(&interpolated, key, value) &&\n \t\t    !fspathcmp(data->path, interpolated ? interpolated : value))\n \t\t\tdata->is_safe = 1;\n \n-\t\tfree((char *)interpolated);\n+\t\tfree(interpolated);\n \t}\n \n \treturn 0;\n@@ -1822,7 +1822,7 @@ static int template_dir_cb(const char *key, const char *value,\n \t\tchar *path = NULL;\n \n \t\tFREE_AND_NULL(data->path);\n-\t\tif (!git_config_pathname((const char **)&path, key, value))\n+\t\tif (!git_config_pathname(&path, key, value))\n \t\t\tdata->path = path ? path : xstrdup(value);\n \t}\n \n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495390","messageId":"18dce492df7d4337dd639be767f2fe280083e9f6.1716465556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716465556.git.ps@pks.im","subject":"[PATCH 07/20] diff: refactor code to clarify memory ownership of prefixes","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-23T12:25:45Z","receivedAt":"2024-05-23T12:25:49Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The source end destination prefixes are tracked in a `const char *`\narray, but may at times contain allocated strings. The result is that\nthose strings may be leaking because we never free them.\n\nRefactor the code to always store allocated strings in those variables,\nfreeing them as required. This requires us to handle the default values\na bit different compared to before. But given that there is only a\nsingle callsite where we use the variables to `struct diff_options` it's\neasy to handle the defaults there.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n diff.c | 18 ++++++++++--------\n 1 file changed, 10 insertions(+), 8 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex 902df9286a..679ef472f4 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -62,8 +62,8 @@ static char *diff_order_file_cfg;\n int diff_auto_refresh_index = 1;\n static int diff_mnemonic_prefix;\n static int diff_no_prefix;\n-static const char *diff_src_prefix = \"a/\";\n-static const char *diff_dst_prefix = \"b/\";\n+static char *diff_src_prefix;\n+static char *diff_dst_prefix;\n static int diff_relative;\n static int diff_stat_name_width;\n static int diff_stat_graph_width;\n@@ -411,10 +411,12 @@ int git_diff_ui_config(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"diff.srcprefix\")) {\n-\t\treturn git_config_string(&diff_src_prefix, var, value);\n+\t\tFREE_AND_NULL(diff_src_prefix);\n+\t\treturn git_config_string((const char **) &diff_src_prefix, var, value);\n \t}\n \tif (!strcmp(var, \"diff.dstprefix\")) {\n-\t\treturn git_config_string(&diff_dst_prefix, var, value);\n+\t\tFREE_AND_NULL(diff_dst_prefix);\n+\t\treturn git_config_string((const char **) &diff_dst_prefix, var, value);\n \t}\n \tif (!strcmp(var, \"diff.relative\")) {\n \t\tdiff_relative = git_config_bool(var, value);\n@@ -3433,8 +3435,8 @@ void diff_set_noprefix(struct diff_options *options)\n \n void diff_set_default_prefix(struct diff_options *options)\n {\n-\toptions->a_prefix = diff_src_prefix;\n-\toptions->b_prefix = diff_dst_prefix;\n+\toptions->a_prefix = diff_src_prefix ? diff_src_prefix : \"a/\";\n+\toptions->b_prefix = diff_dst_prefix ? diff_dst_prefix : \"b/\";\n }\n \n struct userdiff_driver *get_textconv(struct repository *r,\n@@ -5371,8 +5373,8 @@ static int diff_opt_default_prefix(const struct option *opt,\n \n \tBUG_ON_OPT_NEG(unset);\n \tBUG_ON_OPT_ARG(optarg);\n-\tdiff_src_prefix = \"a/\";\n-\tdiff_dst_prefix = \"b/\";\n+\tFREE_AND_NULL(diff_src_prefix);\n+\tFREE_AND_NULL(diff_dst_prefix);\n \tdiff_set_default_prefix(options);\n \treturn 0;\n }\n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495391","messageId":"667eb3f8fff8053ddcbe3ff3f328223b70539fda.1716465556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716465556.git.ps@pks.im","subject":"[PATCH 08/20] convert: refactor code to clarify ownership of check_roundtrip_encoding","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-23T12:25:50Z","receivedAt":"2024-05-23T12:25:54Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The `check_roundtrip_encoding` variable is tracked in a `const char *`\neven though it may contain allocated strings at times. The result is\nthat those strings may be leaking because we never free them.\n\nRefactor the code to always store allocated strings in this variable.\nThe default value is handled in `check_roundtrip()` now, which is the\nonly user of the variable.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n config.c      |  6 ++++--\n convert.c     | 24 +++++++++++++-----------\n convert.h     |  2 +-\n environment.c |  2 +-\n 4 files changed, 19 insertions(+), 15 deletions(-)\n\ndiff --git a/config.c b/config.c\nindex fb56e11276..f9101045ee 100644\n--- a/config.c\n+++ b/config.c\n@@ -1564,8 +1564,10 @@ static int git_default_core_config(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \n-\tif (!strcmp(var, \"core.checkroundtripencoding\"))\n-\t\treturn git_config_string(&check_roundtrip_encoding, var, value);\n+\tif (!strcmp(var, \"core.checkroundtripencoding\")) {\n+\t\tFREE_AND_NULL(check_roundtrip_encoding);\n+\t\treturn git_config_string((const char **) &check_roundtrip_encoding, var, value);\n+\t}\n \n \tif (!strcmp(var, \"core.notesref\")) {\n \t\tif (!value)\ndiff --git a/convert.c b/convert.c\nindex 35b25eb3cb..03c3c528f9 100644\n--- a/convert.c\n+++ b/convert.c\n@@ -345,30 +345,32 @@ static int check_roundtrip(const char *enc_name)\n \t * space separated encodings (eg. \"UTF-16, ASCII, CP1125\").\n \t * Search for the given encoding in that string.\n \t */\n-\tconst char *found = strcasestr(check_roundtrip_encoding, enc_name);\n+\tconst char *encoding = check_roundtrip_encoding ?\n+\t\tcheck_roundtrip_encoding : \"SHIFT-JIS\";\n+\tconst char *found = strcasestr(encoding, enc_name);\n \tconst char *next;\n \tint len;\n \tif (!found)\n \t\treturn 0;\n \tnext = found + strlen(enc_name);\n-\tlen = strlen(check_roundtrip_encoding);\n+\tlen = strlen(encoding);\n \treturn (found && (\n \t\t\t/*\n-\t\t\t * check that the found encoding is at the\n-\t\t\t * beginning of check_roundtrip_encoding or\n-\t\t\t * that it is prefixed with a space or comma\n+\t\t\t * Check that the found encoding is at the beginning of\n+\t\t\t * encoding or that it is prefixed with a space or\n+\t\t\t * comma.\n \t\t\t */\n-\t\t\tfound == check_roundtrip_encoding || (\n+\t\t\tfound == encoding || (\n \t\t\t\t(isspace(found[-1]) || found[-1] == ',')\n \t\t\t)\n \t\t) && (\n \t\t\t/*\n-\t\t\t * check that the found encoding is at the\n-\t\t\t * end of check_roundtrip_encoding or\n-\t\t\t * that it is suffixed with a space or comma\n+\t\t\t * Check that the found encoding is at the end of\n+\t\t\t * encoding or that it is suffixed with a space\n+\t\t\t * or comma.\n \t\t\t */\n-\t\t\tnext == check_roundtrip_encoding + len || (\n-\t\t\t\tnext < check_roundtrip_encoding + len &&\n+\t\t\tnext == encoding + len || (\n+\t\t\t\tnext < encoding + len &&\n \t\t\t\t(isspace(next[0]) || next[0] == ',')\n \t\t\t)\n \t\t));\ndiff --git a/convert.h b/convert.h\nindex ab8b4fa68d..d925589444 100644\n--- a/convert.h\n+++ b/convert.h\n@@ -92,7 +92,7 @@ void convert_attrs(struct index_state *istate,\n \t\t   struct conv_attrs *ca, const char *path);\n \n extern enum eol core_eol;\n-extern const char *check_roundtrip_encoding;\n+extern char *check_roundtrip_encoding;\n const char *get_cached_convert_stats_ascii(struct index_state *istate,\n \t\t\t\t\t   const char *path);\n const char *get_wt_convert_stats_ascii(const char *path);\ndiff --git a/environment.c b/environment.c\nindex 279ea3fd5e..ab6956559e 100644\n--- a/environment.c\n+++ b/environment.c\n@@ -64,7 +64,7 @@ char *excludes_file;\n enum auto_crlf auto_crlf = AUTO_CRLF_FALSE;\n enum eol core_eol = EOL_UNSET;\n int global_conv_flags_eol = CONV_EOL_RNDTRP_WARN;\n-const char *check_roundtrip_encoding = \"SHIFT-JIS\";\n+char *check_roundtrip_encoding;\n enum branch_track git_branch_track = BRANCH_TRACK_REMOTE;\n enum rebase_setup_type autorebase = AUTOREBASE_NEVER;\n enum push_default_type push_default = PUSH_DEFAULT_UNSPECIFIED;\n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495392","messageId":"11eed8cea7a9977e6b6b1e971a936e9f41e9b4f2.1716465556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716465556.git.ps@pks.im","subject":"[PATCH 09/20] builtin/log: stop using globals for log config","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-23T12:25:54Z","receivedAt":"2024-05-23T12:25:58Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We're using global variables to store the log configuration. Many of\nthese can be set both via the command line and via the config, and\ndepending on how they are being set, they may contain allocated strings.\nThis leads to hard-to-track memory ownership and memory leaks.\n\nRefactor the code to instead use a `struct log_config` that is being\nallocated on the stack. This allows us to more clearly scope the\nvariables, track memory ownership and ultimately release the memory.\n\nThis also prepares us for a change to `git_config_string()`, which will\nbe adapted to have a `char **` out parameter instead of `const char **`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/log.c | 259 ++++++++++++++++++++++++++++++--------------------\n 1 file changed, 156 insertions(+), 103 deletions(-)\n\ndiff --git a/builtin/log.c b/builtin/log.c\nindex a2f5845556..f5da29ee2a 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -48,22 +48,8 @@\n #define COVER_FROM_AUTO_MAX_SUBJECT_LEN 100\n #define FORMAT_PATCH_NAME_MAX_DEFAULT 64\n \n-/* Set a default date-time format for git log (\"log.date\" config variable) */\n-static const char *default_date_mode = NULL;\n-\n-static int default_abbrev_commit;\n-static int default_show_root = 1;\n-static int default_follow;\n-static int default_show_signature;\n-static int default_encode_email_headers = 1;\n-static int decoration_style;\n-static int decoration_given;\n-static int use_mailmap_config = 1;\n static unsigned int force_in_body_from;\n static int stdout_mboxrd;\n-static const char *fmt_patch_subject_prefix = \"PATCH\";\n-static int fmt_patch_name_max = FORMAT_PATCH_NAME_MAX_DEFAULT;\n-static const char *fmt_pretty;\n static int format_no_prefix;\n \n static const char * const builtin_log_usage[] = {\n@@ -111,6 +97,39 @@ static int parse_decoration_style(const char *value)\n \treturn -1;\n }\n \n+struct log_config {\n+\tint default_abbrev_commit;\n+\tint default_show_root;\n+\tint default_follow;\n+\tint default_show_signature;\n+\tint default_encode_email_headers;\n+\tint decoration_style;\n+\tint decoration_given;\n+\tint use_mailmap_config;\n+\tchar *fmt_patch_subject_prefix;\n+\tint fmt_patch_name_max;\n+\tchar *fmt_pretty;\n+\tchar *default_date_mode;\n+};\n+\n+static void log_config_init(struct log_config *cfg)\n+{\n+\tmemset(cfg, 0, sizeof(*cfg));\n+\tcfg->default_show_root = 1;\n+\tcfg->default_encode_email_headers = 1;\n+\tcfg->use_mailmap_config = 1;\n+\tcfg->fmt_patch_subject_prefix = xstrdup(\"PATCH\");\n+\tcfg->fmt_patch_name_max = FORMAT_PATCH_NAME_MAX_DEFAULT;\n+\tcfg->decoration_style = auto_decoration_style();\n+}\n+\n+static void log_config_release(struct log_config *cfg)\n+{\n+\tfree(cfg->default_date_mode);\n+\tfree(cfg->fmt_pretty);\n+\tfree(cfg->fmt_patch_subject_prefix);\n+}\n+\n static int use_default_decoration_filter = 1;\n static struct string_list decorate_refs_exclude = STRING_LIST_INIT_NODUP;\n static struct string_list decorate_refs_exclude_config = STRING_LIST_INIT_NODUP;\n@@ -127,20 +146,22 @@ static int clear_decorations_callback(const struct option *opt UNUSED,\n \treturn 0;\n }\n \n-static int decorate_callback(const struct option *opt UNUSED, const char *arg,\n+static int decorate_callback(const struct option *opt, const char *arg,\n \t\t\t     int unset)\n {\n+\tstruct log_config *cfg = opt->value;\n+\n \tif (unset)\n-\t\tdecoration_style = 0;\n+\t\tcfg->decoration_style = 0;\n \telse if (arg)\n-\t\tdecoration_style = parse_decoration_style(arg);\n+\t\tcfg->decoration_style = parse_decoration_style(arg);\n \telse\n-\t\tdecoration_style = DECORATE_SHORT_REFS;\n+\t\tcfg->decoration_style = DECORATE_SHORT_REFS;\n \n-\tif (decoration_style < 0)\n+\tif (cfg->decoration_style < 0)\n \t\tdie(_(\"invalid --decorate option: %s\"), arg);\n \n-\tdecoration_given = 1;\n+\tcfg->decoration_given = 1;\n \n \treturn 0;\n }\n@@ -160,32 +181,26 @@ static int log_line_range_callback(const struct option *option, const char *arg,\n \treturn 0;\n }\n \n-static void init_log_defaults(void)\n+static void cmd_log_init_defaults(struct rev_info *rev,\n+\t\t\t\t  struct log_config *cfg)\n {\n-\tinit_diff_ui_defaults();\n-\n-\tdecoration_style = auto_decoration_style();\n-}\n-\n-static void cmd_log_init_defaults(struct rev_info *rev)\n-{\n-\tif (fmt_pretty)\n-\t\tget_commit_format(fmt_pretty, rev);\n-\tif (default_follow)\n+\tif (cfg->fmt_pretty)\n+\t\tget_commit_format(cfg->fmt_pretty, rev);\n+\tif (cfg->default_follow)\n \t\trev->diffopt.flags.default_follow_renames = 1;\n \trev->verbose_header = 1;\n \tinit_diffstat_widths(&rev->diffopt);\n \trev->diffopt.flags.recursive = 1;\n \trev->diffopt.flags.allow_textconv = 1;\n-\trev->abbrev_commit = default_abbrev_commit;\n-\trev->show_root_diff = default_show_root;\n-\trev->subject_prefix = fmt_patch_subject_prefix;\n-\trev->patch_name_max = fmt_patch_name_max;\n-\trev->show_signature = default_show_signature;\n-\trev->encode_email_headers = default_encode_email_headers;\n+\trev->abbrev_commit = cfg->default_abbrev_commit;\n+\trev->show_root_diff = cfg->default_show_root;\n+\trev->subject_prefix = cfg->fmt_patch_subject_prefix;\n+\trev->patch_name_max = cfg->fmt_patch_name_max;\n+\trev->show_signature = cfg->default_show_signature;\n+\trev->encode_email_headers = cfg->default_encode_email_headers;\n \n-\tif (default_date_mode)\n-\t\tparse_date_format(default_date_mode, &rev->date_mode);\n+\tif (cfg->default_date_mode)\n+\t\tparse_date_format(cfg->default_date_mode, &rev->date_mode);\n }\n \n static void set_default_decoration_filter(struct decoration_filter *decoration_filter)\n@@ -233,7 +248,8 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f\n }\n \n static void cmd_log_init_finish(int argc, const char **argv, const char *prefix,\n-\t\t\t struct rev_info *rev, struct setup_revision_opt *opt)\n+\t\t\t struct rev_info *rev, struct setup_revision_opt *opt,\n+\t\t\t struct log_config *cfg)\n {\n \tstruct userformat_want w;\n \tint quiet = 0, source = 0, mailmap;\n@@ -258,7 +274,7 @@ static void cmd_log_init_finish(int argc, const char **argv, const char *prefix,\n \t\t\t\tN_(\"pattern\"), N_(\"only decorate refs that match <pattern>\")),\n \t\tOPT_STRING_LIST(0, \"decorate-refs-exclude\", &decorate_refs_exclude,\n \t\t\t\tN_(\"pattern\"), N_(\"do not decorate refs that match <pattern>\")),\n-\t\tOPT_CALLBACK_F(0, \"decorate\", NULL, NULL, N_(\"decorate options\"),\n+\t\tOPT_CALLBACK_F(0, \"decorate\", cfg, NULL, N_(\"decorate options\"),\n \t\t\t       PARSE_OPT_OPTARG, decorate_callback),\n \t\tOPT_CALLBACK('L', NULL, &line_cb, \"range:file\",\n \t\t\t     N_(\"trace the evolution of line range <start>,<end> or function :<funcname> in <file>\"),\n@@ -269,7 +285,7 @@ static void cmd_log_init_finish(int argc, const char **argv, const char *prefix,\n \tline_cb.rev = rev;\n \tline_cb.prefix = prefix;\n \n-\tmailmap = use_mailmap_config;\n+\tmailmap = cfg->use_mailmap_config;\n \targc = parse_options(argc, argv, prefix,\n \t\t\t     builtin_log_options, builtin_log_usage,\n \t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT |\n@@ -314,8 +330,8 @@ static void cmd_log_init_finish(int argc, const char **argv, const char *prefix,\n \t\t * \"log --pretty=raw\" is special; ignore UI oriented\n \t\t * configuration variables such as decoration.\n \t\t */\n-\t\tif (!decoration_given)\n-\t\t\tdecoration_style = 0;\n+\t\tif (!cfg->decoration_given)\n+\t\t\tcfg->decoration_style = 0;\n \t\tif (!rev->abbrev_commit_given)\n \t\t\trev->abbrev_commit = 0;\n \t}\n@@ -326,24 +342,24 @@ static void cmd_log_init_finish(int argc, const char **argv, const char *prefix,\n \t\t\t * Disable decoration loading if the format will not\n \t\t\t * show them anyway.\n \t\t\t */\n-\t\t\tdecoration_style = 0;\n-\t\t} else if (!decoration_style) {\n+\t\t\tcfg->decoration_style = 0;\n+\t\t} else if (!cfg->decoration_style) {\n \t\t\t/*\n \t\t\t * If we are going to show them, make sure we do load\n \t\t\t * them here, but taking care not to override a\n \t\t\t * specific style set by config or --decorate.\n \t\t\t */\n-\t\t\tdecoration_style = DECORATE_SHORT_REFS;\n+\t\t\tcfg->decoration_style = DECORATE_SHORT_REFS;\n \t\t}\n \t}\n \n-\tif (decoration_style || rev->simplify_by_decoration) {\n+\tif (cfg->decoration_style || rev->simplify_by_decoration) {\n \t\tset_default_decoration_filter(&decoration_filter);\n \n-\t\tif (decoration_style)\n+\t\tif (cfg->decoration_style)\n \t\t\trev->show_decorations = 1;\n \n-\t\tload_ref_decorations(&decoration_filter, decoration_style);\n+\t\tload_ref_decorations(&decoration_filter, cfg->decoration_style);\n \t}\n \n \tif (rev->line_level_traverse)\n@@ -353,16 +369,11 @@ static void cmd_log_init_finish(int argc, const char **argv, const char *prefix,\n }\n \n static void cmd_log_init(int argc, const char **argv, const char *prefix,\n-\t\t\t struct rev_info *rev, struct setup_revision_opt *opt)\n+\t\t\t struct rev_info *rev, struct setup_revision_opt *opt,\n+\t\t\t struct log_config *cfg)\n {\n-\tcmd_log_init_defaults(rev);\n-\tcmd_log_init_finish(argc, argv, prefix, rev, opt);\n-}\n-\n-static int cmd_log_deinit(int ret, struct rev_info *rev)\n-{\n-\trelease_revisions(rev);\n-\treturn ret;\n+\tcmd_log_init_defaults(rev, cfg);\n+\tcmd_log_init_finish(argc, argv, prefix, rev, opt, cfg);\n }\n \n /*\n@@ -566,30 +577,37 @@ static int cmd_log_walk(struct rev_info *rev)\n static int git_log_config(const char *var, const char *value,\n \t\t\t  const struct config_context *ctx, void *cb)\n {\n+\tstruct log_config *cfg = cb;\n \tconst char *slot_name;\n \n-\tif (!strcmp(var, \"format.pretty\"))\n-\t\treturn git_config_string(&fmt_pretty, var, value);\n-\tif (!strcmp(var, \"format.subjectprefix\"))\n-\t\treturn git_config_string(&fmt_patch_subject_prefix, var, value);\n+\tif (!strcmp(var, \"format.pretty\")) {\n+\t\tFREE_AND_NULL(cfg->fmt_pretty);\n+\t\treturn git_config_string((const char **) &cfg->fmt_pretty, var, value);\n+\t}\n+\tif (!strcmp(var, \"format.subjectprefix\")) {\n+\t\tFREE_AND_NULL(cfg->fmt_patch_subject_prefix);\n+\t\treturn git_config_string((const char **) &cfg->fmt_patch_subject_prefix, var, value);\n+\t}\n \tif (!strcmp(var, \"format.filenamemaxlength\")) {\n-\t\tfmt_patch_name_max = git_config_int(var, value, ctx->kvi);\n+\t\tcfg->fmt_patch_name_max = git_config_int(var, value, ctx->kvi);\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"format.encodeemailheaders\")) {\n-\t\tdefault_encode_email_headers = git_config_bool(var, value);\n+\t\tcfg->default_encode_email_headers = git_config_bool(var, value);\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"log.abbrevcommit\")) {\n-\t\tdefault_abbrev_commit = git_config_bool(var, value);\n+\t\tcfg->default_abbrev_commit = git_config_bool(var, value);\n \t\treturn 0;\n \t}\n-\tif (!strcmp(var, \"log.date\"))\n-\t\treturn git_config_string(&default_date_mode, var, value);\n+\tif (!strcmp(var, \"log.date\")) {\n+\t\tFREE_AND_NULL(cfg->default_date_mode);\n+\t\treturn git_config_string((const char **) &cfg->default_date_mode, var, value);\n+\t}\n \tif (!strcmp(var, \"log.decorate\")) {\n-\t\tdecoration_style = parse_decoration_style(value);\n-\t\tif (decoration_style < 0)\n-\t\t\tdecoration_style = 0; /* maybe warn? */\n+\t\tcfg->decoration_style = parse_decoration_style(value);\n+\t\tif (cfg->decoration_style < 0)\n+\t\t\tcfg->decoration_style = 0; /* maybe warn? */\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"log.diffmerges\")) {\n@@ -598,21 +616,21 @@ static int git_log_config(const char *var, const char *value,\n \t\treturn diff_merges_config(value);\n \t}\n \tif (!strcmp(var, \"log.showroot\")) {\n-\t\tdefault_show_root = git_config_bool(var, value);\n+\t\tcfg->default_show_root = git_config_bool(var, value);\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"log.follow\")) {\n-\t\tdefault_follow = git_config_bool(var, value);\n+\t\tcfg->default_follow = git_config_bool(var, value);\n \t\treturn 0;\n \t}\n \tif (skip_prefix(var, \"color.decorate.\", &slot_name))\n \t\treturn parse_decorate_color_config(var, slot_name, value);\n \tif (!strcmp(var, \"log.mailmap\")) {\n-\t\tuse_mailmap_config = git_config_bool(var, value);\n+\t\tcfg->use_mailmap_config = git_config_bool(var, value);\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"log.showsignature\")) {\n-\t\tdefault_show_signature = git_config_bool(var, value);\n+\t\tcfg->default_show_signature = git_config_bool(var, value);\n \t\treturn 0;\n \t}\n \n@@ -621,11 +639,14 @@ static int git_log_config(const char *var, const char *value,\n \n int cmd_whatchanged(int argc, const char **argv, const char *prefix)\n {\n+\tstruct log_config cfg;\n \tstruct rev_info rev;\n \tstruct setup_revision_opt opt;\n+\tint ret;\n \n-\tinit_log_defaults();\n-\tgit_config(git_log_config, NULL);\n+\tlog_config_init(&cfg);\n+\tinit_diff_ui_defaults();\n+\tgit_config(git_log_config, &cfg);\n \n \trepo_init_revisions(the_repository, &rev, prefix);\n \tgit_config(grep_config, &rev.grep_filter);\n@@ -635,10 +656,15 @@ int cmd_whatchanged(int argc, const char **argv, const char *prefix)\n \tmemset(&opt, 0, sizeof(opt));\n \topt.def = \"HEAD\";\n \topt.revarg_opt = REVARG_COMMITTISH;\n-\tcmd_log_init(argc, argv, prefix, &rev, &opt);\n+\tcmd_log_init(argc, argv, prefix, &rev, &opt, &cfg);\n \tif (!rev.diffopt.output_format)\n \t\trev.diffopt.output_format = DIFF_FORMAT_RAW;\n-\treturn cmd_log_deinit(cmd_log_walk(&rev), &rev);\n+\n+\tret = cmd_log_walk(&rev);\n+\n+\trelease_revisions(&rev);\n+\tlog_config_release(&cfg);\n+\treturn ret;\n }\n \n static void show_tagger(const char *buf, struct rev_info *rev)\n@@ -733,14 +759,16 @@ static void show_setup_revisions_tweak(struct rev_info *rev)\n \n int cmd_show(int argc, const char **argv, const char *prefix)\n {\n+\tstruct log_config cfg;\n \tstruct rev_info rev;\n \tunsigned int i;\n \tstruct setup_revision_opt opt;\n \tstruct pathspec match_all;\n \tint ret = 0;\n \n-\tinit_log_defaults();\n-\tgit_config(git_log_config, NULL);\n+\tlog_config_init(&cfg);\n+\tinit_diff_ui_defaults();\n+\tgit_config(git_log_config, &cfg);\n \n \tif (the_repository->gitdir) {\n \t\tprepare_repo_settings(the_repository);\n@@ -759,10 +787,14 @@ int cmd_show(int argc, const char **argv, const char *prefix)\n \tmemset(&opt, 0, sizeof(opt));\n \topt.def = \"HEAD\";\n \topt.tweak = show_setup_revisions_tweak;\n-\tcmd_log_init(argc, argv, prefix, &rev, &opt);\n+\tcmd_log_init(argc, argv, prefix, &rev, &opt, &cfg);\n \n-\tif (!rev.no_walk)\n-\t\treturn cmd_log_deinit(cmd_log_walk(&rev), &rev);\n+\tif (!rev.no_walk) {\n+\t\tret = cmd_log_walk(&rev);\n+\t\trelease_revisions(&rev);\n+\t\tlog_config_release(&cfg);\n+\t\treturn ret;\n+\t}\n \n \trev.diffopt.no_free = 1;\n \tfor (i = 0; i < rev.pending.nr && !ret; i++) {\n@@ -832,8 +864,10 @@ int cmd_show(int argc, const char **argv, const char *prefix)\n \n \trev.diffopt.no_free = 0;\n \tdiff_free(&rev.diffopt);\n+\trelease_revisions(&rev);\n+\tlog_config_release(&cfg);\n \n-\treturn cmd_log_deinit(ret, &rev);\n+\treturn ret;\n }\n \n /*\n@@ -841,11 +875,14 @@ int cmd_show(int argc, const char **argv, const char *prefix)\n  */\n int cmd_log_reflog(int argc, const char **argv, const char *prefix)\n {\n+\tstruct log_config cfg;\n \tstruct rev_info rev;\n \tstruct setup_revision_opt opt;\n+\tint ret;\n \n-\tinit_log_defaults();\n-\tgit_config(git_log_config, NULL);\n+\tlog_config_init(&cfg);\n+\tinit_diff_ui_defaults();\n+\tgit_config(git_log_config, &cfg);\n \n \trepo_init_revisions(the_repository, &rev, prefix);\n \tinit_reflog_walk(&rev.reflog_info);\n@@ -854,14 +891,18 @@ int cmd_log_reflog(int argc, const char **argv, const char *prefix)\n \trev.verbose_header = 1;\n \tmemset(&opt, 0, sizeof(opt));\n \topt.def = \"HEAD\";\n-\tcmd_log_init_defaults(&rev);\n+\tcmd_log_init_defaults(&rev, &cfg);\n \trev.abbrev_commit = 1;\n \trev.commit_format = CMIT_FMT_ONELINE;\n \trev.use_terminator = 1;\n \trev.always_show_header = 1;\n-\tcmd_log_init_finish(argc, argv, prefix, &rev, &opt);\n+\tcmd_log_init_finish(argc, argv, prefix, &rev, &opt, &cfg);\n+\n+\tret = cmd_log_walk(&rev);\n \n-\treturn cmd_log_deinit(cmd_log_walk(&rev), &rev);\n+\trelease_revisions(&rev);\n+\tlog_config_release(&cfg);\n+\treturn ret;\n }\n \n static void log_setup_revisions_tweak(struct rev_info *rev)\n@@ -876,11 +917,14 @@ static void log_setup_revisions_tweak(struct rev_info *rev)\n \n int cmd_log(int argc, const char **argv, const char *prefix)\n {\n+\tstruct log_config cfg;\n \tstruct rev_info rev;\n \tstruct setup_revision_opt opt;\n+\tint ret;\n \n-\tinit_log_defaults();\n-\tgit_config(git_log_config, NULL);\n+\tlog_config_init(&cfg);\n+\tinit_diff_ui_defaults();\n+\tgit_config(git_log_config, &cfg);\n \n \trepo_init_revisions(the_repository, &rev, prefix);\n \tgit_config(grep_config, &rev.grep_filter);\n@@ -890,8 +934,13 @@ int cmd_log(int argc, const char **argv, const char *prefix)\n \topt.def = \"HEAD\";\n \topt.revarg_opt = REVARG_COMMITTISH;\n \topt.tweak = log_setup_revisions_tweak;\n-\tcmd_log_init(argc, argv, prefix, &rev, &opt);\n-\treturn cmd_log_deinit(cmd_log_walk(&rev), &rev);\n+\tcmd_log_init(argc, argv, prefix, &rev, &opt, &cfg);\n+\n+\tret = cmd_log_walk(&rev);\n+\n+\trelease_revisions(&rev);\n+\tlog_config_release(&cfg);\n+\treturn ret;\n }\n \n /* format-patch */\n@@ -1884,6 +1933,7 @@ static void infer_range_diff_ranges(struct strbuf *r1,\n \n int cmd_format_patch(int argc, const char **argv, const char *prefix)\n {\n+\tstruct log_config cfg;\n \tstruct commit *commit;\n \tstruct commit **list = NULL;\n \tstruct rev_info rev;\n@@ -1943,7 +1993,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\t\t    N_(\"start numbering patches at <n> instead of 1\")),\n \t\tOPT_STRING('v', \"reroll-count\", &reroll_count, N_(\"reroll-count\"),\n \t\t\t    N_(\"mark the series as Nth re-roll\")),\n-\t\tOPT_INTEGER(0, \"filename-max-length\", &fmt_patch_name_max,\n+\t\tOPT_INTEGER(0, \"filename-max-length\", &cfg.fmt_patch_name_max,\n \t\t\t    N_(\"max length of output filename\")),\n \t\tOPT_CALLBACK_F(0, \"rfc\", &rfc, N_(\"rfc\"),\n \t\t\t       N_(\"add <rfc> (default 'RFC') before 'PATCH'\"),\n@@ -2017,16 +2067,17 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \textra_to.strdup_strings = 1;\n \textra_cc.strdup_strings = 1;\n \n-\tinit_log_defaults();\n+\tlog_config_init(&cfg);\n+\tinit_diff_ui_defaults();\n \tinit_display_notes(&notes_opt);\n-\tgit_config(git_format_config, NULL);\n+\tgit_config(git_format_config, &cfg);\n \trepo_init_revisions(the_repository, &rev, prefix);\n \tgit_config(grep_config, &rev.grep_filter);\n \n \trev.show_notes = show_notes;\n \tmemcpy(&rev.notes_opt, &notes_opt, sizeof(notes_opt));\n \trev.commit_format = CMIT_FMT_EMAIL;\n-\trev.encode_email_headers = default_encode_email_headers;\n+\trev.encode_email_headers = cfg.default_encode_email_headers;\n \trev.expand_tabs_in_log_default = 0;\n \trev.verbose_header = 1;\n \trev.diff = 1;\n@@ -2037,7 +2088,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \ts_r_opt.def = \"HEAD\";\n \ts_r_opt.revarg_opt = REVARG_COMMITTISH;\n \n-\tstrbuf_addstr(&sprefix, fmt_patch_subject_prefix);\n+\tstrbuf_addstr(&sprefix, cfg.fmt_patch_subject_prefix);\n \tif (format_no_prefix)\n \t\tdiff_set_noprefix(&rev.diffopt);\n \n@@ -2059,8 +2110,8 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \trev.force_in_body_from = force_in_body_from;\n \n \t/* Make sure \"0000-$sub.patch\" gives non-negative length for $sub */\n-\tif (fmt_patch_name_max <= strlen(\"0000-\") + strlen(fmt_patch_suffix))\n-\t\tfmt_patch_name_max = strlen(\"0000-\") + strlen(fmt_patch_suffix);\n+\tif (cfg.fmt_patch_name_max <= strlen(\"0000-\") + strlen(fmt_patch_suffix))\n+\t\tcfg.fmt_patch_name_max = strlen(\"0000-\") + strlen(fmt_patch_suffix);\n \n \tif (cover_from_description_arg)\n \t\tcover_from_description_mode = parse_cover_from_description(cover_from_description_arg);\n@@ -2156,7 +2207,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \trev.always_show_header = 1;\n \n \trev.zero_commit = zero_commit;\n-\trev.patch_name_max = fmt_patch_name_max;\n+\trev.patch_name_max = cfg.fmt_patch_name_max;\n \n \tif (!rev.diffopt.flags.text && !no_binary_diff)\n \t\trev.diffopt.flags.binary = 1;\n@@ -2450,7 +2501,9 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \tif (rev.ref_message_ids)\n \t\tstring_list_clear(rev.ref_message_ids, 0);\n \tfree(rev.ref_message_ids);\n-\treturn cmd_log_deinit(0, &rev);\n+\trelease_revisions(&rev);\n+\tlog_config_release(&cfg);\n+\treturn 0;\n }\n \n static int add_pending_commit(const char *arg, struct rev_info *revs, int flags)\n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495393","messageId":"d8cd9a05f8cb93bac31dcfe04517b841bb5e0197.1716465556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716465556.git.ps@pks.im","subject":"[PATCH 10/20] builtin/log: stop using globals for format config","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-23T12:25:59Z","receivedAt":"2024-05-23T12:26:03Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"This commit does the exact same as the preceding commit, only for the\nformat configuration instead of the log configuration.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/log.c | 467 ++++++++++++++++++++++++++++----------------------\n 1 file changed, 265 insertions(+), 202 deletions(-)\n\ndiff --git a/builtin/log.c b/builtin/log.c\nindex f5da29ee2a..890bf0c425 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -945,36 +945,6 @@ int cmd_log(int argc, const char **argv, const char *prefix)\n \n /* format-patch */\n \n-static const char *fmt_patch_suffix = \".patch\";\n-static int numbered = 0;\n-static int auto_number = 1;\n-\n-static char *default_attach = NULL;\n-\n-static struct string_list extra_hdr = STRING_LIST_INIT_NODUP;\n-static struct string_list extra_to = STRING_LIST_INIT_NODUP;\n-static struct string_list extra_cc = STRING_LIST_INIT_NODUP;\n-\n-static void add_header(const char *value)\n-{\n-\tstruct string_list_item *item;\n-\tint len = strlen(value);\n-\twhile (len && value[len - 1] == '\\n')\n-\t\tlen--;\n-\n-\tif (!strncasecmp(value, \"to: \", 4)) {\n-\t\titem = string_list_append(&extra_to, value + 4);\n-\t\tlen -= 4;\n-\t} else if (!strncasecmp(value, \"cc: \", 4)) {\n-\t\titem = string_list_append(&extra_cc, value + 4);\n-\t\tlen -= 4;\n-\t} else {\n-\t\titem = string_list_append(&extra_hdr, value);\n-\t}\n-\n-\titem->string[len] = '\\0';\n-}\n-\n enum cover_setting {\n \tCOVER_UNSET,\n \tCOVER_OFF,\n@@ -1001,17 +971,61 @@ enum auto_base_setting {\n \tAUTO_BASE_WHEN_ABLE\n };\n \n-static enum thread_level thread;\n-static int do_signoff;\n-static enum auto_base_setting auto_base;\n-static char *from;\n-static const char *signature = git_version_string;\n-static char *signature_file;\n-static enum cover_setting config_cover_letter;\n-static const char *config_output_directory;\n-static enum cover_from_description cover_from_description_mode = COVER_FROM_MESSAGE;\n-static int show_notes;\n-static struct display_notes_opt notes_opt;\n+struct format_config {\n+\tstruct log_config log;\n+\tenum thread_level thread;\n+\tint do_signoff;\n+\tenum auto_base_setting auto_base;\n+\tchar *base_commit;\n+\tchar *from;\n+\tchar *signature;\n+\tchar *signature_file;\n+\tenum cover_setting config_cover_letter;\n+\tchar *config_output_directory;\n+\tenum cover_from_description cover_from_description_mode;\n+\tint show_notes;\n+\tstruct display_notes_opt notes_opt;\n+\tint numbered_cmdline_opt;\n+\tint numbered;\n+\tint auto_number;\n+\tchar *default_attach;\n+\tstruct string_list extra_hdr;\n+\tstruct string_list extra_to;\n+\tstruct string_list extra_cc;\n+\tint keep_subject;\n+\tint subject_prefix;\n+\tstruct strbuf sprefix;\n+\tchar *fmt_patch_suffix;\n+};\n+\n+static void format_config_init(struct format_config *cfg)\n+{\n+\tmemset(cfg, 0, sizeof(*cfg));\n+\tlog_config_init(&cfg->log);\n+\tcfg->cover_from_description_mode = COVER_FROM_MESSAGE;\n+\tcfg->auto_number = 1;\n+\tstring_list_init_dup(&cfg->extra_hdr);\n+\tstring_list_init_dup(&cfg->extra_to);\n+\tstring_list_init_dup(&cfg->extra_cc);\n+\tstrbuf_init(&cfg->sprefix, 0);\n+\tcfg->fmt_patch_suffix = xstrdup(\".patch\");\n+}\n+\n+static void format_config_release(struct format_config *cfg)\n+{\n+\tlog_config_release(&cfg->log);\n+\tfree(cfg->base_commit);\n+\tfree(cfg->from);\n+\tfree(cfg->signature);\n+\tfree(cfg->signature_file);\n+\tfree(cfg->config_output_directory);\n+\tfree(cfg->default_attach);\n+\tstring_list_clear(&cfg->extra_hdr, 0);\n+\tstring_list_clear(&cfg->extra_to, 0);\n+\tstring_list_clear(&cfg->extra_cc, 0);\n+\tstrbuf_release(&cfg->sprefix);\n+\tfree(cfg->fmt_patch_suffix);\n+}\n \n static enum cover_from_description parse_cover_from_description(const char *arg)\n {\n@@ -1029,27 +1043,51 @@ static enum cover_from_description parse_cover_from_description(const char *arg)\n \t\tdie(_(\"%s: invalid cover from description mode\"), arg);\n }\n \n+static void add_header(struct format_config *cfg, const char *value)\n+{\n+\tstruct string_list_item *item;\n+\tint len = strlen(value);\n+\twhile (len && value[len - 1] == '\\n')\n+\t\tlen--;\n+\n+\tif (!strncasecmp(value, \"to: \", 4)) {\n+\t\titem = string_list_append(&cfg->extra_to, value + 4);\n+\t\tlen -= 4;\n+\t} else if (!strncasecmp(value, \"cc: \", 4)) {\n+\t\titem = string_list_append(&cfg->extra_cc, value + 4);\n+\t\tlen -= 4;\n+\t} else {\n+\t\titem = string_list_append(&cfg->extra_hdr, value);\n+\t}\n+\n+\titem->string[len] = '\\0';\n+}\n+\n static int git_format_config(const char *var, const char *value,\n \t\t\t     const struct config_context *ctx, void *cb)\n {\n+\tstruct format_config *cfg = cb;\n+\n \tif (!strcmp(var, \"format.headers\")) {\n \t\tif (!value)\n \t\t\tdie(_(\"format.headers without value\"));\n-\t\tadd_header(value);\n+\t\tadd_header(cfg, value);\n \t\treturn 0;\n \t}\n-\tif (!strcmp(var, \"format.suffix\"))\n-\t\treturn git_config_string(&fmt_patch_suffix, var, value);\n+\tif (!strcmp(var, \"format.suffix\")) {\n+\t\tFREE_AND_NULL(cfg->fmt_patch_suffix);\n+\t\treturn git_config_string((const char **) &cfg->fmt_patch_suffix, var, value);\n+\t}\n \tif (!strcmp(var, \"format.to\")) {\n \t\tif (!value)\n \t\t\treturn config_error_nonbool(var);\n-\t\tstring_list_append(&extra_to, value);\n+\t\tstring_list_append(&cfg->extra_to, value);\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"format.cc\")) {\n \t\tif (!value)\n \t\t\treturn config_error_nonbool(var);\n-\t\tstring_list_append(&extra_cc, value);\n+\t\tstring_list_append(&cfg->extra_cc, value);\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"diff.color\") || !strcmp(var, \"color.diff\") ||\n@@ -1058,69 +1096,76 @@ static int git_format_config(const char *var, const char *value,\n \t}\n \tif (!strcmp(var, \"format.numbered\")) {\n \t\tif (value && !strcasecmp(value, \"auto\")) {\n-\t\t\tauto_number = 1;\n+\t\t\tcfg->auto_number = 1;\n \t\t\treturn 0;\n \t\t}\n-\t\tnumbered = git_config_bool(var, value);\n-\t\tauto_number = auto_number && numbered;\n+\t\tcfg->numbered = git_config_bool(var, value);\n+\t\tcfg->auto_number = cfg->auto_number && cfg->numbered;\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"format.attach\")) {\n-\t\tif (value && *value)\n-\t\t\tdefault_attach = xstrdup(value);\n-\t\telse if (value && !*value)\n-\t\t\tFREE_AND_NULL(default_attach);\n-\t\telse\n-\t\t\tdefault_attach = xstrdup(git_version_string);\n+\t\tif (value && *value) {\n+\t\t\tFREE_AND_NULL(cfg->default_attach);\n+\t\t\tcfg->default_attach = xstrdup(value);\n+\t\t} else if (value && !*value) {\n+\t\t\tFREE_AND_NULL(cfg->default_attach);\n+\t\t} else {\n+\t\t\tFREE_AND_NULL(cfg->default_attach);\n+\t\t\tcfg->default_attach = xstrdup(git_version_string);\n+\t\t}\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"format.thread\")) {\n \t\tif (value && !strcasecmp(value, \"deep\")) {\n-\t\t\tthread = THREAD_DEEP;\n+\t\t\tcfg->thread = THREAD_DEEP;\n \t\t\treturn 0;\n \t\t}\n \t\tif (value && !strcasecmp(value, \"shallow\")) {\n-\t\t\tthread = THREAD_SHALLOW;\n+\t\t\tcfg->thread = THREAD_SHALLOW;\n \t\t\treturn 0;\n \t\t}\n-\t\tthread = git_config_bool(var, value) ? THREAD_SHALLOW : THREAD_UNSET;\n+\t\tcfg->thread = git_config_bool(var, value) ? THREAD_SHALLOW : THREAD_UNSET;\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"format.signoff\")) {\n-\t\tdo_signoff = git_config_bool(var, value);\n+\t\tcfg->do_signoff = git_config_bool(var, value);\n \t\treturn 0;\n \t}\n-\tif (!strcmp(var, \"format.signature\"))\n-\t\treturn git_config_string(&signature, var, value);\n-\tif (!strcmp(var, \"format.signaturefile\"))\n-\t\treturn git_config_pathname(&signature_file, var, value);\n+\tif (!strcmp(var, \"format.signature\")) {\n+\t\tFREE_AND_NULL(cfg->signature);\n+\t\treturn git_config_string((const char **) &cfg->signature, var, value);\n+\t}\n+\tif (!strcmp(var, \"format.signaturefile\")) {\n+\t\tFREE_AND_NULL(cfg->signature_file);\n+\t\treturn git_config_pathname(&cfg->signature_file, var, value);\n+\t}\n \tif (!strcmp(var, \"format.coverletter\")) {\n \t\tif (value && !strcasecmp(value, \"auto\")) {\n-\t\t\tconfig_cover_letter = COVER_AUTO;\n+\t\t\tcfg->config_cover_letter = COVER_AUTO;\n \t\t\treturn 0;\n \t\t}\n-\t\tconfig_cover_letter = git_config_bool(var, value) ? COVER_ON : COVER_OFF;\n+\t\tcfg->config_cover_letter = git_config_bool(var, value) ? COVER_ON : COVER_OFF;\n \t\treturn 0;\n \t}\n-\tif (!strcmp(var, \"format.outputdirectory\"))\n-\t\treturn git_config_string(&config_output_directory, var, value);\n+\tif (!strcmp(var, \"format.outputdirectory\")) {\n+\t\tFREE_AND_NULL(cfg->config_output_directory);\n+\t\treturn git_config_string((const char **) &cfg->config_output_directory, var, value);\n+\t}\n \tif (!strcmp(var, \"format.useautobase\")) {\n \t\tif (value && !strcasecmp(value, \"whenAble\")) {\n-\t\t\tauto_base = AUTO_BASE_WHEN_ABLE;\n+\t\t\tcfg->auto_base = AUTO_BASE_WHEN_ABLE;\n \t\t\treturn 0;\n \t\t}\n-\t\tauto_base = git_config_bool(var, value) ? AUTO_BASE_ALWAYS : AUTO_BASE_NEVER;\n+\t\tcfg->auto_base = git_config_bool(var, value) ? AUTO_BASE_ALWAYS : AUTO_BASE_NEVER;\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"format.from\")) {\n \t\tint b = git_parse_maybe_bool(value);\n-\t\tfree(from);\n+\t\tFREE_AND_NULL(cfg->from);\n \t\tif (b < 0)\n-\t\t\tfrom = xstrdup(value);\n+\t\t\tcfg->from = xstrdup(value);\n \t\telse if (b)\n-\t\t\tfrom = xstrdup(git_committer_info(IDENT_NO_DATE));\n-\t\telse\n-\t\t\tfrom = NULL;\n+\t\t\tcfg->from = xstrdup(git_committer_info(IDENT_NO_DATE));\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"format.forceinbodyfrom\")) {\n@@ -1130,15 +1175,15 @@ static int git_format_config(const char *var, const char *value,\n \tif (!strcmp(var, \"format.notes\")) {\n \t\tint b = git_parse_maybe_bool(value);\n \t\tif (b < 0)\n-\t\t\tenable_ref_display_notes(&notes_opt, &show_notes, value);\n+\t\t\tenable_ref_display_notes(&cfg->notes_opt, &cfg->show_notes, value);\n \t\telse if (b)\n-\t\t\tenable_default_display_notes(&notes_opt, &show_notes);\n+\t\t\tenable_default_display_notes(&cfg->notes_opt, &cfg->show_notes);\n \t\telse\n-\t\t\tdisable_display_notes(&notes_opt, &show_notes);\n+\t\t\tdisable_display_notes(&cfg->notes_opt, &cfg->show_notes);\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"format.coverfromdescription\")) {\n-\t\tcover_from_description_mode = parse_cover_from_description(value);\n+\t\tcfg->cover_from_description_mode = parse_cover_from_description(value);\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"format.mboxrd\")) {\n@@ -1159,7 +1204,7 @@ static int git_format_config(const char *var, const char *value,\n \tif (!strcmp(var, \"diff.noprefix\"))\n \t\treturn 0;\n \n-\treturn git_log_config(var, value, ctx, cb);\n+\treturn git_log_config(var, value, ctx, &cfg->log);\n }\n \n static const char *output_directory = NULL;\n@@ -1247,7 +1292,7 @@ static void gen_message_id(struct rev_info *info, char *base)\n \tinfo->message_id = strbuf_detach(&buf, NULL);\n }\n \n-static void print_signature(FILE *file)\n+static void print_signature(const char *signature, FILE *file)\n {\n \tif (!signature || !*signature)\n \t\treturn;\n@@ -1317,14 +1362,15 @@ static void prepare_cover_text(struct pretty_print_context *pp,\n \t\t\t       const char *branch_name,\n \t\t\t       struct strbuf *sb,\n \t\t\t       const char *encoding,\n-\t\t\t       int need_8bit_cte)\n+\t\t\t       int need_8bit_cte,\n+\t\t\t       const struct format_config *cfg)\n {\n \tconst char *subject = \"*** SUBJECT HERE ***\";\n \tconst char *body = \"*** BLURB HERE ***\";\n \tstruct strbuf description_sb = STRBUF_INIT;\n \tstruct strbuf subject_sb = STRBUF_INIT;\n \n-\tif (cover_from_description_mode == COVER_FROM_NONE)\n+\tif (cfg->cover_from_description_mode == COVER_FROM_NONE)\n \t\tgoto do_pp;\n \n \tif (description_file && *description_file)\n@@ -1334,13 +1380,13 @@ static void prepare_cover_text(struct pretty_print_context *pp,\n \tif (!description_sb.len)\n \t\tgoto do_pp;\n \n-\tif (cover_from_description_mode == COVER_FROM_SUBJECT ||\n-\t\t\tcover_from_description_mode == COVER_FROM_AUTO)\n+\tif (cfg->cover_from_description_mode == COVER_FROM_SUBJECT ||\n+\t    cfg->cover_from_description_mode == COVER_FROM_AUTO)\n \t\tbody = format_subject(&subject_sb, description_sb.buf, \" \");\n \n-\tif (cover_from_description_mode == COVER_FROM_MESSAGE ||\n-\t\t\t(cover_from_description_mode == COVER_FROM_AUTO &&\n-\t\t\t subject_sb.len > COVER_FROM_AUTO_MAX_SUBJECT_LEN))\n+\tif (cfg->cover_from_description_mode == COVER_FROM_MESSAGE ||\n+\t    (cfg->cover_from_description_mode == COVER_FROM_AUTO &&\n+\t     subject_sb.len > COVER_FROM_AUTO_MAX_SUBJECT_LEN))\n \t\tbody = description_sb.buf;\n \telse\n \t\tsubject = subject_sb.buf;\n@@ -1377,7 +1423,8 @@ static void make_cover_letter(struct rev_info *rev, int use_separate_file,\n \t\t\t      int nr, struct commit **list,\n \t\t\t      const char *description_file,\n \t\t\t      const char *branch_name,\n-\t\t\t      int quiet)\n+\t\t\t      int quiet,\n+\t\t\t      const struct format_config *cfg)\n {\n \tconst char *committer;\n \tstruct shortlog log;\n@@ -1416,7 +1463,7 @@ static void make_cover_letter(struct rev_info *rev, int use_separate_file,\n \tpp.encode_email_headers = rev->encode_email_headers;\n \tpp_user_info(&pp, NULL, &sb, committer, encoding);\n \tprepare_cover_text(&pp, description_file, branch_name, &sb,\n-\t\t\t   encoding, need_8bit_cte);\n+\t\t\t   encoding, need_8bit_cte, cfg);\n \tfprintf(rev->diffopt.file, \"%s\\n\", sb.buf);\n \n \tfree(pp.after_subject);\n@@ -1517,29 +1564,30 @@ static const char * const builtin_format_patch_usage[] = {\n \tNULL\n };\n \n-static int keep_subject = 0;\n+struct keep_callback_data {\n+\tstruct format_config *cfg;\n+\tstruct rev_info *revs;\n+};\n \n static int keep_callback(const struct option *opt, const char *arg, int unset)\n {\n+\tstruct keep_callback_data *data = opt->value;\n \tBUG_ON_OPT_NEG(unset);\n \tBUG_ON_OPT_ARG(arg);\n-\t((struct rev_info *)opt->value)->total = -1;\n-\tkeep_subject = 1;\n+\tdata->revs->total = -1;\n+\tdata->cfg->keep_subject = 1;\n \treturn 0;\n }\n \n-static int subject_prefix = 0;\n-\n static int subject_prefix_callback(const struct option *opt, const char *arg,\n \t\t\t    int unset)\n {\n-\tstruct strbuf *sprefix;\n+\tstruct format_config *cfg = opt->value;\n \n \tBUG_ON_OPT_NEG(unset);\n-\tsprefix = opt->value;\n-\tsubject_prefix = 1;\n-\tstrbuf_reset(sprefix);\n-\tstrbuf_addstr(sprefix, arg);\n+\tcfg->subject_prefix = 1;\n+\tstrbuf_reset(&cfg->sprefix);\n+\tstrbuf_addstr(&cfg->sprefix, arg);\n \treturn 0;\n }\n \n@@ -1556,15 +1604,14 @@ static int rfc_callback(const struct option *opt, const char *arg,\n \treturn 0;\n }\n \n-static int numbered_cmdline_opt = 0;\n-\n static int numbered_callback(const struct option *opt, const char *arg,\n \t\t\t     int unset)\n {\n+\tstruct format_config *cfg = opt->value;\n \tBUG_ON_OPT_ARG(arg);\n-\t*(int *)opt->value = numbered_cmdline_opt = unset ? 0 : 1;\n+\tcfg->numbered = cfg->numbered_cmdline_opt = unset ? 0 : 1;\n \tif (unset)\n-\t\tauto_number =  0;\n+\t\tcfg->auto_number =  0;\n \treturn 0;\n }\n \n@@ -1588,13 +1635,14 @@ static int output_directory_callback(const struct option *opt, const char *arg,\n \n static int thread_callback(const struct option *opt, const char *arg, int unset)\n {\n-\tenum thread_level *thread = (enum thread_level *)opt->value;\n+\tstruct format_config *cfg = opt->value;\n+\n \tif (unset)\n-\t\t*thread = THREAD_UNSET;\n+\t\tcfg->thread = THREAD_UNSET;\n \telse if (!arg || !strcmp(arg, \"shallow\"))\n-\t\t*thread = THREAD_SHALLOW;\n+\t\tcfg->thread = THREAD_SHALLOW;\n \telse if (!strcmp(arg, \"deep\"))\n-\t\t*thread = THREAD_DEEP;\n+\t\tcfg->thread = THREAD_DEEP;\n \t/*\n \t * Please update _git_formatpatch() in git-completion.bash\n \t * when you add new options.\n@@ -1630,15 +1678,17 @@ static int inline_callback(const struct option *opt, const char *arg, int unset)\n \treturn 0;\n }\n \n-static int header_callback(const struct option *opt UNUSED, const char *arg,\n+static int header_callback(const struct option *opt, const char *arg,\n \t\t\t   int unset)\n {\n+\tstruct format_config *cfg = opt->value;\n+\n \tif (unset) {\n-\t\tstring_list_clear(&extra_hdr, 0);\n-\t\tstring_list_clear(&extra_to, 0);\n-\t\tstring_list_clear(&extra_cc, 0);\n+\t\tstring_list_clear(&cfg->extra_hdr, 0);\n+\t\tstring_list_clear(&cfg->extra_to, 0);\n+\t\tstring_list_clear(&cfg->extra_cc, 0);\n \t} else {\n-\t\tadd_header(arg);\n+\t\tadd_header(cfg, arg);\n \t}\n \treturn 0;\n }\n@@ -1660,17 +1710,17 @@ static int from_callback(const struct option *opt, const char *arg, int unset)\n \n static int base_callback(const struct option *opt, const char *arg, int unset)\n {\n-\tconst char **base_commit = opt->value;\n+\tstruct format_config *cfg = opt->value;\n \n \tif (unset) {\n-\t\tauto_base = AUTO_BASE_NEVER;\n-\t\t*base_commit = NULL;\n+\t\tcfg->auto_base = AUTO_BASE_NEVER;\n+\t\tFREE_AND_NULL(cfg->base_commit);\n \t} else if (!strcmp(arg, \"auto\")) {\n-\t\tauto_base = AUTO_BASE_ALWAYS;\n-\t\t*base_commit = NULL;\n+\t\tcfg->auto_base = AUTO_BASE_ALWAYS;\n+\t\tFREE_AND_NULL(cfg->base_commit);\n \t} else {\n-\t\tauto_base = AUTO_BASE_NEVER;\n-\t\t*base_commit = arg;\n+\t\tcfg->auto_base = AUTO_BASE_NEVER;\n+\t\tcfg->base_commit = xstrdup(arg);\n \t}\n \treturn 0;\n }\n@@ -1681,7 +1731,7 @@ struct base_tree_info {\n \tstruct object_id *patch_id;\n };\n \n-static struct commit *get_base_commit(const char *base_commit,\n+static struct commit *get_base_commit(const struct format_config *cfg,\n \t\t\t\t      struct commit **list,\n \t\t\t\t      int total)\n {\n@@ -1689,9 +1739,9 @@ static struct commit *get_base_commit(const char *base_commit,\n \tstruct commit **rev;\n \tint i = 0, rev_nr = 0, auto_select, die_on_failure, ret;\n \n-\tswitch (auto_base) {\n+\tswitch (cfg->auto_base) {\n \tcase AUTO_BASE_NEVER:\n-\t\tif (base_commit) {\n+\t\tif (cfg->base_commit) {\n \t\t\tauto_select = 0;\n \t\t\tdie_on_failure = 1;\n \t\t} else {\n@@ -1701,11 +1751,11 @@ static struct commit *get_base_commit(const char *base_commit,\n \t\tbreak;\n \tcase AUTO_BASE_ALWAYS:\n \tcase AUTO_BASE_WHEN_ABLE:\n-\t\tif (base_commit) {\n+\t\tif (cfg->base_commit) {\n \t\t\tBUG(\"requested automatic base selection but a commit was provided\");\n \t\t} else {\n \t\t\tauto_select = 1;\n-\t\t\tdie_on_failure = auto_base == AUTO_BASE_ALWAYS;\n+\t\t\tdie_on_failure = cfg->auto_base == AUTO_BASE_ALWAYS;\n \t\t}\n \t\tbreak;\n \tdefault:\n@@ -1713,9 +1763,9 @@ static struct commit *get_base_commit(const char *base_commit,\n \t}\n \n \tif (!auto_select) {\n-\t\tbase = lookup_commit_reference_by_name(base_commit);\n+\t\tbase = lookup_commit_reference_by_name(cfg->base_commit);\n \t\tif (!base)\n-\t\t\tdie(_(\"unknown commit %s\"), base_commit);\n+\t\t\tdie(_(\"unknown commit %s\"), cfg->base_commit);\n \t} else {\n \t\tstruct branch *curr_branch = branch_get(NULL);\n \t\tconst char *upstream = branch_get_upstream(curr_branch, NULL);\n@@ -1933,7 +1983,7 @@ static void infer_range_diff_ranges(struct strbuf *r1,\n \n int cmd_format_patch(int argc, const char **argv, const char *prefix)\n {\n-\tstruct log_config cfg;\n+\tstruct format_config cfg;\n \tstruct commit *commit;\n \tstruct commit **list = NULL;\n \tstruct rev_info rev;\n@@ -1958,7 +2008,6 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \tchar *cover_from_description_arg = NULL;\n \tchar *description_file = NULL;\n \tchar *branch_name = NULL;\n-\tchar *base_commit = NULL;\n \tstruct base_tree_info bases;\n \tstruct commit *base;\n \tint show_progress = 0;\n@@ -1969,18 +2018,24 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \tstruct strbuf rdiff1 = STRBUF_INIT;\n \tstruct strbuf rdiff2 = STRBUF_INIT;\n \tstruct strbuf rdiff_title = STRBUF_INIT;\n-\tstruct strbuf sprefix = STRBUF_INIT;\n \tconst char *rfc = NULL;\n \tint creation_factor = -1;\n+\tconst char *signature = git_version_string;\n+\tconst char *signature_file_arg = NULL;\n+\tstruct keep_callback_data keep_callback_data = {\n+\t\t.cfg = &cfg,\n+\t\t.revs = &rev,\n+\t};\n+\tconst char *fmt_patch_suffix = NULL;\n \n \tconst struct option builtin_format_patch_options[] = {\n-\t\tOPT_CALLBACK_F('n', \"numbered\", &numbered, NULL,\n+\t\tOPT_CALLBACK_F('n', \"numbered\", &cfg, NULL,\n \t\t\t    N_(\"use [PATCH n/m] even with a single patch\"),\n \t\t\t    PARSE_OPT_NOARG, numbered_callback),\n-\t\tOPT_CALLBACK_F('N', \"no-numbered\", &numbered, NULL,\n+\t\tOPT_CALLBACK_F('N', \"no-numbered\", &cfg, NULL,\n \t\t\t    N_(\"use [PATCH] even with multiple patches\"),\n \t\t\t    PARSE_OPT_NOARG | PARSE_OPT_NONEG, no_numbered_callback),\n-\t\tOPT_BOOL('s', \"signoff\", &do_signoff, N_(\"add a Signed-off-by trailer\")),\n+\t\tOPT_BOOL('s', \"signoff\", &cfg.do_signoff, N_(\"add a Signed-off-by trailer\")),\n \t\tOPT_BOOL(0, \"stdout\", &use_stdout,\n \t\t\t    N_(\"print patches to standard out\")),\n \t\tOPT_BOOL(0, \"cover-letter\", &cover_letter,\n@@ -1993,7 +2048,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\t\t    N_(\"start numbering patches at <n> instead of 1\")),\n \t\tOPT_STRING('v', \"reroll-count\", &reroll_count, N_(\"reroll-count\"),\n \t\t\t    N_(\"mark the series as Nth re-roll\")),\n-\t\tOPT_INTEGER(0, \"filename-max-length\", &cfg.fmt_patch_name_max,\n+\t\tOPT_INTEGER(0, \"filename-max-length\", &cfg.log.fmt_patch_name_max,\n \t\t\t    N_(\"max length of output filename\")),\n \t\tOPT_CALLBACK_F(0, \"rfc\", &rfc, N_(\"rfc\"),\n \t\t\t       N_(\"add <rfc> (default 'RFC') before 'PATCH'\"),\n@@ -2003,13 +2058,13 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\t\t    N_(\"generate parts of a cover letter based on a branch's description\")),\n \t\tOPT_FILENAME(0, \"description-file\", &description_file,\n \t\t\t     N_(\"use branch description from file\")),\n-\t\tOPT_CALLBACK_F(0, \"subject-prefix\", &sprefix, N_(\"prefix\"),\n+\t\tOPT_CALLBACK_F(0, \"subject-prefix\", &cfg, N_(\"prefix\"),\n \t\t\t    N_(\"use [<prefix>] instead of [PATCH]\"),\n \t\t\t    PARSE_OPT_NONEG, subject_prefix_callback),\n \t\tOPT_CALLBACK_F('o', \"output-directory\", &output_directory,\n \t\t\t    N_(\"dir\"), N_(\"store resulting files in <dir>\"),\n \t\t\t    PARSE_OPT_NONEG, output_directory_callback),\n-\t\tOPT_CALLBACK_F('k', \"keep-subject\", &rev, NULL,\n+\t\tOPT_CALLBACK_F('k', \"keep-subject\", &keep_callback_data, NULL,\n \t\t\t    N_(\"don't strip/add [PATCH]\"),\n \t\t\t    PARSE_OPT_NOARG | PARSE_OPT_NONEG, keep_callback),\n \t\tOPT_BOOL(0, \"no-binary\", &no_binary_diff,\n@@ -2022,11 +2077,11 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\t\t      N_(\"show patch format instead of default (patch + stat)\"),\n \t\t\t      1, PARSE_OPT_NONEG),\n \t\tOPT_GROUP(N_(\"Messaging\")),\n-\t\tOPT_CALLBACK(0, \"add-header\", NULL, N_(\"header\"),\n+\t\tOPT_CALLBACK(0, \"add-header\", &cfg, N_(\"header\"),\n \t\t\t    N_(\"add email header\"), header_callback),\n-\t\tOPT_STRING_LIST(0, \"to\", &extra_to, N_(\"email\"), N_(\"add To: header\")),\n-\t\tOPT_STRING_LIST(0, \"cc\", &extra_cc, N_(\"email\"), N_(\"add Cc: header\")),\n-\t\tOPT_CALLBACK_F(0, \"from\", &from, N_(\"ident\"),\n+\t\tOPT_STRING_LIST(0, \"to\", &cfg.extra_to, N_(\"email\"), N_(\"add To: header\")),\n+\t\tOPT_STRING_LIST(0, \"cc\", &cfg.extra_cc, N_(\"email\"), N_(\"add Cc: header\")),\n+\t\tOPT_CALLBACK_F(0, \"from\", &cfg.from, N_(\"ident\"),\n \t\t\t    N_(\"set From address to <ident> (or committer ident if absent)\"),\n \t\t\t    PARSE_OPT_OPTARG, from_callback),\n \t\tOPT_STRING(0, \"in-reply-to\", &in_reply_to, N_(\"message-id\"),\n@@ -2038,15 +2093,15 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\t\t    N_(\"inline the patch\"),\n \t\t\t    PARSE_OPT_OPTARG | PARSE_OPT_NONEG,\n \t\t\t    inline_callback),\n-\t\tOPT_CALLBACK_F(0, \"thread\", &thread, N_(\"style\"),\n+\t\tOPT_CALLBACK_F(0, \"thread\", &cfg, N_(\"style\"),\n \t\t\t    N_(\"enable message threading, styles: shallow, deep\"),\n \t\t\t    PARSE_OPT_OPTARG, thread_callback),\n \t\tOPT_STRING(0, \"signature\", &signature, N_(\"signature\"),\n \t\t\t    N_(\"add a signature\")),\n-\t\tOPT_CALLBACK_F(0, \"base\", &base_commit, N_(\"base-commit\"),\n+\t\tOPT_CALLBACK_F(0, \"base\", &cfg, N_(\"base-commit\"),\n \t\t\t       N_(\"add prerequisite tree info to the patch series\"),\n \t\t\t       0, base_callback),\n-\t\tOPT_FILENAME(0, \"signature-file\", &signature_file,\n+\t\tOPT_FILENAME(0, \"signature-file\", &signature_file_arg,\n \t\t\t\tN_(\"add a signature from a file\")),\n \t\tOPT__QUIET(&quiet, N_(\"don't print the patch filenames\")),\n \t\tOPT_BOOL(0, \"progress\", &show_progress,\n@@ -2063,21 +2118,17 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\tOPT_END()\n \t};\n \n-\textra_hdr.strdup_strings = 1;\n-\textra_to.strdup_strings = 1;\n-\textra_cc.strdup_strings = 1;\n-\n-\tlog_config_init(&cfg);\n+\tformat_config_init(&cfg);\n \tinit_diff_ui_defaults();\n-\tinit_display_notes(&notes_opt);\n+\tinit_display_notes(&cfg.notes_opt);\n \tgit_config(git_format_config, &cfg);\n \trepo_init_revisions(the_repository, &rev, prefix);\n \tgit_config(grep_config, &rev.grep_filter);\n \n-\trev.show_notes = show_notes;\n-\tmemcpy(&rev.notes_opt, &notes_opt, sizeof(notes_opt));\n+\trev.show_notes = cfg.show_notes;\n+\tmemcpy(&rev.notes_opt, &cfg.notes_opt, sizeof(cfg.notes_opt));\n \trev.commit_format = CMIT_FMT_EMAIL;\n-\trev.encode_email_headers = cfg.default_encode_email_headers;\n+\trev.encode_email_headers = cfg.log.default_encode_email_headers;\n \trev.expand_tabs_in_log_default = 0;\n \trev.verbose_header = 1;\n \trev.diff = 1;\n@@ -2088,12 +2139,12 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \ts_r_opt.def = \"HEAD\";\n \ts_r_opt.revarg_opt = REVARG_COMMITTISH;\n \n-\tstrbuf_addstr(&sprefix, cfg.fmt_patch_subject_prefix);\n+\tstrbuf_addstr(&cfg.sprefix, cfg.log.fmt_patch_subject_prefix);\n \tif (format_no_prefix)\n \t\tdiff_set_noprefix(&rev.diffopt);\n \n-\tif (default_attach) {\n-\t\trev.mime_boundary = default_attach;\n+\tif (cfg.default_attach) {\n+\t\trev.mime_boundary = cfg.default_attach;\n \t\trev.no_inline = 1;\n \t}\n \n@@ -2109,60 +2160,63 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \n \trev.force_in_body_from = force_in_body_from;\n \n+\tif (!fmt_patch_suffix)\n+\t\tfmt_patch_suffix = cfg.fmt_patch_suffix;\n+\n \t/* Make sure \"0000-$sub.patch\" gives non-negative length for $sub */\n-\tif (cfg.fmt_patch_name_max <= strlen(\"0000-\") + strlen(fmt_patch_suffix))\n-\t\tcfg.fmt_patch_name_max = strlen(\"0000-\") + strlen(fmt_patch_suffix);\n+\tif (cfg.log.fmt_patch_name_max <= strlen(\"0000-\") + strlen(fmt_patch_suffix))\n+\t\tcfg.log.fmt_patch_name_max = strlen(\"0000-\") + strlen(fmt_patch_suffix);\n \n \tif (cover_from_description_arg)\n-\t\tcover_from_description_mode = parse_cover_from_description(cover_from_description_arg);\n+\t\tcfg.cover_from_description_mode = parse_cover_from_description(cover_from_description_arg);\n \n \tif (rfc && rfc[0]) {\n-\t\tsubject_prefix = 1;\n+\t\tcfg.subject_prefix = 1;\n \t\tif (rfc[0] == '-')\n-\t\t\tstrbuf_addf(&sprefix, \" %s\", rfc + 1);\n+\t\t\tstrbuf_addf(&cfg.sprefix, \" %s\", rfc + 1);\n \t\telse\n-\t\t\tstrbuf_insertf(&sprefix, 0, \"%s \", rfc);\n+\t\t\tstrbuf_insertf(&cfg.sprefix, 0, \"%s \", rfc);\n \t}\n \n \tif (reroll_count) {\n-\t\tstrbuf_addf(&sprefix, \" v%s\", reroll_count);\n+\t\tstrbuf_addf(&cfg.sprefix, \" v%s\", reroll_count);\n \t\trev.reroll_count = reroll_count;\n \t}\n \n-\trev.subject_prefix = sprefix.buf;\n+\trev.subject_prefix = cfg.sprefix.buf;\n \n-\tfor (i = 0; i < extra_hdr.nr; i++) {\n-\t\tstrbuf_addstr(&buf, extra_hdr.items[i].string);\n+\tfor (i = 0; i < cfg.extra_hdr.nr; i++) {\n+\t\tstrbuf_addstr(&buf, cfg.extra_hdr.items[i].string);\n \t\tstrbuf_addch(&buf, '\\n');\n \t}\n \n-\tif (extra_to.nr)\n+\tif (cfg.extra_to.nr)\n \t\tstrbuf_addstr(&buf, \"To: \");\n-\tfor (i = 0; i < extra_to.nr; i++) {\n+\tfor (i = 0; i < cfg.extra_to.nr; i++) {\n \t\tif (i)\n \t\t\tstrbuf_addstr(&buf, \"    \");\n-\t\tstrbuf_addstr(&buf, extra_to.items[i].string);\n-\t\tif (i + 1 < extra_to.nr)\n+\t\tstrbuf_addstr(&buf, cfg.extra_to.items[i].string);\n+\t\tif (i + 1 < cfg.extra_to.nr)\n \t\t\tstrbuf_addch(&buf, ',');\n \t\tstrbuf_addch(&buf, '\\n');\n \t}\n \n-\tif (extra_cc.nr)\n+\tif (cfg.extra_cc.nr)\n \t\tstrbuf_addstr(&buf, \"Cc: \");\n-\tfor (i = 0; i < extra_cc.nr; i++) {\n+\tfor (i = 0; i < cfg.extra_cc.nr; i++) {\n \t\tif (i)\n \t\t\tstrbuf_addstr(&buf, \"    \");\n-\t\tstrbuf_addstr(&buf, extra_cc.items[i].string);\n-\t\tif (i + 1 < extra_cc.nr)\n+\t\tstrbuf_addstr(&buf, cfg.extra_cc.items[i].string);\n+\t\tif (i + 1 < cfg.extra_cc.nr)\n \t\t\tstrbuf_addch(&buf, ',');\n \t\tstrbuf_addch(&buf, '\\n');\n \t}\n \n \trev.extra_headers = to_free = strbuf_detach(&buf, NULL);\n \n-\tif (from) {\n-\t\tif (split_ident_line(&rev.from_ident, from, strlen(from)))\n-\t\t\tdie(_(\"invalid ident line: %s\"), from);\n+\tif (cfg.from) {\n+\t\tif (split_ident_line(&rev.from_ident, cfg.from, strlen(cfg.from)))\n+\t\t\tdie(_(\"invalid ident line: %s\"), cfg.from);\n \t}\n \n \tif (start_number < 0)\n@@ -2173,14 +2227,14 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t * and it would conflict with --keep-subject (-k) from the\n \t * command line, reset \"numbered\".\n \t */\n-\tif (numbered && keep_subject && !numbered_cmdline_opt)\n-\t\tnumbered = 0;\n+\tif (cfg.numbered && cfg.keep_subject && !cfg.numbered_cmdline_opt)\n+\t\tcfg.numbered = 0;\n \n-\tif (numbered && keep_subject)\n+\tif (cfg.numbered && cfg.keep_subject)\n \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"-n\", \"-k\");\n-\tif (keep_subject && subject_prefix)\n+\tif (cfg.keep_subject && cfg.subject_prefix)\n \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--subject-prefix/--rfc\", \"-k\");\n-\trev.preserve_subject = keep_subject;\n+\trev.preserve_subject = cfg.keep_subject;\n \n \targc = setup_revisions(argc, argv, &rev, &s_r_opt);\n \tif (argc > 1)\n@@ -2207,7 +2261,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \trev.always_show_header = 1;\n \n \trev.zero_commit = zero_commit;\n-\trev.patch_name_max = cfg.fmt_patch_name_max;\n+\trev.patch_name_max = cfg.log.fmt_patch_name_max;\n \n \tif (!rev.diffopt.flags.text && !no_binary_diff)\n \t\trev.diffopt.flags.binary = 1;\n@@ -2228,7 +2282,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\tint saved;\n \n \t\tif (!output_directory)\n-\t\t\toutput_directory = config_output_directory;\n+\t\t\toutput_directory = cfg.config_output_directory;\n \t\toutput_directory = set_outdir(prefix, output_directory);\n \n \t\tif (rev.diffopt.use_color != GIT_COLOR_ALWAYS)\n@@ -2326,14 +2380,14 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\tgoto done;\n \ttotal = nr;\n \tif (cover_letter == -1) {\n-\t\tif (config_cover_letter == COVER_AUTO)\n+\t\tif (cfg.config_cover_letter == COVER_AUTO)\n \t\t\tcover_letter = (total > 1);\n \t\telse\n-\t\t\tcover_letter = (config_cover_letter == COVER_ON);\n+\t\t\tcover_letter = (cfg.config_cover_letter == COVER_ON);\n \t}\n-\tif (!keep_subject && auto_number && (total > 1 || cover_letter))\n-\t\tnumbered = 1;\n-\tif (numbered)\n+\tif (!cfg.keep_subject && cfg.auto_number && (total > 1 || cover_letter))\n+\t\tcfg.numbered = 1;\n+\tif (cfg.numbered)\n \t\trev.total = total + start_number - 1;\n \n \tif (idiff_prev.nr) {\n@@ -2365,27 +2419,40 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\t\t\t\t     _(\"Range-diff against v%d:\"));\n \t}\n \n+\t/*\n+\t * The order of precedence is:\n+\t *\n+\t *   1. The `--signature` and `--no-signature` options.\n+\t *   2. The `--signature-file` option.\n+\t *   3. The `format.signature` config.\n+\t *   4. The `format.signatureFile` config.\n+\t *   5. Default `git_version_string`.\n+\t */\n \tif (!signature) {\n \t\t; /* --no-signature inhibits all signatures */\n \t} else if (signature && signature != git_version_string) {\n \t\t; /* non-default signature already set */\n-\t} else if (signature_file) {\n+\t} else if (signature_file_arg || (cfg.signature_file && !cfg.signature)) {\n \t\tstruct strbuf buf = STRBUF_INIT;\n+\t\tconst char *signature_file = signature_file_arg ?\n+\t\t\tsignature_file_arg : cfg.signature_file;\n \n \t\tif (strbuf_read_file(&buf, signature_file, 128) < 0)\n \t\t\tdie_errno(_(\"unable to read signature file '%s'\"), signature_file);\n \t\tsignature = strbuf_detach(&buf, NULL);\n+\t} else if (cfg.signature) {\n+\t\tsignature = cfg.signature;\n \t}\n \n \tmemset(&bases, 0, sizeof(bases));\n-\tbase = get_base_commit(base_commit, list, nr);\n+\tbase = get_base_commit(&cfg, list, nr);\n \tif (base) {\n \t\treset_revision_walk();\n \t\tclear_object_flags(UNINTERESTING);\n \t\tprepare_bases(&bases, base, list, nr);\n \t}\n \n-\tif (in_reply_to || thread || cover_letter) {\n+\tif (in_reply_to || cfg.thread || cover_letter) {\n \t\trev.ref_message_ids = xmalloc(sizeof(*rev.ref_message_ids));\n \t\tstring_list_init_dup(rev.ref_message_ids);\n \t}\n@@ -2396,19 +2463,19 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \trev.numbered_files = just_numbers;\n \trev.patch_suffix = fmt_patch_suffix;\n \tif (cover_letter) {\n-\t\tif (thread)\n+\t\tif (cfg.thread)\n \t\t\tgen_message_id(&rev, \"cover\");\n \t\tmake_cover_letter(&rev, !!output_directory,\n-\t\t\t\t  origin, nr, list, description_file, branch_name, quiet);\n+\t\t\t\t  origin, nr, list, description_file, branch_name, quiet, &cfg);\n \t\tprint_bases(&bases, rev.diffopt.file);\n-\t\tprint_signature(rev.diffopt.file);\n+\t\tprint_signature(signature, rev.diffopt.file);\n \t\ttotal++;\n \t\tstart_number--;\n \t\t/* interdiff/range-diff in cover-letter; omit from patches */\n \t\trev.idiff_oid1 = NULL;\n \t\trev.rdiff1 = NULL;\n \t}\n-\trev.add_signoff = do_signoff;\n+\trev.add_signoff = cfg.do_signoff;\n \n \tif (show_progress)\n \t\tprogress = start_delayed_progress(_(\"Generating patches\"), total);\n@@ -2418,7 +2485,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\tcommit = list[nr];\n \t\trev.nr = total - nr + (start_number - 1);\n \t\t/* Make the second and subsequent mails replies to the first */\n-\t\tif (thread) {\n+\t\tif (cfg.thread) {\n \t\t\t/* Have we already had a message ID? */\n \t\t\tif (rev.message_id) {\n \t\t\t\t/*\n@@ -2442,7 +2509,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\t\t\t * letter is a reply to the\n \t\t\t\t * --in-reply-to, if specified.\n \t\t\t\t */\n-\t\t\t\tif (thread == THREAD_SHALLOW\n+\t\t\t\tif (cfg.thread == THREAD_SHALLOW\n \t\t\t\t    && rev.ref_message_ids->nr > 0\n \t\t\t\t    && (!cover_letter || rev.nr > 1))\n \t\t\t\t\tfree(rev.message_id);\n@@ -2475,7 +2542,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\t\t\t       mime_boundary_leader,\n \t\t\t\t       rev.mime_boundary);\n \t\t\telse\n-\t\t\t\tprint_signature(rev.diffopt.file);\n+\t\t\t\tprint_signature(signature, rev.diffopt.file);\n \t\t}\n \t\tif (output_directory)\n \t\t\tfclose(rev.diffopt.file);\n@@ -2483,9 +2550,6 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \tstop_progress(&progress);\n \tfree(list);\n \tfree(branch_name);\n-\tstring_list_clear(&extra_to, 0);\n-\tstring_list_clear(&extra_cc, 0);\n-\tstring_list_clear(&extra_hdr, 0);\n \tif (ignore_if_in_upstream)\n \t\tfree_patch_ids(&ids);\n \n@@ -2495,14 +2559,13 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \tstrbuf_release(&rdiff1);\n \tstrbuf_release(&rdiff2);\n \tstrbuf_release(&rdiff_title);\n-\tstrbuf_release(&sprefix);\n \tfree(to_free);\n \tfree(rev.message_id);\n \tif (rev.ref_message_ids)\n \t\tstring_list_clear(rev.ref_message_ids, 0);\n \tfree(rev.ref_message_ids);\n \trelease_revisions(&rev);\n-\tlog_config_release(&cfg);\n+\tformat_config_release(&cfg);\n \treturn 0;\n }\n \n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495394","messageId":"a857637e612e30d742689ffb95bee07184bff32f.1716465556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716465556.git.ps@pks.im","subject":"[PATCH 11/20] config: clarify memory ownership in `git_config_string()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-23T12:26:04Z","receivedAt":"2024-05-23T12:26:07Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The out parameter of `git_config_string()` is a `const char **` even\nthough we transfer ownership of memory to the caller. This is quite\nmisleading and has led to many memory leaks all over the place. Adapt\nthe parameter to instead be `char **`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n alias.c                |  2 +-\n attr.c                 |  2 +-\n attr.h                 |  2 +-\n builtin/commit.c       |  2 +-\n builtin/log.c          | 12 ++++++------\n builtin/merge.c        |  4 ++--\n builtin/rebase.c       |  2 +-\n builtin/receive-pack.c |  2 +-\n builtin/repack.c       |  8 ++++----\n config.c               |  4 ++--\n config.h               |  2 +-\n convert.c              |  6 +++---\n delta-islands.c        |  2 +-\n diff.c                 |  8 ++++----\n environment.c          |  8 ++++----\n environment.h          |  8 ++++----\n gpg-interface.c        |  4 ++--\n http.c                 | 24 ++++++++++++------------\n imap-send.c            | 12 ++++++------\n mailmap.c              |  2 +-\n mailmap.h              |  2 +-\n merge-ll.c             |  6 +++---\n pager.c                |  2 +-\n pretty.c               | 14 +++++++++-----\n promisor-remote.h      |  2 +-\n remote.c               | 20 ++++++++++----------\n remote.h               |  8 ++++----\n sequencer.c            |  2 +-\n upload-pack.c          |  2 +-\n userdiff.h             | 12 ++++++------\n 30 files changed, 95 insertions(+), 91 deletions(-)\n\ndiff --git a/alias.c b/alias.c\nindex 5a238f2e30..269892c356 100644\n--- a/alias.c\n+++ b/alias.c\n@@ -22,7 +22,7 @@ static int config_alias_cb(const char *key, const char *value,\n \n \tif (data->alias) {\n \t\tif (!strcasecmp(p, data->alias))\n-\t\t\treturn git_config_string((const char **)&data->v,\n+\t\t\treturn git_config_string(&data->v,\n \t\t\t\t\t\t key, value);\n \t} else if (data->list) {\n \t\tstring_list_append(data->list, p);\ndiff --git a/attr.c b/attr.c\nindex 33473bdce0..5607db2bbe 100644\n--- a/attr.c\n+++ b/attr.c\n@@ -25,7 +25,7 @@\n #include \"tree-walk.h\"\n #include \"object-name.h\"\n \n-const char *git_attr_tree;\n+char *git_attr_tree;\n \n const char git_attr__true[] = \"(builtin)true\";\n const char git_attr__false[] = \"\\0(builtin)false\";\ndiff --git a/attr.h b/attr.h\nindex 127998ae01..e7cc318b0c 100644\n--- a/attr.h\n+++ b/attr.h\n@@ -236,6 +236,6 @@ const char *git_attr_global_file(void);\n /* Return whether the system gitattributes file is enabled and should be used. */\n int git_attr_system_is_enabled(void);\n \n-extern const char *git_attr_tree;\n+extern char *git_attr_tree;\n \n #endif /* ATTR_H */\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 1cc88e92bf..f53e7e86ff 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -133,7 +133,7 @@ static struct strvec trailer_args = STRVEC_INIT;\n  * is specified explicitly.\n  */\n static enum commit_msg_cleanup_mode cleanup_mode;\n-static const char *cleanup_arg;\n+static char *cleanup_arg;\n \n static enum commit_whence whence;\n static int use_editor = 1, include_status = 1;\ndiff --git a/builtin/log.c b/builtin/log.c\nindex 890bf0c425..517d7982f1 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -582,11 +582,11 @@ static int git_log_config(const char *var, const char *value,\n \n \tif (!strcmp(var, \"format.pretty\")) {\n \t\tFREE_AND_NULL(cfg->fmt_pretty);\n-\t\treturn git_config_string((const char **) &cfg->fmt_pretty, var, value);\n+\t\treturn git_config_string(&cfg->fmt_pretty, var, value);\n \t}\n \tif (!strcmp(var, \"format.subjectprefix\")) {\n \t\tFREE_AND_NULL(cfg->fmt_patch_subject_prefix);\n-\t\treturn git_config_string((const char **) &cfg->fmt_patch_subject_prefix, var, value);\n+\t\treturn git_config_string(&cfg->fmt_patch_subject_prefix, var, value);\n \t}\n \tif (!strcmp(var, \"format.filenamemaxlength\")) {\n \t\tcfg->fmt_patch_name_max = git_config_int(var, value, ctx->kvi);\n@@ -602,7 +602,7 @@ static int git_log_config(const char *var, const char *value,\n \t}\n \tif (!strcmp(var, \"log.date\")) {\n \t\tFREE_AND_NULL(cfg->default_date_mode);\n-\t\treturn git_config_string((const char **) &cfg->default_date_mode, var, value);\n+\t\treturn git_config_string(&cfg->default_date_mode, var, value);\n \t}\n \tif (!strcmp(var, \"log.decorate\")) {\n \t\tcfg->decoration_style = parse_decoration_style(value);\n@@ -1076,7 +1076,7 @@ static int git_format_config(const char *var, const char *value,\n \t}\n \tif (!strcmp(var, \"format.suffix\")) {\n \t\tFREE_AND_NULL(cfg->fmt_patch_suffix);\n-\t\treturn git_config_string((const char **) &cfg->fmt_patch_suffix, var, value);\n+\t\treturn git_config_string(&cfg->fmt_patch_suffix, var, value);\n \t}\n \tif (!strcmp(var, \"format.to\")) {\n \t\tif (!value)\n@@ -1133,7 +1133,7 @@ static int git_format_config(const char *var, const char *value,\n \t}\n \tif (!strcmp(var, \"format.signature\")) {\n \t\tFREE_AND_NULL(cfg->signature);\n-\t\treturn git_config_string((const char **) &cfg->signature, var, value);\n+\t\treturn git_config_string(&cfg->signature, var, value);\n \t}\n \tif (!strcmp(var, \"format.signaturefile\")) {\n \t\tFREE_AND_NULL(cfg->signature_file);\n@@ -1149,7 +1149,7 @@ static int git_format_config(const char *var, const char *value,\n \t}\n \tif (!strcmp(var, \"format.outputdirectory\")) {\n \t\tFREE_AND_NULL(cfg->config_output_directory);\n-\t\treturn git_config_string((const char **) &cfg->config_output_directory, var, value);\n+\t\treturn git_config_string(&cfg->config_output_directory, var, value);\n \t}\n \tif (!strcmp(var, \"format.useautobase\")) {\n \t\tif (value && !strcasecmp(value, \"whenAble\")) {\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex e4bd65eeba..daed2d4e1e 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -100,7 +100,7 @@ static struct strategy all_strategy[] = {\n \t{ \"subtree\",    NO_FAST_FORWARD | NO_TRIVIAL },\n };\n \n-static const char *pull_twohead, *pull_octopus;\n+static char *pull_twohead, *pull_octopus;\n \n enum ff_type {\n \tFF_NO,\n@@ -110,7 +110,7 @@ enum ff_type {\n \n static enum ff_type fast_forward = FF_ALLOW;\n \n-static const char *cleanup_arg;\n+static char *cleanup_arg;\n static enum commit_msg_cleanup_mode cleanup_mode;\n \n static int option_parse_message(const struct option *opt,\ndiff --git a/builtin/rebase.c b/builtin/rebase.c\nindex 0466d9414a..14d4f0a5e6 100644\n--- a/builtin/rebase.c\n+++ b/builtin/rebase.c\n@@ -83,7 +83,7 @@ static const char *action_names[] = {\n struct rebase_options {\n \tenum rebase_type type;\n \tenum empty_type empty;\n-\tconst char *default_backend;\n+\tchar *default_backend;\n \tconst char *state_dir;\n \tstruct commit *upstream;\n \tconst char *upstream_name;\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 56228ad314..01c1f04ece 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -88,7 +88,7 @@ static struct strbuf push_cert = STRBUF_INIT;\n static struct object_id push_cert_oid;\n static struct signature_check sigcheck;\n static const char *push_cert_nonce;\n-static const char *cert_nonce_seed;\n+static char *cert_nonce_seed;\n static struct strvec hidden_refs = STRVEC_INIT;\n \n static const char *NONCE_UNSOLICITED = \"UNSOLICITED\";\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex 43491a4cbf..e40dceaada 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -48,10 +48,10 @@ static const char incremental_bitmap_conflict_error[] = N_(\n );\n \n struct pack_objects_args {\n-\tconst char *window;\n-\tconst char *window_memory;\n-\tconst char *depth;\n-\tconst char *threads;\n+\tchar *window;\n+\tchar *window_memory;\n+\tchar *depth;\n+\tchar *threads;\n \tunsigned long max_pack_size;\n \tint no_reuse_delta;\n \tint no_reuse_object;\ndiff --git a/config.c b/config.c\nindex f9101045ee..a025cfafe0 100644\n--- a/config.c\n+++ b/config.c\n@@ -1338,7 +1338,7 @@ int git_config_bool(const char *name, const char *value)\n \treturn v;\n }\n \n-int git_config_string(const char **dest, const char *var, const char *value)\n+int git_config_string(char **dest, const char *var, const char *value)\n {\n \tif (!value)\n \t\treturn config_error_nonbool(var);\n@@ -2418,7 +2418,7 @@ int git_configset_get_string(struct config_set *set, const char *key, char **des\n {\n \tconst char *value;\n \tif (!git_configset_get_value(set, key, &value, NULL))\n-\t\treturn git_config_string((const char **)dest, key, value);\n+\t\treturn git_config_string(dest, key, value);\n \telse\n \t\treturn 1;\n }\ndiff --git a/config.h b/config.h\nindex b3103bba94..2d2e22ed64 100644\n--- a/config.h\n+++ b/config.h\n@@ -280,7 +280,7 @@ int git_config_bool(const char *, const char *);\n  * Allocates and copies the value string into the `dest` parameter; if no\n  * string is given, prints an error message and returns -1.\n  */\n-int git_config_string(const char **, const char *, const char *);\n+int git_config_string(char **, const char *, const char *);\n \n /**\n  * Similar to `git_config_string`, but expands `~` or `~user` into the\ndiff --git a/convert.c b/convert.c\nindex 03c3c528f9..f2b9f01354 100644\n--- a/convert.c\n+++ b/convert.c\n@@ -981,9 +981,9 @@ int async_query_available_blobs(const char *cmd, struct string_list *available_p\n static struct convert_driver {\n \tconst char *name;\n \tstruct convert_driver *next;\n-\tconst char *smudge;\n-\tconst char *clean;\n-\tconst char *process;\n+\tchar *smudge;\n+\tchar *clean;\n+\tchar *process;\n \tint required;\n } *user_convert, **user_convert_tail;\n \ndiff --git a/delta-islands.c b/delta-islands.c\nindex 4ac3c10551..89d51b72e3 100644\n--- a/delta-islands.c\n+++ b/delta-islands.c\n@@ -313,7 +313,7 @@ struct island_load_data {\n \tsize_t nr;\n \tsize_t alloc;\n };\n-static const char *core_island_name;\n+static char *core_island_name;\n \n static void free_config_regexes(struct island_load_data *ild)\n {\ndiff --git a/diff.c b/diff.c\nindex 679ef472f4..e70301df76 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -56,8 +56,8 @@ static int diff_color_moved_default;\n static int diff_color_moved_ws_default;\n static int diff_context_default = 3;\n static int diff_interhunk_context_default;\n-static const char *diff_word_regex_cfg;\n-static const char *external_diff_cmd_cfg;\n+static char *diff_word_regex_cfg;\n+static char *external_diff_cmd_cfg;\n static char *diff_order_file_cfg;\n int diff_auto_refresh_index = 1;\n static int diff_mnemonic_prefix;\n@@ -412,11 +412,11 @@ int git_diff_ui_config(const char *var, const char *value,\n \t}\n \tif (!strcmp(var, \"diff.srcprefix\")) {\n \t\tFREE_AND_NULL(diff_src_prefix);\n-\t\treturn git_config_string((const char **) &diff_src_prefix, var, value);\n+\t\treturn git_config_string(&diff_src_prefix, var, value);\n \t}\n \tif (!strcmp(var, \"diff.dstprefix\")) {\n \t\tFREE_AND_NULL(diff_dst_prefix);\n-\t\treturn git_config_string((const char **) &diff_dst_prefix, var, value);\n+\t\treturn git_config_string(&diff_dst_prefix, var, value);\n \t}\n \tif (!strcmp(var, \"diff.relative\")) {\n \t\tdiff_relative = git_config_bool(var, value);\ndiff --git a/environment.c b/environment.c\nindex ab6956559e..701d515135 100644\n--- a/environment.c\n+++ b/environment.c\n@@ -42,8 +42,8 @@ int is_bare_repository_cfg = -1; /* unspecified */\n int warn_ambiguous_refs = 1;\n int warn_on_object_refname_ambiguity = 1;\n int repository_format_precious_objects;\n-const char *git_commit_encoding;\n-const char *git_log_output_encoding;\n+char *git_commit_encoding;\n+char *git_log_output_encoding;\n char *apply_default_whitespace;\n char *apply_default_ignorewhitespace;\n char *git_attributes_file;\n@@ -58,8 +58,8 @@ size_t packed_git_window_size = DEFAULT_PACKED_GIT_WINDOW_SIZE;\n size_t packed_git_limit = DEFAULT_PACKED_GIT_LIMIT;\n size_t delta_base_cache_limit = 96 * 1024 * 1024;\n unsigned long big_file_threshold = 512 * 1024 * 1024;\n-const char *editor_program;\n-const char *askpass_program;\n+char *editor_program;\n+char *askpass_program;\n char *excludes_file;\n enum auto_crlf auto_crlf = AUTO_CRLF_FALSE;\n enum eol core_eol = EOL_UNSET;\ndiff --git a/environment.h b/environment.h\nindex be1b88ad6f..e9f01d4d11 100644\n--- a/environment.h\n+++ b/environment.h\n@@ -224,11 +224,11 @@ int odb_pack_keep(const char *name);\n const char *get_log_output_encoding(void);\n const char *get_commit_output_encoding(void);\n \n-extern const char *git_commit_encoding;\n-extern const char *git_log_output_encoding;\n+extern char *git_commit_encoding;\n+extern char *git_log_output_encoding;\n \n-extern const char *editor_program;\n-extern const char *askpass_program;\n+extern char *editor_program;\n+extern char *askpass_program;\n extern char *excludes_file;\n \n /*\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 2b50ed0fa0..5193223714 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -27,14 +27,14 @@ static void gpg_interface_lazy_init(void)\n }\n \n static char *configured_signing_key;\n-static const char *ssh_default_key_command;\n+static char *ssh_default_key_command;\n static char *ssh_allowed_signers;\n static char *ssh_revocation_file;\n static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;\n \n struct gpg_format {\n \tconst char *name;\n-\tconst char *program;\n+\tchar *program;\n \tconst char **verify_args;\n \tconst char **sigs;\n \tint (*verify_signed_buffer)(struct signature_check *sigc,\ndiff --git a/http.c b/http.c\nindex fa3ea87451..67cc47d28f 100644\n--- a/http.c\n+++ b/http.c\n@@ -38,11 +38,11 @@ char curl_errorstr[CURL_ERROR_SIZE];\n \n static int curl_ssl_verify = -1;\n static int curl_ssl_try;\n-static const char *curl_http_version = NULL;\n+static char *curl_http_version;\n static char *ssl_cert;\n static char *ssl_cert_type;\n-static const char *ssl_cipherlist;\n-static const char *ssl_version;\n+static char *ssl_cipherlist;\n+static char *ssl_version;\n static struct {\n \tconst char *name;\n \tlong ssl_version;\n@@ -95,7 +95,7 @@ static struct {\n \t */\n };\n #ifdef CURLGSSAPI_DELEGATION_FLAG\n-static const char *curl_deleg;\n+static char *curl_deleg;\n static struct {\n \tconst char *name;\n \tlong curl_deleg_param;\n@@ -383,11 +383,11 @@ static int http_options(const char *var, const char *value,\n \tif (!strcmp(\"http.sslcert\", var))\n \t\treturn git_config_pathname(&ssl_cert, var, value);\n \tif (!strcmp(\"http.sslcerttype\", var))\n-\t\treturn git_config_string((const char **)&ssl_cert_type, var, value);\n+\t\treturn git_config_string(&ssl_cert_type, var, value);\n \tif (!strcmp(\"http.sslkey\", var))\n \t\treturn git_config_pathname(&ssl_key, var, value);\n \tif (!strcmp(\"http.sslkeytype\", var))\n-\t\treturn git_config_string((const char **)&ssl_key_type, var, value);\n+\t\treturn git_config_string(&ssl_key_type, var, value);\n \tif (!strcmp(\"http.sslcapath\", var))\n \t\treturn git_config_pathname(&ssl_capath, var, value);\n \tif (!strcmp(\"http.sslcainfo\", var))\n@@ -440,19 +440,19 @@ static int http_options(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \tif (!strcmp(\"http.proxy\", var))\n-\t\treturn git_config_string((const char **)&curl_http_proxy, var, value);\n+\t\treturn git_config_string(&curl_http_proxy, var, value);\n \n \tif (!strcmp(\"http.proxyauthmethod\", var))\n-\t\treturn git_config_string((const char **)&http_proxy_authmethod, var, value);\n+\t\treturn git_config_string(&http_proxy_authmethod, var, value);\n \n \tif (!strcmp(\"http.proxysslcert\", var))\n-\t\treturn git_config_string((const char **)&http_proxy_ssl_cert, var, value);\n+\t\treturn git_config_string(&http_proxy_ssl_cert, var, value);\n \n \tif (!strcmp(\"http.proxysslkey\", var))\n-\t\treturn git_config_string((const char **)&http_proxy_ssl_key, var, value);\n+\t\treturn git_config_string(&http_proxy_ssl_key, var, value);\n \n \tif (!strcmp(\"http.proxysslcainfo\", var))\n-\t\treturn git_config_string((const char **)&http_proxy_ssl_ca_info, var, value);\n+\t\treturn git_config_string(&http_proxy_ssl_ca_info, var, value);\n \n \tif (!strcmp(\"http.proxysslcertpasswordprotected\", var)) {\n \t\tproxy_ssl_cert_password_required = git_config_bool(var, value);\n@@ -476,7 +476,7 @@ static int http_options(const char *var, const char *value,\n \t}\n \n \tif (!strcmp(\"http.useragent\", var))\n-\t\treturn git_config_string((const char **)&user_agent, var, value);\n+\t\treturn git_config_string(&user_agent, var, value);\n \n \tif (!strcmp(\"http.emptyauth\", var)) {\n \t\tif (value && !strcmp(\"auto\", value))\ndiff --git a/imap-send.c b/imap-send.c\nindex c0130d0e02..a5d1510180 100644\n--- a/imap-send.c\n+++ b/imap-send.c\n@@ -70,16 +70,16 @@ static char *next_arg(char **);\n \n struct imap_server_conf {\n \tconst char *name;\n-\tconst char *tunnel;\n-\tconst char *host;\n+\tchar *tunnel;\n+\tchar *host;\n \tint port;\n-\tconst char *folder;\n-\tconst char *user;\n-\tconst char *pass;\n+\tchar *folder;\n+\tchar *user;\n+\tchar *pass;\n \tint use_ssl;\n \tint ssl_verify;\n \tint use_html;\n-\tconst char *auth_method;\n+\tchar *auth_method;\n };\n \n static struct imap_server_conf server = {\ndiff --git a/mailmap.c b/mailmap.c\nindex 044466b043..b2efe29b3d 100644\n--- a/mailmap.c\n+++ b/mailmap.c\n@@ -7,7 +7,7 @@\n #include \"setup.h\"\n \n char *git_mailmap_file;\n-const char *git_mailmap_blob;\n+char *git_mailmap_blob;\n \n struct mailmap_info {\n \tchar *name;\ndiff --git a/mailmap.h b/mailmap.h\nindex 429a760945..cbda9bc5e0 100644\n--- a/mailmap.h\n+++ b/mailmap.h\n@@ -4,7 +4,7 @@\n struct string_list;\n \n extern char *git_mailmap_file;\n-extern const char *git_mailmap_blob;\n+extern char *git_mailmap_blob;\n \n int read_mailmap(struct string_list *map);\n void clear_mailmap(struct string_list *map);\ndiff --git a/merge-ll.c b/merge-ll.c\nindex bf1077ae09..e29b15fa4a 100644\n--- a/merge-ll.c\n+++ b/merge-ll.c\n@@ -27,9 +27,9 @@ typedef enum ll_merge_result (*ll_merge_fn)(const struct ll_merge_driver *,\n \n struct ll_merge_driver {\n \tconst char *name;\n-\tconst char *description;\n+\tchar *description;\n \tll_merge_fn fn;\n-\tconst char *recursive;\n+\tchar *recursive;\n \tstruct ll_merge_driver *next;\n \tchar *cmdline;\n };\n@@ -268,7 +268,7 @@ static enum ll_merge_result ll_ext_merge(const struct ll_merge_driver *fn,\n  * merge.default and merge.driver configuration items\n  */\n static struct ll_merge_driver *ll_user_merge, **ll_user_merge_tail;\n-static const char *default_ll_merge;\n+static char *default_ll_merge;\n \n static int read_merge_config(const char *var, const char *value,\n \t\t\t     const struct config_context *ctx UNUSED,\ndiff --git a/pager.c b/pager.c\nindex b8822a9381..e9e121db69 100644\n--- a/pager.c\n+++ b/pager.c\n@@ -13,7 +13,7 @@ int pager_use_color = 1;\n #endif\n \n static struct child_process pager_process;\n-static const char *pager_program;\n+static char *pager_program;\n \n /* Is the value coming back from term_columns() just a guess? */\n static int term_columns_guessed;\ndiff --git a/pretty.c b/pretty.c\nindex 7ead078998..22a81506b7 100644\n--- a/pretty.c\n+++ b/pretty.c\n@@ -62,7 +62,7 @@ static int git_pretty_formats_config(const char *var, const char *value,\n {\n \tstruct cmt_fmt_map *commit_format = NULL;\n \tconst char *name;\n-\tconst char *fmt;\n+\tchar *fmt;\n \tint i;\n \n \tif (!skip_prefix(var, \"pretty.\", &name))\n@@ -93,13 +93,17 @@ static int git_pretty_formats_config(const char *var, const char *value,\n \tif (git_config_string(&fmt, var, value))\n \t\treturn -1;\n \n-\tif (skip_prefix(fmt, \"format:\", &fmt))\n+\tif (skip_prefix(fmt, \"format:\", &commit_format->user_format)) {\n \t\tcommit_format->is_tformat = 0;\n-\telse if (skip_prefix(fmt, \"tformat:\", &fmt) || strchr(fmt, '%'))\n+\t} else if (skip_prefix(fmt, \"tformat:\", &commit_format->user_format)) {\n \t\tcommit_format->is_tformat = 1;\n-\telse\n+\t} else if (strchr(fmt, '%')) {\n+\t\tcommit_format->is_tformat = 1;\n+\t\tcommit_format->user_format = fmt;\n+\t} else {\n \t\tcommit_format->is_alias = 1;\n-\tcommit_format->user_format = fmt;\n+\t\tcommit_format->user_format = fmt;\n+\t}\n \n \treturn 0;\n }\ndiff --git a/promisor-remote.h b/promisor-remote.h\nindex 2cb9eda9ea..88cb599c39 100644\n--- a/promisor-remote.h\n+++ b/promisor-remote.h\n@@ -13,7 +13,7 @@ struct object_id;\n  */\n struct promisor_remote {\n \tstruct promisor_remote *next;\n-\tconst char *partial_clone_filter;\n+\tchar *partial_clone_filter;\n \tconst char name[FLEX_ARRAY];\n };\n \ndiff --git a/remote.c b/remote.c\nindex ec8c158e60..d319f28757 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -428,29 +428,29 @@ static int handle_config(const char *key, const char *value,\n \telse if (!strcmp(subkey, \"prunetags\"))\n \t\tremote->prune_tags = git_config_bool(key, value);\n \telse if (!strcmp(subkey, \"url\")) {\n-\t\tconst char *v;\n+\t\tchar *v;\n \t\tif (git_config_string(&v, key, value))\n \t\t\treturn -1;\n \t\tadd_url(remote, v);\n \t} else if (!strcmp(subkey, \"pushurl\")) {\n-\t\tconst char *v;\n+\t\tchar *v;\n \t\tif (git_config_string(&v, key, value))\n \t\t\treturn -1;\n \t\tadd_pushurl(remote, v);\n \t} else if (!strcmp(subkey, \"push\")) {\n-\t\tconst char *v;\n+\t\tchar *v;\n \t\tif (git_config_string(&v, key, value))\n \t\t\treturn -1;\n \t\trefspec_append(&remote->push, v);\n-\t\tfree((char *)v);\n+\t\tfree(v);\n \t} else if (!strcmp(subkey, \"fetch\")) {\n-\t\tconst char *v;\n+\t\tchar *v;\n \t\tif (git_config_string(&v, key, value))\n \t\t\treturn -1;\n \t\trefspec_append(&remote->fetch, v);\n-\t\tfree((char *)v);\n+\t\tfree(v);\n \t} else if (!strcmp(subkey, \"receivepack\")) {\n-\t\tconst char *v;\n+\t\tchar *v;\n \t\tif (git_config_string(&v, key, value))\n \t\t\treturn -1;\n \t\tif (!remote->receivepack)\n@@ -458,7 +458,7 @@ static int handle_config(const char *key, const char *value,\n \t\telse\n \t\t\terror(_(\"more than one receivepack given, using the first\"));\n \t} else if (!strcmp(subkey, \"uploadpack\")) {\n-\t\tconst char *v;\n+\t\tchar *v;\n \t\tif (git_config_string(&v, key, value))\n \t\t\treturn -1;\n \t\tif (!remote->uploadpack)\n@@ -471,10 +471,10 @@ static int handle_config(const char *key, const char *value,\n \t\telse if (!strcmp(value, \"--tags\"))\n \t\t\tremote->fetch_tags = 2;\n \t} else if (!strcmp(subkey, \"proxy\")) {\n-\t\treturn git_config_string((const char **)&remote->http_proxy,\n+\t\treturn git_config_string(&remote->http_proxy,\n \t\t\t\t\t key, value);\n \t} else if (!strcmp(subkey, \"proxyauthmethod\")) {\n-\t\treturn git_config_string((const char **)&remote->http_proxy_authmethod,\n+\t\treturn git_config_string(&remote->http_proxy_authmethod,\n \t\t\t\t\t key, value);\n \t} else if (!strcmp(subkey, \"vcs\")) {\n \t\treturn git_config_string(&remote->foreign_vcs, key, value);\ndiff --git a/remote.h b/remote.h\nindex dfd4837e60..e8c6655e42 100644\n--- a/remote.h\n+++ b/remote.h\n@@ -46,7 +46,7 @@ struct remote_state {\n \tstruct hashmap branches_hash;\n \n \tstruct branch *current_branch;\n-\tconst char *pushremote_name;\n+\tchar *pushremote_name;\n \n \tstruct rewrites rewrites;\n \tstruct rewrites rewrites_push;\n@@ -65,7 +65,7 @@ struct remote {\n \n \tint origin, configured_in_repo;\n \n-\tconst char *foreign_vcs;\n+\tchar *foreign_vcs;\n \n \t/* An array of all of the url_nr URLs configured for the remote */\n \tconst char **url;\n@@ -309,9 +309,9 @@ struct branch {\n \tconst char *refname;\n \n \t/* The name of the remote listed in the configuration. */\n-\tconst char *remote_name;\n+\tchar *remote_name;\n \n-\tconst char *pushremote_name;\n+\tchar *pushremote_name;\n \n \t/* An array of the \"merge\" lines in the configuration. */\n \tconst char **merge_name;\ndiff --git a/sequencer.c b/sequencer.c\nindex dbd60d79b9..3c81ef9ca5 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -306,7 +306,7 @@ static int git_sequencer_config(const char *k, const char *v,\n \t}\n \n \tif (!opts->default_strategy && !strcmp(k, \"pull.twohead\")) {\n-\t\tint ret = git_config_string((const char**)&opts->default_strategy, k, v);\n+\t\tint ret = git_config_string(&opts->default_strategy, k, v);\n \t\tif (ret == 0) {\n \t\t\t/*\n \t\t\t * pull.twohead is allowed to be multi-valued; we only\ndiff --git a/upload-pack.c b/upload-pack.c\nindex 8fbd138515..5801eb2639 100644\n--- a/upload-pack.c\n+++ b/upload-pack.c\n@@ -94,7 +94,7 @@ struct upload_pack_data {\n \n \tstruct packet_writer writer;\n \n-\tconst char *pack_objects_hook;\n+\tchar *pack_objects_hook;\n \n \tunsigned stateless_rpc : 1;\t\t\t\t/* v0 only */\n \tunsigned no_done : 1;\t\t\t\t\t/* v0 only */\ndiff --git a/userdiff.h b/userdiff.h\nindex d726804c3e..cc8e5abfef 100644\n--- a/userdiff.h\n+++ b/userdiff.h\n@@ -7,19 +7,19 @@ struct index_state;\n struct repository;\n \n struct userdiff_funcname {\n-\tconst char *pattern;\n+\tchar *pattern;\n \tint cflags;\n };\n \n struct userdiff_driver {\n \tconst char *name;\n-\tconst char *external;\n-\tconst char *algorithm;\n+\tchar *external;\n+\tchar *algorithm;\n \tint binary;\n \tstruct userdiff_funcname funcname;\n-\tconst char *word_regex;\n-\tconst char *word_regex_multi_byte;\n-\tconst char *textconv;\n+\tchar *word_regex;\n+\tchar *word_regex_multi_byte;\n+\tchar *textconv;\n \tstruct notes_cache *textconv_cache;\n \tint textconv_want_cache;\n };\n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495395","messageId":"b2f8878b55564c4bf4848642e419c44c1a9d9005.1716465556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716465556.git.ps@pks.im","subject":"[PATCH 12/20] config: plug various memory leaks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-23T12:26:09Z","receivedAt":"2024-05-23T12:26:13Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Now that memory ownership rules around `git_config_string()` and\n`git_config_pathname()` are clearer, it also got easier to spot that\nthe returned memory needs to be free'd. Plug a subset of those cases and\nmark now-passing tests as leak free.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n alias.c                                      |  4 ++-\n config.c                                     | 38 ++++++++++++++------\n t/t1306-xdg-files.sh                         |  1 +\n t/t1350-config-hooks-path.sh                 |  1 +\n t/t3415-rebase-autosquash.sh                 |  1 +\n t/t4041-diff-submodule-option.sh             |  1 +\n t/t4060-diff-submodule-option-diff-format.sh |  1 +\n t/t4210-log-i18n.sh                          |  2 ++\n t/t6006-rev-list-format.sh                   |  1 +\n t/t7005-editor.sh                            |  1 +\n t/t7102-reset.sh                             |  1 +\n t/t9129-git-svn-i18n-commitencoding.sh       |  1 -\n t/t9139-git-svn-non-utf8-commitencoding.sh   |  1 -\n 13 files changed, 41 insertions(+), 13 deletions(-)\n\ndiff --git a/alias.c b/alias.c\nindex 269892c356..4daafd9bda 100644\n--- a/alias.c\n+++ b/alias.c\n@@ -21,9 +21,11 @@ static int config_alias_cb(const char *key, const char *value,\n \t\treturn 0;\n \n \tif (data->alias) {\n-\t\tif (!strcasecmp(p, data->alias))\n+\t\tif (!strcasecmp(p, data->alias)) {\n+\t\t\tFREE_AND_NULL(data->v);\n \t\t\treturn git_config_string(&data->v,\n \t\t\t\t\t\t key, value);\n+\t\t}\n \t} else if (data->list) {\n \t\tstring_list_append(data->list, p);\n \t}\ndiff --git a/config.c b/config.c\nindex a025cfafe0..496cd1a61e 100644\n--- a/config.c\n+++ b/config.c\n@@ -1414,8 +1414,10 @@ static int git_default_core_config(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \n-\tif (!strcmp(var, \"core.attributesfile\"))\n+\tif (!strcmp(var, \"core.attributesfile\")) {\n+\t\tFREE_AND_NULL(git_attributes_file);\n \t\treturn git_config_pathname(&git_attributes_file, var, value);\n+\t}\n \n \tif (!strcmp(var, \"core.hookspath\")) {\n \t\tif (ctx->kvi && ctx->kvi->scope == CONFIG_SCOPE_LOCAL &&\n@@ -1428,6 +1430,7 @@ static int git_default_core_config(const char *var, const char *value,\n \t\t\t      \"again with \"\n \t\t\t      \"`GIT_CLONE_PROTECTION_ACTIVE=false`\"),\n \t\t\t    value);\n+\t\tFREE_AND_NULL(git_hooks_path);\n \t\treturn git_config_pathname(&git_hooks_path, var, value);\n \t}\n \n@@ -1566,7 +1569,7 @@ static int git_default_core_config(const char *var, const char *value,\n \n \tif (!strcmp(var, \"core.checkroundtripencoding\")) {\n \t\tFREE_AND_NULL(check_roundtrip_encoding);\n-\t\treturn git_config_string((const char **) &check_roundtrip_encoding, var, value);\n+\t\treturn git_config_string(&check_roundtrip_encoding, var, value);\n \t}\n \n \tif (!strcmp(var, \"core.notesref\")) {\n@@ -1576,8 +1579,10 @@ static int git_default_core_config(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \n-\tif (!strcmp(var, \"core.editor\"))\n+\tif (!strcmp(var, \"core.editor\")) {\n+\t\tFREE_AND_NULL(editor_program);\n \t\treturn git_config_string(&editor_program, var, value);\n+\t}\n \n \tif (!strcmp(var, \"core.commentchar\") ||\n \t    !strcmp(var, \"core.commentstring\")) {\n@@ -1595,11 +1600,13 @@ static int git_default_core_config(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \n-\tif (!strcmp(var, \"core.askpass\"))\n+\tif (!strcmp(var, \"core.askpass\")) {\n+\t\tFREE_AND_NULL(askpass_program);\n \t\treturn git_config_string(&askpass_program, var, value);\n+\t}\n \n \tif (!strcmp(var, \"core.excludesfile\")) {\n-\t\tfree(excludes_file);\n+\t\tFREE_AND_NULL(excludes_file);\n \t\treturn git_config_pathname(&excludes_file, var, value);\n \t}\n \n@@ -1702,11 +1709,15 @@ static int git_default_sparse_config(const char *var, const char *value)\n \n static int git_default_i18n_config(const char *var, const char *value)\n {\n-\tif (!strcmp(var, \"i18n.commitencoding\"))\n+\tif (!strcmp(var, \"i18n.commitencoding\")) {\n+\t\tFREE_AND_NULL(git_commit_encoding);\n \t\treturn git_config_string(&git_commit_encoding, var, value);\n+\t}\n \n-\tif (!strcmp(var, \"i18n.logoutputencoding\"))\n+\tif (!strcmp(var, \"i18n.logoutputencoding\")) {\n+\t\tFREE_AND_NULL(git_log_output_encoding);\n \t\treturn git_config_string(&git_log_output_encoding, var, value);\n+\t}\n \n \t/* Add other config variables here and to Documentation/config.txt. */\n \treturn 0;\n@@ -1779,10 +1790,15 @@ static int git_default_push_config(const char *var, const char *value)\n \n static int git_default_mailmap_config(const char *var, const char *value)\n {\n-\tif (!strcmp(var, \"mailmap.file\"))\n+\tif (!strcmp(var, \"mailmap.file\")) {\n+\t\tFREE_AND_NULL(git_mailmap_file);\n \t\treturn git_config_pathname(&git_mailmap_file, var, value);\n-\tif (!strcmp(var, \"mailmap.blob\"))\n+\t}\n+\n+\tif (!strcmp(var, \"mailmap.blob\")) {\n+\t\tFREE_AND_NULL(git_mailmap_blob);\n \t\treturn git_config_string(&git_mailmap_blob, var, value);\n+\t}\n \n \t/* Add other config variables here and to Documentation/config.txt. */\n \treturn 0;\n@@ -1790,8 +1806,10 @@ static int git_default_mailmap_config(const char *var, const char *value)\n \n static int git_default_attr_config(const char *var, const char *value)\n {\n-\tif (!strcmp(var, \"attr.tree\"))\n+\tif (!strcmp(var, \"attr.tree\")) {\n+\t\tFREE_AND_NULL(git_attr_tree);\n \t\treturn git_config_string(&git_attr_tree, var, value);\n+\t}\n \n \t/*\n \t * Add other attribute related config variables here and to\ndiff --git a/t/t1306-xdg-files.sh b/t/t1306-xdg-files.sh\nindex 40d3c42618..53e5b290b9 100755\n--- a/t/t1306-xdg-files.sh\n+++ b/t/t1306-xdg-files.sh\n@@ -7,6 +7,7 @@\n \n test_description='Compatibility with $XDG_CONFIG_HOME/git/ files'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n test_expect_success 'read config: xdg file exists and ~/.gitconfig doesn'\\''t' '\ndiff --git a/t/t1350-config-hooks-path.sh b/t/t1350-config-hooks-path.sh\nindex f6dc83e2aa..5c47f9ecc3 100755\n--- a/t/t1350-config-hooks-path.sh\n+++ b/t/t1350-config-hooks-path.sh\n@@ -2,6 +2,7 @@\n \n test_description='Test the core.hooksPath configuration variable'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n test_expect_success 'set up a pre-commit hook in core.hooksPath' '\ndiff --git a/t/t3415-rebase-autosquash.sh b/t/t3415-rebase-autosquash.sh\nindex fcc40d6fe1..22452ff84c 100755\n--- a/t/t3415-rebase-autosquash.sh\n+++ b/t/t3415-rebase-autosquash.sh\n@@ -5,6 +5,7 @@ test_description='auto squash'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n . \"$TEST_DIRECTORY\"/lib-rebase.sh\ndiff --git a/t/t4041-diff-submodule-option.sh b/t/t4041-diff-submodule-option.sh\nindex 0c1502d4b0..8fc40e75eb 100755\n--- a/t/t4041-diff-submodule-option.sh\n+++ b/t/t4041-diff-submodule-option.sh\n@@ -12,6 +12,7 @@ This test tries to verify the sanity of the --submodule option of git diff.\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n # Tested non-UTF-8 encoding\ndiff --git a/t/t4060-diff-submodule-option-diff-format.sh b/t/t4060-diff-submodule-option-diff-format.sh\nindex 97c6424cd5..8ce67442d9 100755\n--- a/t/t4060-diff-submodule-option-diff-format.sh\n+++ b/t/t4060-diff-submodule-option-diff-format.sh\n@@ -10,6 +10,7 @@ test_description='Support for diff format verbose submodule difference in git di\n This test tries to verify the sanity of --submodule=diff option of git diff.\n '\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n # Tested non-UTF-8 encoding\ndiff --git a/t/t4210-log-i18n.sh b/t/t4210-log-i18n.sh\nindex 75216f19ce..7120030b5c 100755\n--- a/t/t4210-log-i18n.sh\n+++ b/t/t4210-log-i18n.sh\n@@ -1,6 +1,8 @@\n #!/bin/sh\n \n test_description='test log with i18n features'\n+\n+TEST_PASSES_SANITIZE_LEAK=true\n . ./lib-gettext.sh\n \n # two forms of é\ndiff --git a/t/t6006-rev-list-format.sh b/t/t6006-rev-list-format.sh\nindex 573eb97a0f..f1623b1c06 100755\n--- a/t/t6006-rev-list-format.sh\n+++ b/t/t6006-rev-list-format.sh\n@@ -8,6 +8,7 @@ test_description='git rev-list --pretty=format test'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-terminal.sh\n \ndiff --git a/t/t7005-editor.sh b/t/t7005-editor.sh\nindex 5fcf281dfb..b9822294fe 100755\n--- a/t/t7005-editor.sh\n+++ b/t/t7005-editor.sh\n@@ -2,6 +2,7 @@\n \n test_description='GIT_EDITOR, core.editor, and stuff'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n unset EDITOR VISUAL GIT_EDITOR\ndiff --git a/t/t7102-reset.sh b/t/t7102-reset.sh\nindex 62d9f846ce..2add26d768 100755\n--- a/t/t7102-reset.sh\n+++ b/t/t7102-reset.sh\n@@ -10,6 +10,7 @@ Documented tests for git reset'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n commit_msg () {\ndiff --git a/t/t9129-git-svn-i18n-commitencoding.sh b/t/t9129-git-svn-i18n-commitencoding.sh\nindex 185248a4cd..01e1e8a8f7 100755\n--- a/t/t9129-git-svn-i18n-commitencoding.sh\n+++ b/t/t9129-git-svn-i18n-commitencoding.sh\n@@ -4,7 +4,6 @@\n \n test_description='git svn honors i18n.commitEncoding in config'\n \n-TEST_FAILS_SANITIZE_LEAK=true\n . ./lib-git-svn.sh\n \n compare_git_head_with () {\ndiff --git a/t/t9139-git-svn-non-utf8-commitencoding.sh b/t/t9139-git-svn-non-utf8-commitencoding.sh\nindex b7f756b2b7..22d80b0be2 100755\n--- a/t/t9139-git-svn-non-utf8-commitencoding.sh\n+++ b/t/t9139-git-svn-non-utf8-commitencoding.sh\n@@ -4,7 +4,6 @@\n \n test_description='git svn refuses to dcommit non-UTF8 messages'\n \n-TEST_FAILS_SANITIZE_LEAK=true\n . ./lib-git-svn.sh\n \n # ISO-2022-JP can pass for valid UTF-8, so skipping that in this test\n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495396","messageId":"23e2cf98b7865f134cb60acc27b750184d3f86fe.1716465556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716465556.git.ps@pks.im","subject":"[PATCH 13/20] builtin/credential: clear credential before exit","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-23T12:26:13Z","receivedAt":"2024-05-23T12:26:17Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We never release memory associated with `struct credential`. Fix this\nand mark the corresponding test as leak free.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/credential.c   | 2 ++\n t/t0300-credentials.sh | 2 ++\n 2 files changed, 4 insertions(+)\n\ndiff --git a/builtin/credential.c b/builtin/credential.c\nindex 5100d441f2..b72e76dd9a 100644\n--- a/builtin/credential.c\n+++ b/builtin/credential.c\n@@ -39,5 +39,7 @@ int cmd_credential(int argc, const char **argv, const char *prefix UNUSED)\n \t} else {\n \t\tusage(usage_msg);\n \t}\n+\n+\tcredential_clear(&c);\n \treturn 0;\n }\ndiff --git a/t/t0300-credentials.sh b/t/t0300-credentials.sh\nindex 432f029d48..6a76b7fdbd 100755\n--- a/t/t0300-credentials.sh\n+++ b/t/t0300-credentials.sh\n@@ -1,6 +1,8 @@\n #!/bin/sh\n \n test_description='basic credential helper tests'\n+\n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-credential.sh\n \n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495397","messageId":"a11ce6a0edc5eb1ac29b4ccc3e775ac3501c4ce5.1716465556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716465556.git.ps@pks.im","subject":"[PATCH 14/20] commit-reach: fix memory leak in `ahead_behind()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-23T12:26:18Z","receivedAt":"2024-05-23T12:26:22Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We use a priority queue in `ahead_behind()` to compute the ahead/behind\ncount for commits. We may not iterate through all commits part of that\nqueue though in case all of its entries are stale. Consequently, as we\nnever make the effort to release the remaining commits, we end up\nleaking bit arrays that we have allocated for each of the contained\ncommits.\n\nPlug this leak and mark the corresponding test as leak free.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n commit-reach.c           | 4 ++++\n t/t3203-branch-output.sh | 2 ++\n 2 files changed, 6 insertions(+)\n\ndiff --git a/commit-reach.c b/commit-reach.c\nindex 8f9b008f87..384aee1ab3 100644\n--- a/commit-reach.c\n+++ b/commit-reach.c\n@@ -1106,6 +1106,10 @@ void ahead_behind(struct repository *r,\n \n \t/* STALE is used here, PARENT2 is used by insert_no_dup(). */\n \trepo_clear_commit_marks(r, PARENT2 | STALE);\n+\twhile (prio_queue_peek(&queue)) {\n+\t\tstruct commit *c = prio_queue_get(&queue);\n+\t\tfree_bit_array(c);\n+\t}\n \tclear_bit_arrays(&bit_arrays);\n \tclear_prio_queue(&queue);\n }\ndiff --git a/t/t3203-branch-output.sh b/t/t3203-branch-output.sh\nindex 758963b189..e627f08a17 100755\n--- a/t/t3203-branch-output.sh\n+++ b/t/t3203-branch-output.sh\n@@ -1,6 +1,8 @@\n #!/bin/sh\n \n test_description='git branch display tests'\n+\n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-terminal.sh\n \n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495398","messageId":"24362604b2390d69a58c2064d9da0fe248070d46.1716465556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716465556.git.ps@pks.im","subject":"[PATCH 15/20] submodule: fix leaking memory for submodule entries","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-23T12:26:23Z","receivedAt":"2024-05-23T12:26:27Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"In `free_one_config()` we never end up freeing the `url` and `ignore`\nfields and thus leak memory. Fix those leaks and mark now-passing tests\nas leak free.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n submodule-config.c                     | 2 ++\n t/t1013-read-tree-submodule.sh         | 1 +\n t/t2013-checkout-submodule.sh          | 1 +\n t/t3007-ls-files-recurse-submodules.sh | 1 +\n t/t7112-reset-submodule.sh             | 1 +\n 5 files changed, 6 insertions(+)\n\ndiff --git a/submodule-config.c b/submodule-config.c\nindex 11428b4ada..ec45ea67b9 100644\n--- a/submodule-config.c\n+++ b/submodule-config.c\n@@ -91,6 +91,8 @@ static void free_one_config(struct submodule_entry *entry)\n \tfree((void *) entry->config->path);\n \tfree((void *) entry->config->name);\n \tfree((void *) entry->config->branch);\n+\tfree((void *) entry->config->url);\n+\tfree((void *) entry->config->ignore);\n \tfree((void *) entry->config->update_strategy.command);\n \tfree(entry->config);\n }\ndiff --git a/t/t1013-read-tree-submodule.sh b/t/t1013-read-tree-submodule.sh\nindex bfc90d4cf2..cf8b94ebed 100755\n--- a/t/t1013-read-tree-submodule.sh\n+++ b/t/t1013-read-tree-submodule.sh\n@@ -2,6 +2,7 @@\n \n test_description='read-tree can handle submodules'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-submodule-update.sh\n \ndiff --git a/t/t2013-checkout-submodule.sh b/t/t2013-checkout-submodule.sh\nindex b2bdd1fcb4..3c1d663d94 100755\n--- a/t/t2013-checkout-submodule.sh\n+++ b/t/t2013-checkout-submodule.sh\n@@ -2,6 +2,7 @@\n \n test_description='checkout can handle submodules'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-submodule-update.sh\n \ndiff --git a/t/t3007-ls-files-recurse-submodules.sh b/t/t3007-ls-files-recurse-submodules.sh\nindex 61771eec83..f04bdc8c78 100755\n--- a/t/t3007-ls-files-recurse-submodules.sh\n+++ b/t/t3007-ls-files-recurse-submodules.sh\n@@ -6,6 +6,7 @@ This test verifies the recurse-submodules feature correctly lists files from\n submodules.\n '\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n test_expect_success 'setup directory structure and submodules' '\ndiff --git a/t/t7112-reset-submodule.sh b/t/t7112-reset-submodule.sh\nindex a3e2413bc3..b0d3d93b0b 100755\n--- a/t/t7112-reset-submodule.sh\n+++ b/t/t7112-reset-submodule.sh\n@@ -2,6 +2,7 @@\n \n test_description='reset can handle submodules'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-submodule-update.sh\n \n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495399","messageId":"c43c93db3bdc0ea5283a6d5c71d37d6fe949dd8c.1716465556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716465556.git.ps@pks.im","subject":"[PATCH 16/20] strvec: add functions to replace and remove strings","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-23T12:26:28Z","receivedAt":"2024-05-23T12:26:31Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Add two functions that allow to replace and remove strings contained in\nthe strvec. This will be used by a subsequent commit that refactors\ngit-mv(1).\n\nWhile at it, add a bunch of unit tests that cover both old and new\nfunctionality.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile                |   1 +\n strvec.c                |  20 ++++\n strvec.h                |  13 ++\n t/unit-tests/t-strvec.c | 259 ++++++++++++++++++++++++++++++++++++++++\n t/unit-tests/test-lib.c |  13 ++\n t/unit-tests/test-lib.h |  13 ++\n 6 files changed, 319 insertions(+)\n create mode 100644 t/unit-tests/t-strvec.c\n\ndiff --git a/Makefile b/Makefile\nindex cf504963c2..d4000fb1d6 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1336,6 +1336,7 @@ THIRD_PARTY_SOURCES += sha1dc/%\n \n UNIT_TEST_PROGRAMS += t-mem-pool\n UNIT_TEST_PROGRAMS += t-strbuf\n+UNIT_TEST_PROGRAMS += t-strvec\n UNIT_TEST_PROGRAMS += t-ctype\n UNIT_TEST_PROGRAMS += t-prio-queue\n UNIT_TEST_PROGS = $(patsubst %,$(UNIT_TEST_BIN)/%$X,$(UNIT_TEST_PROGRAMS))\ndiff --git a/strvec.c b/strvec.c\nindex 178f4f3748..d4073ec9fa 100644\n--- a/strvec.c\n+++ b/strvec.c\n@@ -56,6 +56,26 @@ void strvec_pushv(struct strvec *array, const char **items)\n \t\tstrvec_push(array, *items);\n }\n \n+const char *strvec_replace(struct strvec *array, size_t idx, const char *replacement)\n+{\n+\tchar *to_free;\n+\tif (idx >= array->nr)\n+\t\tBUG(\"index outside of array boundary\");\n+\tto_free = (char *) array->v[idx];\n+\tarray->v[idx] = xstrdup(replacement);\n+\tfree(to_free);\n+\treturn array->v[idx];\n+}\n+\n+void strvec_remove(struct strvec *array, size_t idx)\n+{\n+\tif (idx >= array->nr)\n+\t\tBUG(\"index outside of array boundary\");\n+\tfree((char *)array->v[idx]);\n+\tmemmove(array->v + idx, array->v + idx + 1, (array->nr - idx) * sizeof(char *));\n+\tarray->nr--;\n+}\n+\n void strvec_pop(struct strvec *array)\n {\n \tif (!array->nr)\ndiff --git a/strvec.h b/strvec.h\nindex 4715d3e51f..6c7e8b7d50 100644\n--- a/strvec.h\n+++ b/strvec.h\n@@ -64,6 +64,19 @@ void strvec_pushl(struct strvec *, ...);\n /* Push a null-terminated array of strings onto the end of the array. */\n void strvec_pushv(struct strvec *, const char **);\n \n+/**\n+ * Replace the value at the given index with a new value. The index must be\n+ * valid. Returns a pointer to the inserted value.\n+ */\n+const char *strvec_replace(struct strvec *array, size_t idx, const char *replacement);\n+\n+/*\n+ * Remove the value at the given index. The remainder of the array will be\n+ * moved to fill the resulting gap. The provided index must point into the\n+ * array.\n+ */\n+void strvec_remove(struct strvec *array, size_t idx);\n+\n /**\n  * Remove the final element from the array. If there are no\n  * elements in the array, do nothing.\ndiff --git a/t/unit-tests/t-strvec.c b/t/unit-tests/t-strvec.c\nnew file mode 100644\nindex 0000000000..c07ad62c7d\n--- /dev/null\n+++ b/t/unit-tests/t-strvec.c\n@@ -0,0 +1,259 @@\n+#include \"test-lib.h\"\n+#include \"strbuf.h\"\n+#include \"strvec.h\"\n+\n+#define check_strvec(vec, ...) \\\n+\tcheck_strvec_loc(TEST_LOCATION(), vec, __VA_ARGS__)\n+static void check_strvec_loc(const char *loc, struct strvec *vec, ...)\n+{\n+\tva_list ap;\n+\tsize_t nr = 0;\n+\n+\tva_start(ap, vec);\n+\twhile (1) {\n+\t\tconst char *str = va_arg(ap, const char *);\n+\t\tif (!str)\n+\t\t\tbreak;\n+\n+\t\tif (!check_uint(vec->nr, >, nr) ||\n+\t\t    !check_uint(vec->alloc, >, nr) ||\n+\t\t    !check_str(vec->v[nr], str)) {\n+\t\t\tstruct strbuf msg = STRBUF_INIT;\n+\t\t\tstrbuf_addf(&msg, \"strvec index %\"PRIuMAX, (uintmax_t) nr);\n+\t\t\ttest_assert(loc, msg.buf, 0);\n+\t\t\tstrbuf_release(&msg);\n+\t\t\treturn;\n+\t\t}\n+\n+\t\tnr++;\n+\t}\n+\n+\tcheck_uint(vec->nr, ==, nr);\n+\tcheck_uint(vec->alloc, >=, nr);\n+\tcheck_pointer_eq(vec->v[nr], NULL);\n+}\n+\n+static void t_static_init(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tcheck_pointer_eq(vec.v, empty_strvec);\n+\tcheck_uint(vec.nr, ==, 0);\n+\tcheck_uint(vec.alloc, ==, 0);\n+}\n+\n+static void t_dynamic_init(void)\n+{\n+\tstruct strvec vec;\n+\tstrvec_init(&vec);\n+\tcheck_pointer_eq(vec.v, empty_strvec);\n+\tcheck_uint(vec.nr, ==, 0);\n+\tcheck_uint(vec.alloc, ==, 0);\n+}\n+\n+static void t_clear(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_push(&vec, \"foo\");\n+\tstrvec_clear(&vec);\n+\tcheck_pointer_eq(vec.v, empty_strvec);\n+\tcheck_uint(vec.nr, ==, 0);\n+\tcheck_uint(vec.alloc, ==, 0);\n+}\n+\n+static void t_push(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_push(&vec, \"foo\");\n+\tcheck_strvec(&vec, \"foo\", NULL);\n+\n+\tstrvec_push(&vec, \"bar\");\n+\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n+\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_pushf(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushf(&vec, \"foo: %d\", 1);\n+\tcheck_strvec(&vec, \"foo: 1\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_pushl(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_pushv(void)\n+{\n+\tconst char *strings[] = {\n+\t\t\"foo\", \"bar\", \"baz\", NULL,\n+\t};\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_pushv(&vec, strings);\n+\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_replace_at_head(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_replace(&vec, 0, \"replaced\");\n+\tcheck_strvec(&vec, \"replaced\", \"bar\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_replace_at_tail(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_replace(&vec, 2, \"replaced\");\n+\tcheck_strvec(&vec, \"foo\", \"bar\", \"replaced\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_replace_in_between(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_replace(&vec, 1, \"replaced\");\n+\tcheck_strvec(&vec, \"foo\", \"replaced\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_remove_at_head(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_remove(&vec, 0);\n+\tcheck_strvec(&vec, \"bar\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_remove_at_tail(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_remove(&vec, 2);\n+\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_remove_in_between(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_remove(&vec, 1);\n+\tcheck_strvec(&vec, \"foo\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_pop_empty_array(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pop(&vec);\n+\tcheck_strvec(&vec, NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_pop_non_empty_array(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_pop(&vec);\n+\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_split_empty_string(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_split(&vec, \"\");\n+\tcheck_strvec(&vec, NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_split_single_item(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_split(&vec, \"foo\");\n+\tcheck_strvec(&vec, \"foo\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_split_multiple_items(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_split(&vec, \"foo bar baz\");\n+\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_split_whitespace_only(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_split(&vec, \" \\t\\n\");\n+\tcheck_strvec(&vec, NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_split_multiple_consecutive_whitespaces(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_split(&vec, \"foo\\n\\t bar\");\n+\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_detach(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tconst char **detached;\n+\n+\tstrvec_push(&vec, \"foo\");\n+\n+\tdetached = strvec_detach(&vec);\n+\tcheck_str(detached[0], \"foo\");\n+\tcheck_pointer_eq(detached[1], NULL);\n+\n+\tcheck_pointer_eq(vec.v, empty_strvec);\n+\tcheck_uint(vec.nr, ==, 0);\n+\tcheck_uint(vec.alloc, ==, 0);\n+\n+\tfree((char *) detached[0]);\n+\tfree(detached);\n+}\n+\n+int cmd_main(int argc, const char **argv)\n+{\n+\tTEST(t_static_init(), \"static initialization\");\n+\tTEST(t_dynamic_init(), \"dynamic initialization\");\n+\tTEST(t_clear(), \"clear\");\n+\tTEST(t_push(), \"push\");\n+\tTEST(t_pushf(), \"pushf\");\n+\tTEST(t_pushl(), \"pushl\");\n+\tTEST(t_pushv(), \"pushv\");\n+\tTEST(t_replace_at_head(), \"replace at head\");\n+\tTEST(t_replace_in_between(), \"replace in between\");\n+\tTEST(t_replace_at_tail(), \"replace at tail\");\n+\tTEST(t_remove_at_head(), \"remove at head\");\n+\tTEST(t_remove_in_between(), \"remove in between\");\n+\tTEST(t_remove_at_tail(), \"remove at tail\");\n+\tTEST(t_pop_empty_array(), \"pop with empty array\");\n+\tTEST(t_pop_non_empty_array(), \"pop with non-empty array\");\n+\tTEST(t_split_empty_string(), \"split empty string\");\n+\tTEST(t_split_single_item(), \"split single item\");\n+\tTEST(t_split_multiple_items(), \"split multiple items\");\n+\tTEST(t_split_whitespace_only(), \"split whitespace only\");\n+\tTEST(t_split_multiple_consecutive_whitespaces(), \"split multiple consecutive whitespaces\");\n+\tTEST(t_detach(), \"detach\");\n+\treturn test_done();\n+}\ndiff --git a/t/unit-tests/test-lib.c b/t/unit-tests/test-lib.c\nindex 66d6980ffb..3c513ce59a 100644\n--- a/t/unit-tests/test-lib.c\n+++ b/t/unit-tests/test-lib.c\n@@ -318,6 +318,19 @@ int check_bool_loc(const char *loc, const char *check, int ok)\n \n union test__tmp test__tmp[2];\n \n+int check_pointer_eq_loc(const char *loc, const char *check, int ok,\n+\t\t\t const void *a, const void *b)\n+{\n+\tint ret = test_assert(loc, check, ok);\n+\n+\tif (!ret) {\n+\t\ttest_msg(\"   left: %p\", a);\n+\t\ttest_msg(\"  right: %p\", b);\n+\t}\n+\n+\treturn ret;\n+}\n+\n int check_int_loc(const char *loc, const char *check, int ok,\n \t\t  intmax_t a, intmax_t b)\n {\ndiff --git a/t/unit-tests/test-lib.h b/t/unit-tests/test-lib.h\nindex a8f07ae0b7..2de6d715d5 100644\n--- a/t/unit-tests/test-lib.h\n+++ b/t/unit-tests/test-lib.h\n@@ -75,6 +75,18 @@ int test_assert(const char *location, const char *check, int ok);\n \tcheck_bool_loc(TEST_LOCATION(), #x, x)\n int check_bool_loc(const char *loc, const char *check, int ok);\n \n+/*\n+ * Compare two integers. Prints a message with the two values if the\n+ * comparison fails. NB this is not thread safe.\n+ */\n+#define check_pointer_eq(a, b)\t\t\t\t\t\t\\\n+\t(test__tmp[0].p = (a), test__tmp[1].p = (b),\t\t\t\\\n+\t check_pointer_eq_loc(TEST_LOCATION(), #a\" == \"#b,\t\t\\\n+\t\t\t      test__tmp[0].p == test__tmp[1].p,\t\t\\\n+\t\t\t      test__tmp[0].p, test__tmp[1].p))\n+int check_pointer_eq_loc(const char *loc, const char *check, int ok,\n+\t\t\t const void *a, const void *b);\n+\n /*\n  * Compare two integers. Prints a message with the two values if the\n  * comparison fails. NB this is not thread safe.\n@@ -136,6 +148,7 @@ union test__tmp {\n \tintmax_t i;\n \tuintmax_t u;\n \tchar c;\n+\tconst void *p;\n };\n \n extern union test__tmp test__tmp[2];\n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495400","messageId":"97470398ad943776577f18ad2e19bb8707e6dc46.1716465556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716465556.git.ps@pks.im","subject":"[PATCH 17/20] builtin/mv: refactor `add_slash()` to always return allocated strings","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-23T12:26:33Z","receivedAt":"2024-05-23T12:26:37Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The `add_slash()` function will only conditionally return an allocated\nstring when the passed-in string did not yet have a trailing slash. This\nmakes the memory ownership harder to track than really necessary.\n\nIt's dubious whether this optimization really buys us all that much. The\nnumber of times we execute this function is bounded by the number of\narguments to git-mv(1), so in the typical case we may end up saving an\nallocation or two.\n\nSimplify the code to unconditionally return allocated strings.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/mv.c | 38 ++++++++++++++++++++------------------\n 1 file changed, 20 insertions(+), 18 deletions(-)\n\ndiff --git a/builtin/mv.c b/builtin/mv.c\nindex 74aa9746aa..9f4c75df04 100644\n--- a/builtin/mv.c\n+++ b/builtin/mv.c\n@@ -76,7 +76,7 @@ static const char **internal_prefix_pathspec(const char *prefix,\n \treturn result;\n }\n \n-static const char *add_slash(const char *path)\n+static char *add_slash(const char *path)\n {\n \tsize_t len = strlen(path);\n \tif (len && path[len - 1] != '/') {\n@@ -86,7 +86,7 @@ static const char *add_slash(const char *path)\n \t\twith_slash[len] = 0;\n \t\treturn with_slash;\n \t}\n-\treturn path;\n+\treturn xstrdup(path);\n }\n \n #define SUBMODULE_WITH_GITDIR ((const char *)1)\n@@ -111,7 +111,7 @@ static void prepare_move_submodule(const char *src, int first,\n static int index_range_of_same_dir(const char *src, int length,\n \t\t\t\t   int *first_p, int *last_p)\n {\n-\tconst char *src_w_slash = add_slash(src);\n+\tchar *src_w_slash = add_slash(src);\n \tint first, last, len_w_slash = length + 1;\n \n \tfirst = index_name_pos(the_repository->index, src_w_slash, len_w_slash);\n@@ -124,8 +124,8 @@ static int index_range_of_same_dir(const char *src, int length,\n \t\tif (strncmp(path, src_w_slash, len_w_slash))\n \t\t\tbreak;\n \t}\n-\tif (src_w_slash != src)\n-\t\tfree((char *)src_w_slash);\n+\n+\tfree(src_w_slash);\n \t*first_p = first;\n \t*last_p = last;\n \treturn last - first;\n@@ -141,7 +141,7 @@ static int index_range_of_same_dir(const char *src, int length,\n static int empty_dir_has_sparse_contents(const char *name)\n {\n \tint ret = 0;\n-\tconst char *with_slash = add_slash(name);\n+\tchar *with_slash = add_slash(name);\n \tint length = strlen(with_slash);\n \n \tint pos = index_name_pos(the_repository->index, with_slash, length);\n@@ -159,8 +159,7 @@ static int empty_dir_has_sparse_contents(const char *name)\n \t}\n \n free_return:\n-\tif (with_slash != name)\n-\t\tfree((char *)with_slash);\n+\tfree(with_slash);\n \treturn ret;\n }\n \n@@ -178,7 +177,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\tOPT_END(),\n \t};\n \tconst char **source, **destination, **dest_path, **submodule_gitfile;\n-\tconst char *dst_w_slash;\n+\tchar *dst_w_slash = NULL;\n \tconst char **src_dir = NULL;\n \tint src_dir_nr = 0, src_dir_alloc = 0;\n \tstruct strbuf a_src_dir = STRBUF_INIT;\n@@ -243,10 +242,6 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\t\t\tdst_mode = SPARSE;\n \t\t}\n \t}\n-\tif (dst_w_slash != dest_path[0]) {\n-\t\tfree((char *)dst_w_slash);\n-\t\tdst_w_slash = NULL;\n-\t}\n \n \t/* Checking */\n \tfor (i = 0; i < argc; i++) {\n@@ -265,12 +260,14 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \n \t\t\tpos = index_name_pos(the_repository->index, src, length);\n \t\t\tif (pos < 0) {\n-\t\t\t\tconst char *src_w_slash = add_slash(src);\n+\t\t\t\tchar *src_w_slash = add_slash(src);\n \t\t\t\tif (!path_in_sparse_checkout(src_w_slash, the_repository->index) &&\n \t\t\t\t    empty_dir_has_sparse_contents(src)) {\n+\t\t\t\t\tfree(src_w_slash);\n \t\t\t\t\tmodes[i] |= SKIP_WORKTREE_DIR;\n \t\t\t\t\tgoto dir_check;\n \t\t\t\t}\n+\t\t\t\tfree(src_w_slash);\n \t\t\t\t/* only error if existence is expected. */\n \t\t\t\tif (!(modes[i] & SPARSE))\n \t\t\t\t\tbad = _(\"bad source\");\n@@ -310,7 +307,9 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \n dir_check:\n \t\tif (S_ISDIR(st.st_mode)) {\n-\t\t\tint j, dst_len, n;\n+\t\t\tchar *dst_with_slash;\n+\t\t\tsize_t dst_with_slash_len;\n+\t\t\tint j, n;\n \t\t\tint first = index_name_pos(the_repository->index, src, length), last;\n \n \t\t\tif (first >= 0) {\n@@ -335,19 +334,21 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\t\tREALLOC_ARRAY(modes, n);\n \t\t\tREALLOC_ARRAY(submodule_gitfile, n);\n \n-\t\t\tdst = add_slash(dst);\n-\t\t\tdst_len = strlen(dst);\n+\t\t\tdst_with_slash = add_slash(dst);\n+\t\t\tdst_with_slash_len = strlen(dst_with_slash);\n \n \t\t\tfor (j = 0; j < last - first; j++) {\n \t\t\t\tconst struct cache_entry *ce = the_repository->index->cache[first + j];\n \t\t\t\tconst char *path = ce->name;\n \t\t\t\tsource[argc + j] = path;\n \t\t\t\tdestination[argc + j] =\n-\t\t\t\t\tprefix_path(dst, dst_len, path + length + 1);\n+\t\t\t\t\tprefix_path(dst_with_slash, dst_with_slash_len, path + length + 1);\n \t\t\t\tmemset(modes + argc + j, 0, sizeof(enum update_mode));\n \t\t\t\tmodes[argc + j] |= ce_skip_worktree(ce) ? SPARSE : INDEX;\n \t\t\t\tsubmodule_gitfile[argc + j] = NULL;\n \t\t\t}\n+\n+\t\t\tfree(dst_with_slash);\n \t\t\targc += last - first;\n \t\t\tgoto act_on_entry;\n \t\t}\n@@ -565,6 +566,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\t\t       COMMIT_LOCK | SKIP_IF_UNCHANGED))\n \t\tdie(_(\"Unable to write new index file\"));\n \n+\tfree(dst_w_slash);\n \tstring_list_clear(&src_for_dst, 0);\n \tstring_list_clear(&dirty_paths, 0);\n \tUNLEAK(source);\n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495401","messageId":"7a2e5e82ccfcd2194b2f2787f33733953c2e0d6d.1716465556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716465556.git.ps@pks.im","subject":"[PATCH 18/20] builtin/mv duplicate string list memory","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-23T12:26:38Z","receivedAt":"2024-05-23T12:26:41Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"makes the next patch easier, where we will migrate to the paths being\nowned by a strvec. given that we are talking about command line\nparameters here it's also not like we have tons of allocations that this\nwould save\n\nwhile at it, fix a memory leak\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/mv.c | 19 +++++++++++++------\n 1 file changed, 13 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/mv.c b/builtin/mv.c\nindex 9f4c75df04..12dcc0b13c 100644\n--- a/builtin/mv.c\n+++ b/builtin/mv.c\n@@ -183,11 +183,12 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \tstruct strbuf a_src_dir = STRBUF_INIT;\n \tenum update_mode *modes, dst_mode = 0;\n \tstruct stat st, dest_st;\n-\tstruct string_list src_for_dst = STRING_LIST_INIT_NODUP;\n+\tstruct string_list src_for_dst = STRING_LIST_INIT_DUP;\n \tstruct lock_file lock_file = LOCK_INIT;\n \tstruct cache_entry *ce;\n-\tstruct string_list only_match_skip_worktree = STRING_LIST_INIT_NODUP;\n-\tstruct string_list dirty_paths = STRING_LIST_INIT_NODUP;\n+\tstruct string_list only_match_skip_worktree = STRING_LIST_INIT_DUP;\n+\tstruct string_list dirty_paths = STRING_LIST_INIT_DUP;\n+\tint ret;\n \n \tgit_config(git_default_config, NULL);\n \n@@ -440,8 +441,10 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \n \tif (only_match_skip_worktree.nr) {\n \t\tadvise_on_updating_sparse_paths(&only_match_skip_worktree);\n-\t\tif (!ignore_errors)\n-\t\t\treturn 1;\n+\t\tif (!ignore_errors) {\n+\t\t\tret = 1;\n+\t\t\tgoto out;\n+\t\t}\n \t}\n \n \tfor (i = 0; i < argc; i++) {\n@@ -566,12 +569,16 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\t\t       COMMIT_LOCK | SKIP_IF_UNCHANGED))\n \t\tdie(_(\"Unable to write new index file\"));\n \n+\tret = 0;\n+\n+out:\n \tfree(dst_w_slash);\n \tstring_list_clear(&src_for_dst, 0);\n \tstring_list_clear(&dirty_paths, 0);\n+\tstring_list_clear(&only_match_skip_worktree, 0);\n \tUNLEAK(source);\n \tUNLEAK(dest_path);\n \tfree(submodule_gitfile);\n \tfree(modes);\n-\treturn 0;\n+\treturn ret;\n }\n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495402","messageId":"b546ca4d62c6828df0080028bbf18020dface3fc.1716465556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716465556.git.ps@pks.im","subject":"[PATCH 19/20] builtin/mv: refactor to use `struct strvec`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-23T12:26:44Z","receivedAt":"2024-05-23T12:26:48Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Memory allocation patterns in git-mv(1) are extremely hard to follow:\nWe copy around string pointers into manually-managed arrays, some of\nwhich alias each other, but only sometimes, while we also drop some of\nthose strings at other times without ever daring to free them.\n\nWhile this may be my own subjective feeling, it seems like others have\ngiven up as the code has multiple calls to `UNLEAK()`. These are not\nsufficient though, and git-mv(1) is still leaking all over the place\neven with them.\n\nRefactor the code to instead track strings in `struct strvec`. While\nthis has the effect of effectively duplicating some of the strings\nwithout an actual need, it is way easier to reason about and fixes all\nof the aliasing of memory that has been going on. It allows us to get\nrid of the `UNLEAK()` calls and also fixes leaks that those calls did\nnot paper over.\n\nMark tests which are now leak-free accordingly.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/mv.c                             | 125 +++++++++++------------\n t/t4001-diff-rename.sh                   |   4 +-\n t/t4043-diff-rename-binary.sh            |   1 +\n t/t4120-apply-popt.sh                    |   1 +\n t/t6400-merge-df.sh                      |   1 +\n t/t6412-merge-large-rename.sh            |   1 +\n t/t6426-merge-skip-unneeded-updates.sh   |   1 +\n t/t6429-merge-sequence-rename-caching.sh |   1 +\n 8 files changed, 68 insertions(+), 67 deletions(-)\n\ndiff --git a/builtin/mv.c b/builtin/mv.c\nindex 12dcc0b13c..e461d29ca1 100644\n--- a/builtin/mv.c\n+++ b/builtin/mv.c\n@@ -20,6 +20,7 @@\n #include \"read-cache-ll.h\"\n #include \"repository.h\"\n #include \"setup.h\"\n+#include \"strvec.h\"\n #include \"submodule.h\"\n #include \"entry.h\"\n \n@@ -38,42 +39,32 @@ enum update_mode {\n #define DUP_BASENAME 1\n #define KEEP_TRAILING_SLASH 2\n \n-static const char **internal_prefix_pathspec(const char *prefix,\n-\t\t\t\t\t     const char **pathspec,\n-\t\t\t\t\t     int count, unsigned flags)\n+static void internal_prefix_pathspec(struct strvec *out,\n+\t\t\t\t     const char *prefix,\n+\t\t\t\t     const char **pathspec,\n+\t\t\t\t     int count, unsigned flags)\n {\n-\tint i;\n-\tconst char **result;\n \tint prefixlen = prefix ? strlen(prefix) : 0;\n-\tALLOC_ARRAY(result, count + 1);\n \n \t/* Create an intermediate copy of the pathspec based on the flags */\n-\tfor (i = 0; i < count; i++) {\n-\t\tint length = strlen(pathspec[i]);\n-\t\tint to_copy = length;\n-\t\tchar *it;\n+\tfor (int i = 0; i < count; i++) {\n+\t\tsize_t length = strlen(pathspec[i]);\n+\t\tsize_t to_copy = length;\n+\t\tconst char *maybe_basename;\n+\t\tchar *trimmed, *prefixed_path;\n+\n \t\twhile (!(flags & KEEP_TRAILING_SLASH) &&\n \t\t       to_copy > 0 && is_dir_sep(pathspec[i][to_copy - 1]))\n \t\t\tto_copy--;\n \n-\t\tit = xmemdupz(pathspec[i], to_copy);\n-\t\tif (flags & DUP_BASENAME) {\n-\t\t\tresult[i] = xstrdup(basename(it));\n-\t\t\tfree(it);\n-\t\t} else {\n-\t\t\tresult[i] = it;\n-\t\t}\n-\t}\n-\tresult[count] = NULL;\n+\t\ttrimmed = xmemdupz(pathspec[i], to_copy);\n+\t\tmaybe_basename = (flags & DUP_BASENAME) ? basename(trimmed) : trimmed;\n+\t\tprefixed_path = prefix_path(prefix, prefixlen, maybe_basename);\n+\t\tstrvec_push(out, prefixed_path);\n \n-\t/* Prefix the pathspec and free the old intermediate strings */\n-\tfor (i = 0; i < count; i++) {\n-\t\tconst char *match = prefix_path(prefix, prefixlen, result[i]);\n-\t\tfree((char *) result[i]);\n-\t\tresult[i] = match;\n+\t\tfree(prefixed_path);\n+\t\tfree(trimmed);\n \t}\n-\n-\treturn result;\n }\n \n static char *add_slash(const char *path)\n@@ -176,7 +167,10 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\tOPT_BOOL(0, \"sparse\", &ignore_sparse, N_(\"allow updating entries outside of the sparse-checkout cone\")),\n \t\tOPT_END(),\n \t};\n-\tconst char **source, **destination, **dest_path, **submodule_gitfile;\n+\tstruct strvec sources = STRVEC_INIT;\n+\tstruct strvec dest_paths = STRVEC_INIT;\n+\tstruct strvec destinations = STRVEC_INIT;\n+\tconst char **submodule_gitfile;\n \tchar *dst_w_slash = NULL;\n \tconst char **src_dir = NULL;\n \tint src_dir_nr = 0, src_dir_alloc = 0;\n@@ -201,7 +195,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \tif (repo_read_index(the_repository) < 0)\n \t\tdie(_(\"index file corrupt\"));\n \n-\tsource = internal_prefix_pathspec(prefix, argv, argc, 0);\n+\tinternal_prefix_pathspec(&sources, prefix, argv, argc, 0);\n \tCALLOC_ARRAY(modes, argc);\n \n \t/*\n@@ -212,41 +206,39 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \tflags = KEEP_TRAILING_SLASH;\n \tif (argc == 1 && is_directory(argv[0]) && !is_directory(argv[1]))\n \t\tflags = 0;\n-\tdest_path = internal_prefix_pathspec(prefix, argv + argc, 1, flags);\n-\tdst_w_slash = add_slash(dest_path[0]);\n+\tinternal_prefix_pathspec(&dest_paths, prefix, argv + argc, 1, flags);\n+\tdst_w_slash = add_slash(dest_paths.v[0]);\n \tsubmodule_gitfile = xcalloc(argc, sizeof(char *));\n \n-\tif (dest_path[0][0] == '\\0')\n+\tif (dest_paths.v[0][0] == '\\0')\n \t\t/* special case: \".\" was normalized to \"\" */\n-\t\tdestination = internal_prefix_pathspec(dest_path[0], argv, argc, DUP_BASENAME);\n-\telse if (!lstat(dest_path[0], &st) &&\n-\t\t\tS_ISDIR(st.st_mode)) {\n-\t\tdestination = internal_prefix_pathspec(dst_w_slash, argv, argc, DUP_BASENAME);\n+\t\tinternal_prefix_pathspec(&destinations, dest_paths.v[0], argv, argc, DUP_BASENAME);\n+\telse if (!lstat(dest_paths.v[0], &st) && S_ISDIR(st.st_mode)) {\n+\t\tinternal_prefix_pathspec(&destinations, dst_w_slash, argv, argc, DUP_BASENAME);\n+\t} else if (!path_in_sparse_checkout(dst_w_slash, the_repository->index) &&\n+\t\t   empty_dir_has_sparse_contents(dst_w_slash)) {\n+\t\tinternal_prefix_pathspec(&destinations, dst_w_slash, argv, argc, DUP_BASENAME);\n+\t\tdst_mode = SKIP_WORKTREE_DIR;\n+\t} else if (argc != 1) {\n+\t\tdie(_(\"destination '%s' is not a directory\"), dest_paths.v[0]);\n \t} else {\n-\t\tif (!path_in_sparse_checkout(dst_w_slash, the_repository->index) &&\n-\t\t    empty_dir_has_sparse_contents(dst_w_slash)) {\n-\t\t\tdestination = internal_prefix_pathspec(dst_w_slash, argv, argc, DUP_BASENAME);\n-\t\t\tdst_mode = SKIP_WORKTREE_DIR;\n-\t\t} else if (argc != 1) {\n-\t\t\tdie(_(\"destination '%s' is not a directory\"), dest_path[0]);\n-\t\t} else {\n-\t\t\tdestination = dest_path;\n-\t\t\t/*\n-\t\t\t * <destination> is a file outside of sparse-checkout\n-\t\t\t * cone. Insist on cone mode here for backward\n-\t\t\t * compatibility. We don't want dst_mode to be assigned\n-\t\t\t * for a file when the repo is using no-cone mode (which\n-\t\t\t * is deprecated at this point) sparse-checkout. As\n-\t\t\t * SPARSE here is only considering cone-mode situation.\n-\t\t\t */\n-\t\t\tif (!path_in_cone_mode_sparse_checkout(destination[0], the_repository->index))\n-\t\t\t\tdst_mode = SPARSE;\n-\t\t}\n+\t\tstrvec_pushv(&destinations, dest_paths.v);\n+\n+\t\t/*\n+\t\t * <destination> is a file outside of sparse-checkout\n+\t\t * cone. Insist on cone mode here for backward\n+\t\t * compatibility. We don't want dst_mode to be assigned\n+\t\t * for a file when the repo is using no-cone mode (which\n+\t\t * is deprecated at this point) sparse-checkout. As\n+\t\t * SPARSE here is only considering cone-mode situation.\n+\t\t */\n+\t\tif (!path_in_cone_mode_sparse_checkout(destinations.v[0], the_repository->index))\n+\t\t\tdst_mode = SPARSE;\n \t}\n \n \t/* Checking */\n \tfor (i = 0; i < argc; i++) {\n-\t\tconst char *src = source[i], *dst = destination[i];\n+\t\tconst char *src = sources.v[i], *dst = destinations.v[i];\n \t\tint length;\n \t\tconst char *bad = NULL;\n \t\tint skip_sparse = 0;\n@@ -330,8 +322,6 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\t\tsrc_dir[src_dir_nr++] = src;\n \n \t\t\tn = argc + last - first;\n-\t\t\tREALLOC_ARRAY(source, n);\n-\t\t\tREALLOC_ARRAY(destination, n);\n \t\t\tREALLOC_ARRAY(modes, n);\n \t\t\tREALLOC_ARRAY(submodule_gitfile, n);\n \n@@ -341,12 +331,16 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\t\tfor (j = 0; j < last - first; j++) {\n \t\t\t\tconst struct cache_entry *ce = the_repository->index->cache[first + j];\n \t\t\t\tconst char *path = ce->name;\n-\t\t\t\tsource[argc + j] = path;\n-\t\t\t\tdestination[argc + j] =\n-\t\t\t\t\tprefix_path(dst_with_slash, dst_with_slash_len, path + length + 1);\n+\t\t\t\tchar *prefixed_path = prefix_path(dst_with_slash, dst_with_slash_len, path + length + 1);\n+\n+\t\t\t\tstrvec_push(&sources, path);\n+\t\t\t\tstrvec_push(&destinations, prefixed_path);\n+\n \t\t\t\tmemset(modes + argc + j, 0, sizeof(enum update_mode));\n \t\t\t\tmodes[argc + j] |= ce_skip_worktree(ce) ? SPARSE : INDEX;\n \t\t\t\tsubmodule_gitfile[argc + j] = NULL;\n+\n+\t\t\t\tfree(prefixed_path);\n \t\t\t}\n \n \t\t\tfree(dst_with_slash);\n@@ -430,8 +424,8 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n remove_entry:\n \t\tif (--argc > 0) {\n \t\t\tint n = argc - i;\n-\t\t\tMOVE_ARRAY(source + i, source + i + 1, n);\n-\t\t\tMOVE_ARRAY(destination + i, destination + i + 1, n);\n+\t\t\tstrvec_remove(&sources, i);\n+\t\t\tstrvec_remove(&destinations, i);\n \t\t\tMOVE_ARRAY(modes + i, modes + i + 1, n);\n \t\t\tMOVE_ARRAY(submodule_gitfile + i,\n \t\t\t\t   submodule_gitfile + i + 1, n);\n@@ -448,7 +442,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t}\n \n \tfor (i = 0; i < argc; i++) {\n-\t\tconst char *src = source[i], *dst = destination[i];\n+\t\tconst char *src = sources.v[i], *dst = destinations.v[i];\n \t\tenum update_mode mode = modes[i];\n \t\tint pos;\n \t\tint sparse_and_dirty = 0;\n@@ -576,8 +570,9 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \tstring_list_clear(&src_for_dst, 0);\n \tstring_list_clear(&dirty_paths, 0);\n \tstring_list_clear(&only_match_skip_worktree, 0);\n-\tUNLEAK(source);\n-\tUNLEAK(dest_path);\n+\tstrvec_clear(&sources);\n+\tstrvec_clear(&dest_paths);\n+\tstrvec_clear(&destinations);\n \tfree(submodule_gitfile);\n \tfree(modes);\n \treturn ret;\ndiff --git a/t/t4001-diff-rename.sh b/t/t4001-diff-rename.sh\nindex 49c042a38a..cd1931dd55 100755\n--- a/t/t4001-diff-rename.sh\n+++ b/t/t4001-diff-rename.sh\n@@ -3,9 +3,9 @@\n # Copyright (c) 2005 Junio C Hamano\n #\n \n-test_description='Test rename detection in diff engine.\n+test_description='Test rename detection in diff engine.'\n \n-'\n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-diff.sh\n \ndiff --git a/t/t4043-diff-rename-binary.sh b/t/t4043-diff-rename-binary.sh\nindex 2a2cf91352..e486493908 100755\n--- a/t/t4043-diff-rename-binary.sh\n+++ b/t/t4043-diff-rename-binary.sh\n@@ -5,6 +5,7 @@\n \n test_description='Move a binary file'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n \ndiff --git a/t/t4120-apply-popt.sh b/t/t4120-apply-popt.sh\nindex 697e86c0ff..f788428540 100755\n--- a/t/t4120-apply-popt.sh\n+++ b/t/t4120-apply-popt.sh\n@@ -5,6 +5,7 @@\n \n test_description='git apply -p handling.'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n test_expect_success setup '\ndiff --git a/t/t6400-merge-df.sh b/t/t6400-merge-df.sh\nindex 3de4ef6bd9..27d6efdc9a 100755\n--- a/t/t6400-merge-df.sh\n+++ b/t/t6400-merge-df.sh\n@@ -7,6 +7,7 @@ test_description='Test merge with directory/file conflicts'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n test_expect_success 'prepare repository' '\ndiff --git a/t/t6412-merge-large-rename.sh b/t/t6412-merge-large-rename.sh\nindex ca018d11f5..d0863a8fb5 100755\n--- a/t/t6412-merge-large-rename.sh\n+++ b/t/t6412-merge-large-rename.sh\n@@ -4,6 +4,7 @@ test_description='merging with large rename matrix'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n count() {\ndiff --git a/t/t6426-merge-skip-unneeded-updates.sh b/t/t6426-merge-skip-unneeded-updates.sh\nindex b059475ed0..62f0180325 100755\n--- a/t/t6426-merge-skip-unneeded-updates.sh\n+++ b/t/t6426-merge-skip-unneeded-updates.sh\n@@ -22,6 +22,7 @@ test_description=\"merge cases\"\n #                     underscore notation is to differentiate different\n #                     files that might be renamed into each other's paths.)\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-merge.sh\n \ndiff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh\nindex 0f39ed0d08..cb1c4ceef7 100755\n--- a/t/t6429-merge-sequence-rename-caching.sh\n+++ b/t/t6429-merge-sequence-rename-caching.sh\n@@ -2,6 +2,7 @@\n \n test_description=\"remember regular & dir renames in sequence of merges\"\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n #\n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495403","messageId":"bba735388d8b0a58e7a935174bd0ba054591b506.1716465556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716465556.git.ps@pks.im","subject":"[PATCH 20/20] builtin/mv: fix leaks for submodule gitfile paths","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-23T12:26:49Z","receivedAt":"2024-05-23T12:26:53Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Similar to the preceding commit, we have effectively given tracking\nmemory ownership of submodule gitfile paths. Refactor the code to start\ntracking allocated strings in a separate `struct strvec` such that we\ncan easily plug those leaks. Mark now-passing tests as leak free.\n\nNote that ideally, we wouldn't require two separate data structures to\ntrack those paths. But we do need to store `NULL` pointers for the\ngitfile paths such that we can indicate that its corresponding entries\nin the other arrays do not have such a path at all. And given that\n`struct strvec`s cannot store `NULL` pointers we cannot use them to\nstore this information.\n\nThere is another small gotcha that is easy to miss: you may be wondering\nwhy we don't want to store `SUBMODULE_WITH_GITDIR` in the strvec. This\nis because this is a mere sentinel value and not actually a string at\nall.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/mv.c                              | 44 +++++++++++++----------\n t/t4059-diff-submodule-not-initialized.sh |  1 +\n t/t7001-mv.sh                             |  2 ++\n t/t7417-submodule-path-url.sh             |  1 +\n t/t7421-submodule-summary-add.sh          |  1 +\n 5 files changed, 30 insertions(+), 19 deletions(-)\n\ndiff --git a/builtin/mv.c b/builtin/mv.c\nindex e461d29ca1..81ca910de6 100644\n--- a/builtin/mv.c\n+++ b/builtin/mv.c\n@@ -82,21 +82,23 @@ static char *add_slash(const char *path)\n \n #define SUBMODULE_WITH_GITDIR ((const char *)1)\n \n-static void prepare_move_submodule(const char *src, int first,\n-\t\t\t\t   const char **submodule_gitfile)\n+static const char *submodule_gitfile_path(const char *src, int first)\n {\n \tstruct strbuf submodule_dotgit = STRBUF_INIT;\n+\tconst char *path;\n+\n \tif (!S_ISGITLINK(the_repository->index->cache[first]->ce_mode))\n \t\tdie(_(\"Directory %s is in index and no submodule?\"), src);\n \tif (!is_staging_gitmodules_ok(the_repository->index))\n \t\tdie(_(\"Please stage your changes to .gitmodules or stash them to proceed\"));\n+\n \tstrbuf_addf(&submodule_dotgit, \"%s/.git\", src);\n-\t*submodule_gitfile = read_gitfile(submodule_dotgit.buf);\n-\tif (*submodule_gitfile)\n-\t\t*submodule_gitfile = xstrdup(*submodule_gitfile);\n-\telse\n-\t\t*submodule_gitfile = SUBMODULE_WITH_GITDIR;\n+\n+\tpath = read_gitfile(submodule_dotgit.buf);\n \tstrbuf_release(&submodule_dotgit);\n+\tif (path)\n+\t\treturn path;\n+\treturn SUBMODULE_WITH_GITDIR;\n }\n \n static int index_range_of_same_dir(const char *src, int length,\n@@ -170,7 +172,8 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \tstruct strvec sources = STRVEC_INIT;\n \tstruct strvec dest_paths = STRVEC_INIT;\n \tstruct strvec destinations = STRVEC_INIT;\n-\tconst char **submodule_gitfile;\n+\tstruct strvec submodule_gitfiles_to_free = STRVEC_INIT;\n+\tconst char **submodule_gitfiles;\n \tchar *dst_w_slash = NULL;\n \tconst char **src_dir = NULL;\n \tint src_dir_nr = 0, src_dir_alloc = 0;\n@@ -208,7 +211,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\tflags = 0;\n \tinternal_prefix_pathspec(&dest_paths, prefix, argv + argc, 1, flags);\n \tdst_w_slash = add_slash(dest_paths.v[0]);\n-\tsubmodule_gitfile = xcalloc(argc, sizeof(char *));\n+\tsubmodule_gitfiles = xcalloc(argc, sizeof(char *));\n \n \tif (dest_paths.v[0][0] == '\\0')\n \t\t/* special case: \".\" was normalized to \"\" */\n@@ -306,8 +309,10 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\t\tint first = index_name_pos(the_repository->index, src, length), last;\n \n \t\t\tif (first >= 0) {\n-\t\t\t\tprepare_move_submodule(src, first,\n-\t\t\t\t\t\t       submodule_gitfile + i);\n+\t\t\t\tconst char *path = submodule_gitfile_path(src, first);\n+\t\t\t\tif (path != SUBMODULE_WITH_GITDIR)\n+\t\t\t\t\tpath = strvec_push(&submodule_gitfiles_to_free, path);\n+\t\t\t\tsubmodule_gitfiles[i] = path;\n \t\t\t\tgoto act_on_entry;\n \t\t\t} else if (index_range_of_same_dir(src, length,\n \t\t\t\t\t\t\t   &first, &last) < 1) {\n@@ -323,7 +328,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \n \t\t\tn = argc + last - first;\n \t\t\tREALLOC_ARRAY(modes, n);\n-\t\t\tREALLOC_ARRAY(submodule_gitfile, n);\n+\t\t\tREALLOC_ARRAY(submodule_gitfiles, n);\n \n \t\t\tdst_with_slash = add_slash(dst);\n \t\t\tdst_with_slash_len = strlen(dst_with_slash);\n@@ -338,7 +343,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \n \t\t\t\tmemset(modes + argc + j, 0, sizeof(enum update_mode));\n \t\t\t\tmodes[argc + j] |= ce_skip_worktree(ce) ? SPARSE : INDEX;\n-\t\t\t\tsubmodule_gitfile[argc + j] = NULL;\n+\t\t\t\tsubmodule_gitfiles[argc + j] = NULL;\n \n \t\t\t\tfree(prefixed_path);\n \t\t\t}\n@@ -427,8 +432,8 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\t\tstrvec_remove(&sources, i);\n \t\t\tstrvec_remove(&destinations, i);\n \t\t\tMOVE_ARRAY(modes + i, modes + i + 1, n);\n-\t\t\tMOVE_ARRAY(submodule_gitfile + i,\n-\t\t\t\t   submodule_gitfile + i + 1, n);\n+\t\t\tMOVE_ARRAY(submodule_gitfiles + i,\n+\t\t\t\t   submodule_gitfiles + i + 1, n);\n \t\t\ti--;\n \t\t}\n \t}\n@@ -462,12 +467,12 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\t\t\tcontinue;\n \t\t\tdie_errno(_(\"renaming '%s' failed\"), src);\n \t\t}\n-\t\tif (submodule_gitfile[i]) {\n+\t\tif (submodule_gitfiles[i]) {\n \t\t\tif (!update_path_in_gitmodules(src, dst))\n \t\t\t\tgitmodules_modified = 1;\n-\t\t\tif (submodule_gitfile[i] != SUBMODULE_WITH_GITDIR)\n+\t\t\tif (submodule_gitfiles[i] != SUBMODULE_WITH_GITDIR)\n \t\t\t\tconnect_work_tree_and_git_dir(dst,\n-\t\t\t\t\t\t\t      submodule_gitfile[i],\n+\t\t\t\t\t\t\t      submodule_gitfiles[i],\n \t\t\t\t\t\t\t      1);\n \t\t}\n \n@@ -573,7 +578,8 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \tstrvec_clear(&sources);\n \tstrvec_clear(&dest_paths);\n \tstrvec_clear(&destinations);\n-\tfree(submodule_gitfile);\n+\tstrvec_clear(&submodule_gitfiles_to_free);\n+\tfree(submodule_gitfiles);\n \tfree(modes);\n \treturn ret;\n }\ndiff --git a/t/t4059-diff-submodule-not-initialized.sh b/t/t4059-diff-submodule-not-initialized.sh\nindex d489230df8..668f526303 100755\n--- a/t/t4059-diff-submodule-not-initialized.sh\n+++ b/t/t4059-diff-submodule-not-initialized.sh\n@@ -9,6 +9,7 @@ This test tries to verify that add_submodule_odb works when the submodule was\n initialized previously but the checkout has since been removed.\n '\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n # Tested non-UTF-8 encoding\ndiff --git a/t/t7001-mv.sh b/t/t7001-mv.sh\nindex 879a6dce60..86258f9f43 100755\n--- a/t/t7001-mv.sh\n+++ b/t/t7001-mv.sh\n@@ -1,6 +1,8 @@\n #!/bin/sh\n \n test_description='git mv in subdirs'\n+\n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-diff-data.sh\n \ndiff --git a/t/t7417-submodule-path-url.sh b/t/t7417-submodule-path-url.sh\nindex 5e3051da8b..dbbb3853dc 100755\n--- a/t/t7417-submodule-path-url.sh\n+++ b/t/t7417-submodule-path-url.sh\n@@ -4,6 +4,7 @@ test_description='check handling of .gitmodule path with dash'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n test_expect_success 'setup' '\ndiff --git a/t/t7421-submodule-summary-add.sh b/t/t7421-submodule-summary-add.sh\nindex ce64d8b137..479c8fdde1 100755\n--- a/t/t7421-submodule-summary-add.sh\n+++ b/t/t7421-submodule-summary-add.sh\n@@ -10,6 +10,7 @@ while making sure to add submodules using `git submodule add` instead of\n `git add` as done in t7401.\n '\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n test_expect_success 'setup' '\n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495429","messageId":"xmqqle40v5ms.fsf@gitster.g","threadId":"61535","inReplyTo":"cover.1716465556.git.ps@pks.im","subject":"Re: [PATCH 00/20] Various memory leak fixes","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-23T16:45:47Z","receivedAt":"2024-05-23T16:45:52Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> my mind had a couple of minutes where it was roaming, and of course it\n> immediately searched for and chased down the next rabbit hole. The\n> result is this patch series which fixes a bunch of leaks all over the\n> place. There isn't really any structure to the leaks that I did fix --\n> it's mostly things that I stumbled over. In the end, this series makes\n> another 56 test suites pass with leak checking enabled, 13 of which have\n> already been passing without any changes.\n\n... meaning there were coverage gaps?\n\n> While most things are unstructured, there are two topics that stand out:\n>\n>   - Patches 5 to 12 address a shortcoming of our config API. Both\n>     `git_config_string()` and `git_config_pathname()` have a `const char\n>     **` out parameter, but they do in fact transfer memory ownership to\n>     the caller. This resulted in a bunch of memory leaks all over the\n>     place.\n>\n>     These patches thus refactor a bunch of code and then ultimately\n>     switch the out parameter to become a `char *`\n\nI do remember getting hurt by this one relatively recently.\nAddressing the issue is very much appreciated.\n\n>   - Patches 16 to 20 have the goal of making git-mv(1) memory leak free.\n>     I had a very hard time understanding how it tracks memory. I think\n>     this wasn't only me, or otherwise there wouldn't be calls to\n>     `UNLEAK()` in there. In any case, I decided to rewrite the arrays to\n>     use a `struct strvec`, which makes tracking and releasing of memory\n>     a ton easier.\n>\n>     It does come at the cost of more allocations because we may now\n>     duplicate strings that we didn't before. But I think the tradeoff is\n>     worth it because the number of strings we may now duplicate is\n>     bounded by the number of command line arguments anyway.\n\nNice.  I have to admit that \"git mv\" is not one of the best-done\ncode in this project X-<, and improving it with rewriting was long\noverdue.\n\nThanks.\n"},{"id":"495430","messageId":"CAPig+cSiw7=Ah6o=i=BzSvbrvKLK3BtXJJX0BSQ7cJA-Ke=xww@mail.gmail.com","threadId":"61535","inReplyTo":"18dce492df7d4337dd639be767f2fe280083e9f6.1716465556.git.ps@pks.im","subject":"Re: [PATCH 07/20] diff: refactor code to clarify memory ownership of prefixes","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2024-05-23T16:59:14Z","receivedAt":"2024-05-23T16:59:26Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Thu, May 23, 2024 at 8:26 AM Patrick Steinhardt <ps@pks.im> wrote:\n> The source end destination prefixes are tracked in a `const char *`\n\ns/end/and/\n\n> array, but may at times contain allocated strings. The result is that\n> those strings may be leaking because we never free them.\n>\n> Refactor the code to always store allocated strings in those variables,\n> freeing them as required. This requires us to handle the default values\n> a bit different compared to before. But given that there is only a\n> single callsite where we use the variables to `struct diff_options` it's\n> easy to handle the defaults there.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n"},{"id":"495431","messageId":"CAPig+cTKrUL1AzY4Y+M130AvH5S=kSTdwo542T_GdFC=6JUu=Q@mail.gmail.com","threadId":"61535","inReplyTo":"c43c93db3bdc0ea5283a6d5c71d37d6fe949dd8c.1716465556.git.ps@pks.im","subject":"Re: [PATCH 16/20] strvec: add functions to replace and remove strings","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2024-05-23T17:09:39Z","receivedAt":"2024-05-23T17:09:51Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Thu, May 23, 2024 at 8:27 AM Patrick Steinhardt <ps@pks.im> wrote:\n> Add two functions that allow to replace and remove strings contained in\n> the strvec. This will be used by a subsequent commit that refactors\n> git-mv(1).\n> [...]\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n> diff --git a/strvec.c b/strvec.c\n> @@ -56,6 +56,26 @@ void strvec_pushv(struct strvec *array, const char **items)\n> +const char *strvec_replace(struct strvec *array, size_t idx, const char *replacement)\n> +{\n> +       char *to_free;\n> +       if (idx >= array->nr)\n> +               BUG(\"index outside of array boundary\");\n> +       to_free = (char *) array->v[idx];\n> +       array->v[idx] = xstrdup(replacement);\n> +       free(to_free);\n> +       return array->v[idx];\n> +}\n\nThe reason you delay calling free() until after xstrdup() is to\nprotect against the case when `replacement` is a substring of the\nstring already stored at `v[idx]`, correct?\n"},{"id":"495432","messageId":"xmqq7cfkv4cr.fsf@gitster.g","threadId":"61535","inReplyTo":"b2f8878b55564c4bf4848642e419c44c1a9d9005.1716465556.git.ps@pks.im","subject":"Re: [PATCH 12/20] config: plug various memory leaks","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-23T17:13:24Z","receivedAt":"2024-05-23T17:13:29Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\nThere seems to be a bit of problem with \"git mailinfo\" with this\npatch.  No need to resend this series, as I think I can cope, but to\nsolicit help from folks diagnosing and fixing the issue with the\n\"git am\" pipeline.\n\n> diff --git a/t/t4210-log-i18n.sh b/t/t4210-log-i18n.sh\n> index 75216f19ce..7120030b5c 100755\n> --- a/t/t4210-log-i18n.sh\n> +++ b/t/t4210-log-i18n.sh\n> @@ -1,6 +1,8 @@\n>  #!/bin/sh\n>  \n>  test_description='test log with i18n features'\n> +\n> +TEST_PASSES_SANITIZE_LEAK=true\n>  . ./lib-gettext.sh\n>  \n>  # two forms of é\n\nYou've been sending your patches as \"multipart/signed\" (which by the\nway was cumbersome enough for me but that is primarily the problem\nwith Emacs/GNUS and we should handle multipart/signed well), whose\nheader reads like so:\n\nContent-Type: multipart/signed; micalg=pgp-sha512;\n\tprotocol=\"application/pgp-signature\"; boundary=\"ovDSRLSkA00eIgbN\"\n\nThe \"message\" part in it says that it is encoded in iso-8859-1:\n\n    --ovDSRLSkA00eIgbN\n    Content-Type: text/plain; charset=iso-8859-1\n    Content-Disposition: inline\n    Content-Transfer-Encoding: quoted-printable\n\n    ...\n    diff --git a/t/t4210-log-i18n.sh b/t/t4210-log-i18n.sh\n    index 75216f19ce..7120030b5c 100755\n    --- a/t/t4210-log-i18n.sh\n    +++ b/t/t4210-log-i18n.sh\n    @@ -1,6 +1,8 @@\n     #!/bin/sh\n    =20\n     test_description=3D'test log with i18n features'\n    +\n    +TEST_PASSES_SANITIZE_LEAK=3Dtrue\n     . ./lib-gettext.sh\n    =20\n     # two forms of =E9\n\nBut the source in t/t4210-log-i18n.sh actually is written in UTF-8.\nThe thing is, the \"-u\" option (recode into utf-8\" is supposed to be\nthe default for \"git am\", and it is passed down to the underlying\nmailinfo machinery in builtin/am.c:parse_mail().\n\nBut apparently that is not working correctly.  I see in the patch an\nunrecoded byte E9 in the resulting patch file that is fed to the\nunderlying \"git apply\" machinery, failing the application.\n\nANyway...\n"},{"id":"495434","messageId":"xmqq34q8v3aq.fsf@gitster.g","threadId":"61535","inReplyTo":"05fbadbae2184479c87c37675dde7bd79b3e32ab.1716465556.git.ps@pks.im","subject":"Re: [PATCH 02/20] transport-helper: fix leaking helper name","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-23T17:36:13Z","receivedAt":"2024-05-23T17:36:16Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> When initializing the transport helper in `transport_get()`, we\n> allocate the name of the helper. We neither end up transferring\n> ownership of the name, nor do we free it. The associated memory thus\n> leaks.\n>\n> Fix this memory leak and mark now-passing tests as leak free.\n\nIt would be more helpful to the readers if this said \"Fix this\nmemory leak by having the helper own the memory, and mark ...\".\n\nNot a huge deal, though.\n\n>  struct helper_data {\n> -\tconst char *name;\n> +\tchar *name;\n>  \tstruct child_process *helper;\n>  \tFILE *out;\n>  \tunsigned fetch : 1,\n> @@ -111,6 +111,7 @@ static void do_take_over(struct transport *transport)\n>  \tdata = (struct helper_data *)transport->data;\n>  \ttransport_take_over(transport, data->helper);\n>  \tfclose(data->out);\n> +\tfree(data->name);\n>  \tfree(data);\n>  }\n\nThe patch is looking good.  Thanks.\n"},{"id":"495436","messageId":"xmqqwmnktocp.fsf@gitster.g","threadId":"61535","inReplyTo":"0e9fa9ca7386f527903887242008b5b0443ada69.1716465556.git.ps@pks.im","subject":"Re: [PATCH 01/20] t: mark a bunch of tests as leak-free","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-23T17:44:22Z","receivedAt":"2024-05-23T17:44:25Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n>   - t4153: Passes since 71c7916053 (apply: plug a leak in apply_data,\n>     2024-04-23).\n>\n>   - t7006: Passes since at least Git v2.40. I did not care to go back\n>     any further than that.\n\nSince the base commit you chose to apply this step to (which is\nunknown to me) and the tip of 'master' today 4365c6fc (The sixth\nbatch, 2024-05-20), we must have reintroduced more leaks.\n\n$ SANITIZE=leak GIT_TEST_PASSING_SANITIZE_LEAK=true \\\n  Meta/Make -j16 --test=4153,7006 test\n\ndies with\n\n    Test Summary Report\n    -------------------\n    t4153-am-resume-override-opts.sh (Wstat: 256 (exited 1) Tests: 5 Failed: 1)\n      Failed test:  2\n      Non-zero exit status: 1\n    t7006-pager.sh                  (Wstat: 256 (exited 1) Tests: 109 Failed: 6)\n      Failed tests:  14, 70-74\n      Non-zero exit status: 1\n\nHere, Meta/Make is a thin wrapper around \"make\", I primarily use it\nfor its --test=only,these,tests feature, which is an opposite of\nGIT_SKIP_TESTS. (Meta/ is a separate checkout of the 'todo' branch\nof this project, that keeps things like whats-cooking.txt and\nmiscellaneous tools I use to manage the project).\n\n"},{"id":"495511","messageId":"ZlA6E69UdqLE_HzH@tanuki","threadId":"61535","inReplyTo":"xmqqle40v5ms.fsf@gitster.g","subject":"Re: [PATCH 00/20] Various memory leak fixes","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-24T06:56:19Z","receivedAt":"2024-05-24T06:56:25Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, May 23, 2024 at 09:45:47AM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > my mind had a couple of minutes where it was roaming, and of course it\n> > immediately searched for and chased down the next rabbit hole. The\n> > result is this patch series which fixes a bunch of leaks all over the\n> > place. There isn't really any structure to the leaks that I did fix --\n> > it's mostly things that I stumbled over. In the end, this series makes\n> > another 56 test suites pass with leak checking enabled, 13 of which have\n> > already been passing without any changes.\n> \n> ... meaning there were coverage gaps?\n\nWell, we never run CI jobs with GIT_TEST_PASSING_SANITIZE_LEAK=check. We\ncould add such a job, but the question is how important it is to notice.\nThe much more important part is to not regress, and that we do check in\nour CI already.\n\nThe only oddball here is newly added tests. It's a bit of a shame that\nthe leak checking is opt-in rather than opt out, as this is the primary\nway for how new tests land that do pass but aren't marked accordingly.\nAlso, it may raise more eyebrows during review if a new test suite is\nmarked as failing compared to not being marked as passing.\n\nIdeally, we'd just get over with all the tests that currently fail with\nthe leak sanitizer. And honestly, that doesn't feel out of reach given\nthat you can fix >10% of all non-passing tests in a day or two. With\nthis patch series, we now have more test suites with the leak checking\nenabled rather than disabled. If we continue on this track I could\ncertainly see this happening in a release or two.\n\nBut maybe that's just whishful thinking.\n\n> >   - Patches 16 to 20 have the goal of making git-mv(1) memory leak free.\n> >     I had a very hard time understanding how it tracks memory. I think\n> >     this wasn't only me, or otherwise there wouldn't be calls to\n> >     `UNLEAK()` in there. In any case, I decided to rewrite the arrays to\n> >     use a `struct strvec`, which makes tracking and releasing of memory\n> >     a ton easier.\n> >\n> >     It does come at the cost of more allocations because we may now\n> >     duplicate strings that we didn't before. But I think the tradeoff is\n> >     worth it because the number of strings we may now duplicate is\n> >     bounded by the number of command line arguments anyway.\n> \n> Nice.  I have to admit that \"git mv\" is not one of the best-done\n> code in this project X-<, and improving it with rewriting was long\n> overdue.\n\nWell, the patches only rewrite a very small part of git-mv(1). So while\nthe code may be a bit easier to understand now, I still think that it\nleaves a lot to be desired after the refactoring.\n\nPatrick\n"},{"id":"495512","messageId":"ZlA6GkowIo16qprV@tanuki","threadId":"61535","inReplyTo":"CAPig+cTKrUL1AzY4Y+M130AvH5S=kSTdwo542T_GdFC=6JUu=Q@mail.gmail.com","subject":"Re: [PATCH 16/20] strvec: add functions to replace and remove strings","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-24T06:56:26Z","receivedAt":"2024-05-24T06:56:35Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, May 23, 2024 at 01:09:39PM -0400, Eric Sunshine wrote:\n> On Thu, May 23, 2024 at 8:27 AM Patrick Steinhardt <ps@pks.im> wrote:\n> > Add two functions that allow to replace and remove strings contained in\n> > the strvec. This will be used by a subsequent commit that refactors\n> > git-mv(1).\n> > [...]\n> > Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> > ---\n> > diff --git a/strvec.c b/strvec.c\n> > @@ -56,6 +56,26 @@ void strvec_pushv(struct strvec *array, const char **items)\n> > +const char *strvec_replace(struct strvec *array, size_t idx, const char *replacement)\n> > +{\n> > +       char *to_free;\n> > +       if (idx >= array->nr)\n> > +               BUG(\"index outside of array boundary\");\n> > +       to_free = (char *) array->v[idx];\n> > +       array->v[idx] = xstrdup(replacement);\n> > +       free(to_free);\n> > +       return array->v[idx];\n> > +}\n> \n> The reason you delay calling free() until after xstrdup() is to\n> protect against the case when `replacement` is a substring of the\n> string already stored at `v[idx]`, correct?\n\nYup. The patches for the strvec API have been lying around for quite a\nwhile as I have originally implemented them in a different context. And\nthere I did hit this edge case indeed. It's rather unlikely to happen\noverall, but I think it shouldn't hurt to be prepared.\n\nThere is no coverage of this scenario in this patch series though. Let\nme add a unit test for this.\n\nPatrick\n"},{"id":"495513","messageId":"ZlA6KHZtn4lvyFcI@tanuki","threadId":"61535","inReplyTo":"xmqqwmnktocp.fsf@gitster.g","subject":"Re: [PATCH 01/20] t: mark a bunch of tests as leak-free","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-24T06:56:40Z","receivedAt":"2024-05-24T06:56:44Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, May 23, 2024 at 10:44:22AM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> >   - t4153: Passes since 71c7916053 (apply: plug a leak in apply_data,\n> >     2024-04-23).\n> >\n> >   - t7006: Passes since at least Git v2.40. I did not care to go back\n> >     any further than that.\n> \n> Since the base commit you chose to apply this step to (which is\n> unknown to me) and the tip of 'master' today 4365c6fc (The sixth\n> batch, 2024-05-20), we must have reintroduced more leaks.\n> \n> $ SANITIZE=leak GIT_TEST_PASSING_SANITIZE_LEAK=true \\\n>   Meta/Make -j16 --test=4153,7006 test\n> \n> dies with\n> \n>     Test Summary Report\n>     -------------------\n>     t4153-am-resume-override-opts.sh (Wstat: 256 (exited 1) Tests: 5 Failed: 1)\n>       Failed test:  2\n>       Non-zero exit status: 1\n>     t7006-pager.sh                  (Wstat: 256 (exited 1) Tests: 109 Failed: 6)\n>       Failed tests:  14, 70-74\n>       Non-zero exit status: 1\n> \n> Here, Meta/Make is a thin wrapper around \"make\", I primarily use it\n> for its --test=only,these,tests feature, which is an opposite of\n> GIT_SKIP_TESTS. (Meta/ is a separate checkout of the 'todo' branch\n> of this project, that keeps things like whats-cooking.txt and\n> miscellaneous tools I use to manage the project).\n\nHum. Both of these skip a bunch of tests due to a missing TTY prereq on\nmy system. So I guess it's not a regression, just me missing test\ncoverage. And seemingly, the same applies to our CI systems because the\npipeline is green there.\n\nAnd indeed, the TTY prerequisite fails due a totally unrelated error:\n\n    Can't locate IO/Pty.pm in @INC\n\nI'll fix this locally and in our CI setup. Ideally, we'd also make this\nthing more robust going forward, but I'll leave that for a future\niteraiton.\n\nPatrick\n"},{"id":"495514","messageId":"ZlA6jZmKomOu1p_S@tanuki","threadId":"61535","inReplyTo":"xmqq7cfkv4cr.fsf@gitster.g","subject":"Re: [PATCH 12/20] config: plug various memory leaks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-24T06:58:21Z","receivedAt":"2024-05-24T06:58:25Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, May 23, 2024 at 10:13:24AM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> > diff --git a/t/t4210-log-i18n.sh b/t/t4210-log-i18n.sh\n> > index 75216f19ce..7120030b5c 100755\n> > --- a/t/t4210-log-i18n.sh\n> > +++ b/t/t4210-log-i18n.sh\n> > @@ -1,6 +1,8 @@\n> >  #!/bin/sh\n> >  \n> >  test_description='test log with i18n features'\n> > +\n> > +TEST_PASSES_SANITIZE_LEAK=true\n> >  . ./lib-gettext.sh\n> >  \n> >  # two forms of é\n> \n> You've been sending your patches as \"multipart/signed\" (which by the\n> way was cumbersome enough for me but that is primarily the problem\n> with Emacs/GNUS and we should handle multipart/signed well), whose\n> header reads like so:\n> \n> Content-Type: multipart/signed; micalg=pgp-sha512;\n> \tprotocol=\"application/pgp-signature\"; boundary=\"ovDSRLSkA00eIgbN\"\n\nWell, signing is of dubious value anyway, but it does have the upside of\nstressing our toolchain a bit more and thus making it (hopefully) more\nrobust :) If it bothers you, then I can probably stop doing so. But for\nnow I will keep it as-is.\n\n> The \"message\" part in it says that it is encoded in iso-8859-1:\n> \n>     --ovDSRLSkA00eIgbN\n>     Content-Type: text/plain; charset=iso-8859-1\n>     Content-Disposition: inline\n>     Content-Transfer-Encoding: quoted-printable\n> \n>     ...\n>     diff --git a/t/t4210-log-i18n.sh b/t/t4210-log-i18n.sh\n>     index 75216f19ce..7120030b5c 100755\n>     --- a/t/t4210-log-i18n.sh\n>     +++ b/t/t4210-log-i18n.sh\n>     @@ -1,6 +1,8 @@\n>      #!/bin/sh\n>     =20\n>      test_description=3D'test log with i18n features'\n>     +\n>     +TEST_PASSES_SANITIZE_LEAK=3Dtrue\n>      . ./lib-gettext.sh\n>     =20\n>      # two forms of =E9\n> \n> But the source in t/t4210-log-i18n.sh actually is written in UTF-8.\n> The thing is, the \"-u\" option (recode into utf-8\" is supposed to be\n> the default for \"git am\", and it is passed down to the underlying\n> mailinfo machinery in builtin/am.c:parse_mail().\n> \n> But apparently that is not working correctly.  I see in the patch an\n> unrecoded byte E9 in the resulting patch file that is fed to the\n> underlying \"git apply\" machinery, failing the application.\n\nHm. I'll double check mail headers before sending out the next iteration.\n\nPatrick\n"},{"id":"495524","messageId":"ZlBV-QtqoNX-7m8a@tanuki","threadId":"61535","inReplyTo":"ZlA6jZmKomOu1p_S@tanuki","subject":"Re: [PATCH 12/20] config: plug various memory leaks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-24T08:55:21Z","receivedAt":"2024-05-24T08:55:27Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, May 24, 2024 at 08:58:21AM +0200, Patrick Steinhardt wrote:\n> On Thu, May 23, 2024 at 10:13:24AM -0700, Junio C Hamano wrote:\n> > Patrick Steinhardt <ps@pks.im> writes:\n> > The \"message\" part in it says that it is encoded in iso-8859-1:\n> > \n> >     --ovDSRLSkA00eIgbN\n> >     Content-Type: text/plain; charset=iso-8859-1\n> >     Content-Disposition: inline\n> >     Content-Transfer-Encoding: quoted-printable\n> > \n> >     ...\n> >     diff --git a/t/t4210-log-i18n.sh b/t/t4210-log-i18n.sh\n> >     index 75216f19ce..7120030b5c 100755\n> >     --- a/t/t4210-log-i18n.sh\n> >     +++ b/t/t4210-log-i18n.sh\n> >     @@ -1,6 +1,8 @@\n> >      #!/bin/sh\n> >     =20\n> >      test_description=3D'test log with i18n features'\n> >     +\n> >     +TEST_PASSES_SANITIZE_LEAK=3Dtrue\n> >      . ./lib-gettext.sh\n> >     =20\n> >      # two forms of =E9\n> > \n> > But the source in t/t4210-log-i18n.sh actually is written in UTF-8.\n> > The thing is, the \"-u\" option (recode into utf-8\" is supposed to be\n> > the default for \"git am\", and it is passed down to the underlying\n> > mailinfo machinery in builtin/am.c:parse_mail().\n> > \n> > But apparently that is not working correctly.  I see in the patch an\n> > unrecoded byte E9 in the resulting patch file that is fed to the\n> > underlying \"git apply\" machinery, failing the application.\n> \n> Hm. I'll double check mail headers before sending out the next iteration.\n\nI've changed my mutt configuration to prefer UTF-8 over ISO-8859-1, so\nthe next iteration should hopefully work alright. But as you say, this\nis probably something that needs to be fixed in the git-am(1) machinery.\n\nPatrick\n"},{"id":"495527","messageId":"cover.1716541556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716465556.git.ps@pks.im","subject":"[PATCH v2 00/21] Various memory leak fixes","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-24T10:03:10Z","receivedAt":"2024-05-24T10:03:17Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis is the second version of my patch series that fixes various memory\nleaks in Git. Changes compared to v1:\n\n  - t4153 and t7006 aren't marked as passing anymore. I thought they\n    pass because most of these tests were skipped because of a missing\n    TTY prerequisite both on my local machine, but also in our CI.\n\n  - Add another patch to install the Perl IO:Pty module on Alpine and\n    Ubuntu. This fulfills the TTY prerequisite and thus surfaces the\n    memory leaks in both of the above tests.\n\n  - Add another unit test for strvec that exercise replacing a string in\n    the strvec with a copy of itself.\n\n  - A bunch of commit message improvements.\n\nThanks!\n\nPatrick\n\nPatrick Steinhardt (21):\n  ci: add missing dependency for TTY prereq\n  t: mark a bunch of tests as leak-free\n  transport-helper: fix leaking helper name\n  strbuf: fix leak when `appendwholeline()` fails with EOF\n  checkout: clarify memory ownership in `unique_tracking_name()`\n  http: refactor code to clarify memory ownership\n  config: clarify memory ownership in `git_config_pathname()`\n  diff: refactor code to clarify memory ownership of prefixes\n  convert: refactor code to clarify ownership of\n    check_roundtrip_encoding\n  builtin/log: stop using globals for log config\n  builtin/log: stop using globals for format config\n  config: clarify memory ownership in `git_config_string()`\n  config: plug various memory leaks\n  builtin/credential: clear credential before exit\n  commit-reach: fix memory leak in `ahead_behind()`\n  submodule: fix leaking memory for submodule entries\n  strvec: add functions to replace and remove strings\n  builtin/mv: refactor `add_slash()` to always return allocated strings\n  builtin/mv duplicate string list memory\n  builtin/mv: refactor to use `struct strvec`\n  builtin/mv: fix leaks for submodule gitfile paths\n\n Makefile                                      |   1 +\n alias.c                                       |   6 +-\n attr.c                                        |   2 +-\n attr.h                                        |   2 +-\n builtin/blame.c                               |   2 +-\n builtin/checkout.c                            |  14 +-\n builtin/commit.c                              |   4 +-\n builtin/config.c                              |   2 +-\n builtin/credential.c                          |   2 +\n builtin/log.c                                 | 708 ++++++++++--------\n builtin/merge.c                               |   4 +-\n builtin/mv.c                                  | 222 +++---\n builtin/rebase.c                              |   2 +-\n builtin/receive-pack.c                        |   6 +-\n builtin/repack.c                              |   8 +-\n builtin/worktree.c                            |  20 +-\n checkout.c                                    |   4 +-\n checkout.h                                    |   6 +-\n ci/install-dependencies.sh                    |   4 +-\n commit-reach.c                                |   4 +\n config.c                                      |  52 +-\n config.h                                      |  10 +-\n convert.c                                     |  30 +-\n convert.h                                     |   2 +-\n delta-islands.c                               |   2 +-\n diff.c                                        |  20 +-\n environment.c                                 |  16 +-\n environment.h                                 |  14 +-\n fetch-pack.c                                  |   4 +-\n fsck.c                                        |   4 +-\n fsmonitor-settings.c                          |   5 +-\n gpg-interface.c                               |   6 +-\n http.c                                        |  50 +-\n imap-send.c                                   |  12 +-\n mailmap.c                                     |   4 +-\n mailmap.h                                     |   4 +-\n merge-ll.c                                    |   6 +-\n pager.c                                       |   2 +-\n pretty.c                                      |  14 +-\n promisor-remote.h                             |   2 +-\n remote.c                                      |  20 +-\n remote.h                                      |   8 +-\n sequencer.c                                   |   2 +-\n setup.c                                       |   6 +-\n strbuf.c                                      |   4 +-\n strvec.c                                      |  20 +\n strvec.h                                      |  13 +\n submodule-config.c                            |   2 +\n t/t0300-credentials.sh                        |   2 +\n t/t0411-clone-from-partial.sh                 |   1 +\n t/t0610-reftable-basics.sh                    |   1 +\n t/t0611-reftable-httpd.sh                     |   1 +\n t/t1013-read-tree-submodule.sh                |   1 +\n t/t1306-xdg-files.sh                          |   1 +\n t/t1350-config-hooks-path.sh                  |   1 +\n t/t1400-update-ref.sh                         |   2 +\n t/t2013-checkout-submodule.sh                 |   1 +\n t/t2024-checkout-dwim.sh                      |   1 +\n t/t2060-switch.sh                             |   1 +\n t/t2405-worktree-submodule.sh                 |   1 +\n t/t3007-ls-files-recurse-submodules.sh        |   1 +\n t/t3203-branch-output.sh                      |   2 +\n t/t3415-rebase-autosquash.sh                  |   1 +\n t/t3426-rebase-submodule.sh                   |   1 +\n t/t3512-cherry-pick-submodule.sh              |   1 +\n t/t3513-revert-submodule.sh                   |   1 +\n t/t3600-rm.sh                                 |   1 +\n t/t3906-stash-submodule.sh                    |   1 +\n t/t4001-diff-rename.sh                        |   4 +-\n t/t4041-diff-submodule-option.sh              |   1 +\n t/t4043-diff-rename-binary.sh                 |   1 +\n t/t4059-diff-submodule-not-initialized.sh     |   1 +\n t/t4060-diff-submodule-option-diff-format.sh  |   1 +\n t/t4120-apply-popt.sh                         |   1 +\n t/t4137-apply-submodule.sh                    |   1 +\n t/t4210-log-i18n.sh                           |   2 +\n t/t5563-simple-http-auth.sh                   |   1 +\n t/t5564-http-proxy.sh                         |   1 +\n t/t5581-http-curl-verbose.sh                  |   1 +\n t/t6006-rev-list-format.sh                    |   1 +\n t/t6041-bisect-submodule.sh                   |   1 +\n t/t6400-merge-df.sh                           |   1 +\n t/t6412-merge-large-rename.sh                 |   1 +\n t/t6426-merge-skip-unneeded-updates.sh        |   1 +\n t/t6429-merge-sequence-rename-caching.sh      |   1 +\n t/t6438-submodule-directory-file-conflicts.sh |   1 +\n t/t7001-mv.sh                                 |   2 +\n t/t7005-editor.sh                             |   1 +\n t/t7102-reset.sh                              |   1 +\n t/t7112-reset-submodule.sh                    |   1 +\n t/t7417-submodule-path-url.sh                 |   1 +\n t/t7421-submodule-summary-add.sh              |   1 +\n t/t7423-submodule-symlinks.sh                 |   1 +\n t/t9129-git-svn-i18n-commitencoding.sh        |   1 -\n t/t9139-git-svn-non-utf8-commitencoding.sh    |   1 -\n t/t9200-git-cvsexportcommit.sh                |   1 +\n t/t9401-git-cvsserver-crlf.sh                 |   1 +\n t/t9600-cvsimport.sh                          |   1 +\n t/t9601-cvsimport-vendor-branch.sh            |   1 +\n t/t9602-cvsimport-branches-tags.sh            |   1 +\n t/t9603-cvsimport-patchsets.sh                |   2 +\n t/t9604-cvsimport-timestamps.sh               |   2 +\n t/unit-tests/t-strvec.c                       | 269 +++++++\n t/unit-tests/test-lib.c                       |  13 +\n t/unit-tests/test-lib.h                       |  13 +\n transport-helper.c                            |   6 +-\n transport.c                                   |   1 +\n upload-pack.c                                 |   2 +-\n userdiff.h                                    |  12 +-\n 109 files changed, 1151 insertions(+), 586 deletions(-)\n create mode 100644 t/unit-tests/t-strvec.c\n\nRange-diff against v1:\n -:  ---------- >  1:  857b8b14ce ci: add missing dependency for TTY prereq\n 1:  0e9fa9ca73 !  2:  ceade7dbba t: mark a bunch of tests as leak-free\n    @@ Commit message\n           - t2405: Passes since 6741e917de (repository: avoid leaking\n             `fsmonitor` data, 2024-04-12).\n     \n    -      - t4153: Passes since 71c7916053 (apply: plug a leak in apply_data,\n    -        2024-04-23).\n    -\n    -      - t7006: Passes since at least Git v2.40. I did not care to go back\n    -        any further than that.\n    -\n           - t7423: Introduced via b20c10fd9b (t7423: add tests for symlinked\n             submodule directories, 2024-01-28), passes since e8d0608944\n             (submodule: require the submodule path to contain directories only,\n    @@ t/t2405-worktree-submodule.sh: test_description='Combination of submodules and m\n      \n      base_path=$(pwd -P)\n     \n    - ## t/t4153-am-resume-override-opts.sh ##\n    -@@\n    - \n    - test_description='git-am command-line options override saved options'\n    - \n    -+TEST_PASSES_SANITIZE_LEAK=true\n    - . ./test-lib.sh\n    - . \"$TEST_DIRECTORY\"/lib-terminal.sh\n    - \n    -\n    - ## t/t7006-pager.sh ##\n    -@@\n    - \n    - test_description='Test automatic use of a pager.'\n    - \n    -+TEST_PASSES_SANITIZE_LEAK=true\n    - . ./test-lib.sh\n    - . \"$TEST_DIRECTORY\"/lib-pager.sh\n    - . \"$TEST_DIRECTORY\"/lib-terminal.sh\n    -\n      ## t/t7423-submodule-symlinks.sh ##\n     @@\n      \n 2:  05fbadbae2 !  3:  a96b5ac359 transport-helper: fix leaking helper name\n    @@ Commit message\n         ownership of the name, nor do we free it. The associated memory thus\n         leaks.\n     \n    -    Fix this memory leak and mark now-passing tests as leak free.\n    +    Fix this memory leak by freeing the string at the calling side in\n    +    `transport_get()`. `transport_helper_init()` now creates its own copy of\n    +    the string and thus can free it as required.\n    +\n    +    An alterantive way to fix this would be to transfer ownership of the\n    +    string passed into `transport_helper_init()`, which would avoid the call\n    +    to xstrdup(1). But it does make for a more surprising calling convention\n    +    as we do not typically transfer ownership of strings like this.\n    +\n    +    Mark now-passing tests as leak free.\n     \n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n 3:  d76079797f =  4:  9dd8709d1b strbuf: fix leak when `appendwholeline()` fails with EOF\n 4:  ffd9eb795f =  5:  6d4e9ce706 checkout: clarify memory ownership in `unique_tracking_name()`\n 5:  d4bf3c1f95 =  6:  141cae2de1 http: refactor code to clarify memory ownership\n 6:  1702762772 =  7:  ff5e761e55 config: clarify memory ownership in `git_config_pathname()`\n 7:  18dce492df !  8:  afe69c7303 diff: refactor code to clarify memory ownership of prefixes\n    @@ Metadata\n      ## Commit message ##\n         diff: refactor code to clarify memory ownership of prefixes\n     \n    -    The source end destination prefixes are tracked in a `const char *`\n    +    The source and destination prefixes are tracked in a `const char *`\n         array, but may at times contain allocated strings. The result is that\n         those strings may be leaking because we never free them.\n     \n 8:  667eb3f8ff =  9:  eb7fce55b0 convert: refactor code to clarify ownership of check_roundtrip_encoding\n 9:  11eed8cea7 = 10:  ee2fcf388c builtin/log: stop using globals for log config\n10:  d8cd9a05f8 = 11:  3490ad3a02 builtin/log: stop using globals for format config\n11:  a857637e61 = 12:  6cfc28c7e2 config: clarify memory ownership in `git_config_string()`\n12:  b2f8878b55 = 13:  70e8e26513 config: plug various memory leaks\n13:  23e2cf98b7 = 14:  f1a1c43e76 builtin/credential: clear credential before exit\n14:  a11ce6a0ed = 15:  64b92156f8 commit-reach: fix memory leak in `ahead_behind()`\n15:  24362604b2 = 16:  cd8a992f08 submodule: fix leaking memory for submodule entries\n16:  c43c93db3b ! 17:  128e2eaf7a strvec: add functions to replace and remove strings\n    @@ t/unit-tests/t-strvec.c (new)\n     +\tstrvec_clear(&vec);\n     +}\n     +\n    ++static void t_replace_with_substring(void)\n    ++{\n    ++\tstruct strvec vec = STRVEC_INIT;\n    ++\tstrvec_pushl(&vec, \"foo\", NULL);\n    ++\tstrvec_replace(&vec, 0, vec.v[0] + 1);\n    ++\tcheck_strvec(&vec, \"oo\", NULL);\n    ++\tstrvec_clear(&vec);\n    ++}\n    ++\n     +static void t_remove_at_head(void)\n     +{\n     +\tstruct strvec vec = STRVEC_INIT;\n    @@ t/unit-tests/t-strvec.c (new)\n     +\tTEST(t_replace_at_head(), \"replace at head\");\n     +\tTEST(t_replace_in_between(), \"replace in between\");\n     +\tTEST(t_replace_at_tail(), \"replace at tail\");\n    ++\tTEST(t_replace_with_substring(), \"replace with substring\");\n     +\tTEST(t_remove_at_head(), \"remove at head\");\n     +\tTEST(t_remove_in_between(), \"remove in between\");\n     +\tTEST(t_remove_at_tail(), \"remove at tail\");\n17:  97470398ad = 18:  1310b24fc2 builtin/mv: refactor `add_slash()` to always return allocated strings\n18:  7a2e5e82cc = 19:  d4fef9825a builtin/mv duplicate string list memory\n19:  b546ca4d62 = 20:  797cdb286a builtin/mv: refactor to use `struct strvec`\n20:  bba735388d = 21:  095469193c builtin/mv: fix leaks for submodule gitfile paths\n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495528","messageId":"857b8b14ce519e3c473f1808da7daca95ebff8ea.1716541556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716541556.git.ps@pks.im","subject":"[PATCH v2 01/21] ci: add missing dependency for TTY prereq","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-24T10:03:15Z","receivedAt":"2024-05-24T10:03:26Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"In \"t/lib-terminal.sh\", we declare a lazy prerequisite for tests that\nrequire a TTY. The prerequisite uses a Perl script to figure out whether\nwe do have a usable TTY or not and thus implicitly depends on the PERL\nprerequisite, as well. Furthermore though, the script requires another\ndependency that is easy to miss, namely on the IO::Pty module. If that\nmodule is not installed, then the script will exit early due to an\nreason unrelated to missing TTYs.\n\nThis easily leads to missing test coverage. But most importantly, our CI\nsystems are missing this dependency and thus don't execute those tests\nat all. Fix this.\n---\n ci/install-dependencies.sh | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/ci/install-dependencies.sh b/ci/install-dependencies.sh\nindex 2e7688ae8b..6ec0f85972 100755\n--- a/ci/install-dependencies.sh\n+++ b/ci/install-dependencies.sh\n@@ -27,7 +27,7 @@ alpine-*)\n \tapk add --update shadow sudo build-base curl-dev openssl-dev expat-dev gettext \\\n \t\tpcre2-dev python3 musl-libintl perl-utils ncurses \\\n \t\tapache2 apache2-http2 apache2-proxy apache2-ssl apache2-webdav apr-util-dbd_sqlite3 \\\n-\t\tbash cvs gnupg perl-cgi perl-dbd-sqlite >/dev/null\n+\t\tbash cvs gnupg perl-cgi perl-dbd-sqlite perl-io-tty >/dev/null\n \t;;\n fedora-*)\n \tdnf -yq update >/dev/null &&\n@@ -42,7 +42,7 @@ ubuntu-*)\n \t\tlanguage-pack-is libsvn-perl apache2 cvs cvsps git gnupg subversion \\\n \t\tmake libssl-dev libcurl4-openssl-dev libexpat-dev wget sudo default-jre \\\n \t\ttcl tk gettext zlib1g-dev perl-modules liberror-perl libauthen-sasl-perl \\\n-\t\tlibemail-valid-perl libio-socket-ssl-perl libnet-smtp-ssl-perl libdbd-sqlite3-perl libcgi-pm-perl \\\n+\t\tlibemail-valid-perl libio-pty-perl libio-socket-ssl-perl libnet-smtp-ssl-perl libdbd-sqlite3-perl libcgi-pm-perl \\\n \t\t${CC_PACKAGE:-${CC:-gcc}} $PYTHON_PACKAGE\n \n \tmkdir --parents \"$CUSTOM_PATH\"\n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495529","messageId":"ceade7dbba0ad82bfca979f95a946ba54a16236c.1716541556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716541556.git.ps@pks.im","subject":"[PATCH v2 02/21] t: mark a bunch of tests as leak-free","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-24T10:03:19Z","receivedAt":"2024-05-24T10:03:27Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"There are a bunch of tests which do not have any leaks:\n\n  - t0411: Introduced via 5c5a4a1c05 (t0411: add tests for cloning from\n    partial repo, 2024-01-28), passes since its inception.\n\n  - t0610: Introduced via 57db2a094d (refs: introduce reftable backend,\n    2024-02-07), passes since its inception.\n\n  - t2405: Passes since 6741e917de (repository: avoid leaking\n    `fsmonitor` data, 2024-04-12).\n\n  - t7423: Introduced via b20c10fd9b (t7423: add tests for symlinked\n    submodule directories, 2024-01-28), passes since e8d0608944\n    (submodule: require the submodule path to contain directories only,\n    2024-03-26). The fix is not ovbiously related, but probably works\n    because we now die early in many code paths.\n\n  - t9xxx: All of these are exercising CVS-related tooling and pass\n    since at least Git v2.40. It's likely that these pass for a long\n    time already, but nobody ever noticed because noone has CVS on their\n    machine.\n\nMark all of these tests as passing.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t0411-clone-from-partial.sh      | 1 +\n t/t0610-reftable-basics.sh         | 1 +\n t/t2405-worktree-submodule.sh      | 1 +\n t/t7423-submodule-symlinks.sh      | 1 +\n t/t9200-git-cvsexportcommit.sh     | 1 +\n t/t9401-git-cvsserver-crlf.sh      | 1 +\n t/t9600-cvsimport.sh               | 1 +\n t/t9601-cvsimport-vendor-branch.sh | 1 +\n t/t9602-cvsimport-branches-tags.sh | 1 +\n t/t9603-cvsimport-patchsets.sh     | 2 ++\n t/t9604-cvsimport-timestamps.sh    | 2 ++\n 11 files changed, 13 insertions(+)\n\ndiff --git a/t/t0411-clone-from-partial.sh b/t/t0411-clone-from-partial.sh\nindex c98d501869..932bf2067d 100755\n--- a/t/t0411-clone-from-partial.sh\n+++ b/t/t0411-clone-from-partial.sh\n@@ -2,6 +2,7 @@\n \n test_description='check that local clone does not fetch from promisor remotes'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n test_expect_success 'create evil repo' '\ndiff --git a/t/t0610-reftable-basics.sh b/t/t0610-reftable-basics.sh\nindex cc5bbfd732..b06c46999d 100755\n--- a/t/t0610-reftable-basics.sh\n+++ b/t/t0610-reftable-basics.sh\n@@ -10,6 +10,7 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n GIT_TEST_DEFAULT_REF_FORMAT=reftable\n export GIT_TEST_DEFAULT_REF_FORMAT\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n INVALID_OID=$(test_oid 001)\ndiff --git a/t/t2405-worktree-submodule.sh b/t/t2405-worktree-submodule.sh\nindex 11018f37c7..1d7f605633 100755\n--- a/t/t2405-worktree-submodule.sh\n+++ b/t/t2405-worktree-submodule.sh\n@@ -5,6 +5,7 @@ test_description='Combination of submodules and multiple worktrees'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n base_path=$(pwd -P)\ndiff --git a/t/t7423-submodule-symlinks.sh b/t/t7423-submodule-symlinks.sh\nindex 3d3c7af3ce..f45d806201 100755\n--- a/t/t7423-submodule-symlinks.sh\n+++ b/t/t7423-submodule-symlinks.sh\n@@ -2,6 +2,7 @@\n \n test_description='check that submodule operations do not follow symlinks'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n test_expect_success 'prepare' '\ndiff --git a/t/t9200-git-cvsexportcommit.sh b/t/t9200-git-cvsexportcommit.sh\nindex a44eabf0d8..3d4842164c 100755\n--- a/t/t9200-git-cvsexportcommit.sh\n+++ b/t/t9200-git-cvsexportcommit.sh\n@@ -4,6 +4,7 @@\n #\n test_description='Test export of commits to CVS'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n if ! test_have_prereq PERL; then\ndiff --git a/t/t9401-git-cvsserver-crlf.sh b/t/t9401-git-cvsserver-crlf.sh\nindex a34805acdc..a67e6abd49 100755\n--- a/t/t9401-git-cvsserver-crlf.sh\n+++ b/t/t9401-git-cvsserver-crlf.sh\n@@ -12,6 +12,7 @@ repository using cvs CLI client via git-cvsserver server'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n marked_as () {\ndiff --git a/t/t9600-cvsimport.sh b/t/t9600-cvsimport.sh\nindex 5680849218..41fcf3606b 100755\n--- a/t/t9600-cvsimport.sh\n+++ b/t/t9600-cvsimport.sh\n@@ -4,6 +4,7 @@ test_description='git cvsimport basic tests'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./lib-cvs.sh\n \n if ! test_have_prereq NOT_ROOT; then\ndiff --git a/t/t9601-cvsimport-vendor-branch.sh b/t/t9601-cvsimport-vendor-branch.sh\nindex 116cddba3a..e007669495 100755\n--- a/t/t9601-cvsimport-vendor-branch.sh\n+++ b/t/t9601-cvsimport-vendor-branch.sh\n@@ -35,6 +35,7 @@ test_description='git cvsimport handling of vendor branches'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./lib-cvs.sh\n \n setup_cvs_test_repository t9601\ndiff --git a/t/t9602-cvsimport-branches-tags.sh b/t/t9602-cvsimport-branches-tags.sh\nindex e5266c9a87..3768e3bd8c 100755\n--- a/t/t9602-cvsimport-branches-tags.sh\n+++ b/t/t9602-cvsimport-branches-tags.sh\n@@ -7,6 +7,7 @@ test_description='git cvsimport handling of branches and tags'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./lib-cvs.sh\n \n setup_cvs_test_repository t9602\ndiff --git a/t/t9603-cvsimport-patchsets.sh b/t/t9603-cvsimport-patchsets.sh\nindex 19f38f78f2..2a387fdbaa 100755\n--- a/t/t9603-cvsimport-patchsets.sh\n+++ b/t/t9603-cvsimport-patchsets.sh\n@@ -12,6 +12,8 @@\n # bug.\n \n test_description='git cvsimport testing for correct patchset estimation'\n+\n+TEST_PASSES_SANITIZE_LEAK=true\n . ./lib-cvs.sh\n \n setup_cvs_test_repository t9603\ndiff --git a/t/t9604-cvsimport-timestamps.sh b/t/t9604-cvsimport-timestamps.sh\nindex 2d03259729..9cf0685d56 100755\n--- a/t/t9604-cvsimport-timestamps.sh\n+++ b/t/t9604-cvsimport-timestamps.sh\n@@ -1,6 +1,8 @@\n #!/bin/sh\n \n test_description='git cvsimport timestamps'\n+\n+TEST_PASSES_SANITIZE_LEAK=true\n . ./lib-cvs.sh\n \n test_lazy_prereq POSIX_TIMEZONE '\n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495530","messageId":"a96b5ac359bae75ffe30f9549a231c971d883e44.1716541556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716541556.git.ps@pks.im","subject":"[PATCH v2 03/21] transport-helper: fix leaking helper name","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-24T10:03:24Z","receivedAt":"2024-05-24T10:03:28Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When initializing the transport helper in `transport_get()`, we\nallocate the name of the helper. We neither end up transferring\nownership of the name, nor do we free it. The associated memory thus\nleaks.\n\nFix this memory leak by freeing the string at the calling side in\n`transport_get()`. `transport_helper_init()` now creates its own copy of\nthe string and thus can free it as required.\n\nAn alterantive way to fix this would be to transfer ownership of the\nstring passed into `transport_helper_init()`, which would avoid the call\nto xstrdup(1). But it does make for a more surprising calling convention\nas we do not typically transfer ownership of strings like this.\n\nMark now-passing tests as leak free.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t0611-reftable-httpd.sh    | 1 +\n t/t5563-simple-http-auth.sh  | 1 +\n t/t5564-http-proxy.sh        | 1 +\n t/t5581-http-curl-verbose.sh | 1 +\n transport-helper.c           | 6 ++++--\n transport.c                  | 1 +\n 6 files changed, 9 insertions(+), 2 deletions(-)\n\ndiff --git a/t/t0611-reftable-httpd.sh b/t/t0611-reftable-httpd.sh\nindex 5e05b9c1f2..2805995cc8 100755\n--- a/t/t0611-reftable-httpd.sh\n+++ b/t/t0611-reftable-httpd.sh\n@@ -2,6 +2,7 @@\n \n test_description='reftable HTTPD tests'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-httpd.sh\n \ndiff --git a/t/t5563-simple-http-auth.sh b/t/t5563-simple-http-auth.sh\nindex 5d5caa3f58..4af796de67 100755\n--- a/t/t5563-simple-http-auth.sh\n+++ b/t/t5563-simple-http-auth.sh\n@@ -2,6 +2,7 @@\n \n test_description='test http auth header and credential helper interop'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-httpd.sh\n \ndiff --git a/t/t5564-http-proxy.sh b/t/t5564-http-proxy.sh\nindex 9da5134614..bb35b87071 100755\n--- a/t/t5564-http-proxy.sh\n+++ b/t/t5564-http-proxy.sh\n@@ -2,6 +2,7 @@\n \n test_description=\"test fetching through http proxy\"\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-httpd.sh\n \ndiff --git a/t/t5581-http-curl-verbose.sh b/t/t5581-http-curl-verbose.sh\nindex cded79c16b..724f610054 100755\n--- a/t/t5581-http-curl-verbose.sh\n+++ b/t/t5581-http-curl-verbose.sh\n@@ -4,6 +4,7 @@ test_description='test GIT_CURL_VERBOSE'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-httpd.sh\n start_httpd\ndiff --git a/transport-helper.c b/transport-helper.c\nindex 780fcaf529..9820947ab2 100644\n--- a/transport-helper.c\n+++ b/transport-helper.c\n@@ -22,7 +22,7 @@\n static int debug;\n \n struct helper_data {\n-\tconst char *name;\n+\tchar *name;\n \tstruct child_process *helper;\n \tFILE *out;\n \tunsigned fetch : 1,\n@@ -111,6 +111,7 @@ static void do_take_over(struct transport *transport)\n \tdata = (struct helper_data *)transport->data;\n \ttransport_take_over(transport, data->helper);\n \tfclose(data->out);\n+\tfree(data->name);\n \tfree(data);\n }\n \n@@ -253,6 +254,7 @@ static int disconnect_helper(struct transport *transport)\n \t\tclose(data->helper->out);\n \t\tfclose(data->out);\n \t\tres = finish_command(data->helper);\n+\t\tFREE_AND_NULL(data->name);\n \t\tFREE_AND_NULL(data->helper);\n \t}\n \treturn res;\n@@ -1297,7 +1299,7 @@ static struct transport_vtable vtable = {\n int transport_helper_init(struct transport *transport, const char *name)\n {\n \tstruct helper_data *data = xcalloc(1, sizeof(*data));\n-\tdata->name = name;\n+\tdata->name = xstrdup(name);\n \n \ttransport_check_allowed(name);\n \ndiff --git a/transport.c b/transport.c\nindex 0ad04b77fd..83ddea8fbc 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -1176,6 +1176,7 @@ struct transport *transport_get(struct remote *remote, const char *url)\n \t\tint len = external_specification_len(url);\n \t\tchar *handler = xmemdupz(url, len);\n \t\ttransport_helper_init(ret, handler);\n+\t\tfree(handler);\n \t}\n \n \tif (ret->smart_options) {\n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495531","messageId":"9dd8709d1b3b350008218133986befdb2ae74bae.1716541556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716541556.git.ps@pks.im","subject":"[PATCH v2 04/21] strbuf: fix leak when `appendwholeline()` fails with EOF","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-24T10:03:29Z","receivedAt":"2024-05-24T10:03:33Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"In `strbuf_appendwholeline()` we call `strbuf_getwholeline()` with a\ntemporary buffer. In case the call returns an error we indicate this by\nreturning EOF, but never release the temporary buffer. This can lead to\na memory leak when the line has been partially filled. Fix this.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n strbuf.c              | 4 +++-\n t/t1400-update-ref.sh | 2 ++\n 2 files changed, 5 insertions(+), 1 deletion(-)\n\ndiff --git a/strbuf.c b/strbuf.c\nindex 0d929e4e19..e1076c9891 100644\n--- a/strbuf.c\n+++ b/strbuf.c\n@@ -691,8 +691,10 @@ int strbuf_getwholeline(struct strbuf *sb, FILE *fp, int term)\n int strbuf_appendwholeline(struct strbuf *sb, FILE *fp, int term)\n {\n \tstruct strbuf line = STRBUF_INIT;\n-\tif (strbuf_getwholeline(&line, fp, term))\n+\tif (strbuf_getwholeline(&line, fp, term)) {\n+\t\tstrbuf_release(&line);\n \t\treturn EOF;\n+\t}\n \tstrbuf_addbuf(sb, &line);\n \tstrbuf_release(&line);\n \treturn 0;\ndiff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh\nindex ec3443cc87..bbee2783ab 100755\n--- a/t/t1400-update-ref.sh\n+++ b/t/t1400-update-ref.sh\n@@ -4,6 +4,8 @@\n #\n \n test_description='Test git update-ref and basic ref logging'\n+\n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n Z=$ZERO_OID\n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495532","messageId":"6d4e9ce706b84ff23aa30b60ba7d849dc8c9cee3.1716541556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716541556.git.ps@pks.im","subject":"[PATCH v2 05/21] checkout: clarify memory ownership in `unique_tracking_name()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-24T10:03:34Z","receivedAt":"2024-05-24T10:03:37Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The function `unique_tracking_name()` returns an allocated string, but\ndoes not clearly indicate this because its return type is `const char *`\ninstead of `char *`. This has led to various callsites where we never\nfree its returned memory at all, which causes memory leaks.\n\nPlug those leaks and mark now-passing tests as leak free.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/checkout.c                            | 14 +++++++------\n builtin/worktree.c                            | 20 ++++++++++---------\n checkout.c                                    |  4 ++--\n checkout.h                                    |  6 +++---\n t/t2024-checkout-dwim.sh                      |  1 +\n t/t2060-switch.sh                             |  1 +\n t/t3426-rebase-submodule.sh                   |  1 +\n t/t3512-cherry-pick-submodule.sh              |  1 +\n t/t3513-revert-submodule.sh                   |  1 +\n t/t3600-rm.sh                                 |  1 +\n t/t3906-stash-submodule.sh                    |  1 +\n t/t4137-apply-submodule.sh                    |  1 +\n t/t6041-bisect-submodule.sh                   |  1 +\n t/t6438-submodule-directory-file-conflicts.sh |  1 +\n 14 files changed, 34 insertions(+), 20 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex f90a4ca4b7..3cf44b4683 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -1275,12 +1275,12 @@ static void setup_new_branch_info_and_source_tree(\n \t}\n }\n \n-static const char *parse_remote_branch(const char *arg,\n-\t\t\t\t       struct object_id *rev,\n-\t\t\t\t       int could_be_checkout_paths)\n+static char *parse_remote_branch(const char *arg,\n+\t\t\t\t struct object_id *rev,\n+\t\t\t\t int could_be_checkout_paths)\n {\n \tint num_matches = 0;\n-\tconst char *remote = unique_tracking_name(arg, rev, &num_matches);\n+\tchar *remote = unique_tracking_name(arg, rev, &num_matches);\n \n \tif (remote && could_be_checkout_paths) {\n \t\tdie(_(\"'%s' could be both a local file and a tracking branch.\\n\"\n@@ -1316,6 +1316,7 @@ static int parse_branchname_arg(int argc, const char **argv,\n \tconst char **new_branch = &opts->new_branch;\n \tint argcount = 0;\n \tconst char *arg;\n+\tchar *remote = NULL;\n \tint dash_dash_pos;\n \tint has_dash_dash = 0;\n \tint i;\n@@ -1416,8 +1417,8 @@ static int parse_branchname_arg(int argc, const char **argv,\n \t\t\trecover_with_dwim = 0;\n \n \t\tif (recover_with_dwim) {\n-\t\t\tconst char *remote = parse_remote_branch(arg, rev,\n-\t\t\t\t\t\t\t\t could_be_checkout_paths);\n+\t\t\tremote = parse_remote_branch(arg, rev,\n+\t\t\t\t\t\t     could_be_checkout_paths);\n \t\t\tif (remote) {\n \t\t\t\t*new_branch = arg;\n \t\t\t\targ = remote;\n@@ -1459,6 +1460,7 @@ static int parse_branchname_arg(int argc, const char **argv,\n \t\targc--;\n \t}\n \n+\tfree(remote);\n \treturn argcount;\n }\n \ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 7e0868df72..937da6c0ee 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -736,16 +736,14 @@ static int dwim_orphan(const struct add_opts *opts, int opt_track, int remote)\n \treturn 1;\n }\n \n-static const char *dwim_branch(const char *path, const char **new_branch)\n+static char *dwim_branch(const char *path, char **new_branch)\n {\n \tint n;\n \tint branch_exists;\n \tconst char *s = worktree_basename(path, &n);\n-\tconst char *branchname = xstrndup(s, n);\n+\tchar *branchname = xstrndup(s, n);\n \tstruct strbuf ref = STRBUF_INIT;\n \n-\tUNLEAK(branchname);\n-\n \tbranch_exists = !strbuf_check_branch_ref(&ref, branchname) &&\n \t\t\trefs_ref_exists(get_main_ref_store(the_repository),\n \t\t\t\t\tref.buf);\n@@ -756,8 +754,7 @@ static const char *dwim_branch(const char *path, const char **new_branch)\n \t*new_branch = branchname;\n \tif (guess_remote) {\n \t\tstruct object_id oid;\n-\t\tconst char *remote =\n-\t\t\tunique_tracking_name(*new_branch, &oid, NULL);\n+\t\tchar *remote = unique_tracking_name(*new_branch, &oid, NULL);\n \t\treturn remote;\n \t}\n \treturn NULL;\n@@ -769,6 +766,8 @@ static int add(int ac, const char **av, const char *prefix)\n \tconst char *new_branch_force = NULL;\n \tchar *path;\n \tconst char *branch;\n+\tchar *branch_to_free = NULL;\n+\tchar *new_branch_to_free = NULL;\n \tconst char *new_branch = NULL;\n \tconst char *opt_track = NULL;\n \tconst char *lock_reason = NULL;\n@@ -859,16 +858,17 @@ static int add(int ac, const char **av, const char *prefix)\n \t\topts.orphan = dwim_orphan(&opts, !!opt_track, 0);\n \t} else if (ac < 2) {\n \t\t/* DWIM: Guess branch name from path. */\n-\t\tconst char *s = dwim_branch(path, &new_branch);\n+\t\tchar *s = dwim_branch(path, &new_branch_to_free);\n \t\tif (s)\n-\t\t\tbranch = s;\n+\t\t\tbranch = branch_to_free = s;\n+\t\tnew_branch = new_branch_to_free;\n \n \t\t/* DWIM: Infer --orphan when repo has no refs. */\n \t\topts.orphan = (!s) && dwim_orphan(&opts, !!opt_track, 1);\n \t} else if (ac == 2) {\n \t\tstruct object_id oid;\n \t\tstruct commit *commit;\n-\t\tconst char *remote;\n+\t\tchar *remote;\n \n \t\tcommit = lookup_commit_reference_by_name(branch);\n \t\tif (!commit) {\n@@ -923,6 +923,8 @@ static int add(int ac, const char **av, const char *prefix)\n \n \tret = add_worktree(path, branch, &opts);\n \tfree(path);\n+\tfree(branch_to_free);\n+\tfree(new_branch_to_free);\n \treturn ret;\n }\n \ndiff --git a/checkout.c b/checkout.c\nindex 4256e71a7c..cfaea4bd10 100644\n--- a/checkout.c\n+++ b/checkout.c\n@@ -45,8 +45,8 @@ static int check_tracking_name(struct remote *remote, void *cb_data)\n \treturn 0;\n }\n \n-const char *unique_tracking_name(const char *name, struct object_id *oid,\n-\t\t\t\t int *dwim_remotes_matched)\n+char *unique_tracking_name(const char *name, struct object_id *oid,\n+\t\t\t   int *dwim_remotes_matched)\n {\n \tstruct tracking_name_data cb_data = TRACKING_NAME_DATA_INIT;\n \tconst char *default_remote = NULL;\ndiff --git a/checkout.h b/checkout.h\nindex 3c514a5ab4..ba15a13fb3 100644\n--- a/checkout.h\n+++ b/checkout.h\n@@ -8,8 +8,8 @@\n  * tracking branch.  Return the name of the remote if such a branch\n  * exists, NULL otherwise.\n  */\n-const char *unique_tracking_name(const char *name,\n-\t\t\t\t struct object_id *oid,\n-\t\t\t\t int *dwim_remotes_matched);\n+char *unique_tracking_name(const char *name,\n+\t\t\t   struct object_id *oid,\n+\t\t\t   int *dwim_remotes_matched);\n \n #endif /* CHECKOUT_H */\ndiff --git a/t/t2024-checkout-dwim.sh b/t/t2024-checkout-dwim.sh\nindex a3b1449ef1..2caada3d83 100755\n--- a/t/t2024-checkout-dwim.sh\n+++ b/t/t2024-checkout-dwim.sh\n@@ -4,6 +4,7 @@ test_description='checkout <branch>\n \n Ensures that checkout on an unborn branch does what the user expects'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n # Is the current branch \"refs/heads/$1\"?\ndiff --git a/t/t2060-switch.sh b/t/t2060-switch.sh\nindex c91c4db936..77b2346291 100755\n--- a/t/t2060-switch.sh\n+++ b/t/t2060-switch.sh\n@@ -5,6 +5,7 @@ test_description='switch basic functionality'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n test_expect_success 'setup' '\ndiff --git a/t/t3426-rebase-submodule.sh b/t/t3426-rebase-submodule.sh\nindex ba069dccbd..94ea88e384 100755\n--- a/t/t3426-rebase-submodule.sh\n+++ b/t/t3426-rebase-submodule.sh\n@@ -2,6 +2,7 @@\n \n test_description='rebase can handle submodules'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-submodule-update.sh\n . \"$TEST_DIRECTORY\"/lib-rebase.sh\ndiff --git a/t/t3512-cherry-pick-submodule.sh b/t/t3512-cherry-pick-submodule.sh\nindex f22d1ddead..9387a22a9e 100755\n--- a/t/t3512-cherry-pick-submodule.sh\n+++ b/t/t3512-cherry-pick-submodule.sh\n@@ -5,6 +5,7 @@ test_description='cherry-pick can handle submodules'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-submodule-update.sh\n \ndiff --git a/t/t3513-revert-submodule.sh b/t/t3513-revert-submodule.sh\nindex 8bfe3ed246..e178968b40 100755\n--- a/t/t3513-revert-submodule.sh\n+++ b/t/t3513-revert-submodule.sh\n@@ -2,6 +2,7 @@\n \n test_description='revert can handle submodules'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-submodule-update.sh\n \ndiff --git a/t/t3600-rm.sh b/t/t3600-rm.sh\nindex 98259e2ada..31ac31d4bc 100755\n--- a/t/t3600-rm.sh\n+++ b/t/t3600-rm.sh\n@@ -8,6 +8,7 @@ test_description='Test of the various options to git rm.'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n # Setup some files to be removed, some with funny characters\ndiff --git a/t/t3906-stash-submodule.sh b/t/t3906-stash-submodule.sh\nindex 0f7348ec21..0f61f01ef4 100755\n--- a/t/t3906-stash-submodule.sh\n+++ b/t/t3906-stash-submodule.sh\n@@ -2,6 +2,7 @@\n \n test_description='stash can handle submodules'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-submodule-update.sh\n \ndiff --git a/t/t4137-apply-submodule.sh b/t/t4137-apply-submodule.sh\nindex 07d5262537..ebd0d4ad17 100755\n--- a/t/t4137-apply-submodule.sh\n+++ b/t/t4137-apply-submodule.sh\n@@ -2,6 +2,7 @@\n \n test_description='git apply handling submodules'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-submodule-update.sh\n \ndiff --git a/t/t6041-bisect-submodule.sh b/t/t6041-bisect-submodule.sh\nindex 82013fc903..3946e18089 100755\n--- a/t/t6041-bisect-submodule.sh\n+++ b/t/t6041-bisect-submodule.sh\n@@ -2,6 +2,7 @@\n \n test_description='bisect can handle submodules'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-submodule-update.sh\n \ndiff --git a/t/t6438-submodule-directory-file-conflicts.sh b/t/t6438-submodule-directory-file-conflicts.sh\nindex 8df67a0ef9..3594190af8 100755\n--- a/t/t6438-submodule-directory-file-conflicts.sh\n+++ b/t/t6438-submodule-directory-file-conflicts.sh\n@@ -2,6 +2,7 @@\n \n test_description='merge can handle submodules'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-submodule-update.sh\n \n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495533","messageId":"141cae2de1acb5a181d9c4f4c2a75dfab6b561af.1716541556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716541556.git.ps@pks.im","subject":"[PATCH v2 06/21] http: refactor code to clarify memory ownership","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-24T10:03:38Z","receivedAt":"2024-05-24T10:03:42Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"There are various variables assigned via `git_config_string()` and\n`git_config_pathname()` which are never free'd. This bug is relatable\nbecause the out parameter of those functions are a `const char **`, even\nthough memory ownership is transferred to the caller.\n\nWe're about to adapt the functions to instead use `char **`. Prepare the\ncode accordingly. Note that the `(const char **)` casts will go away\nonce we have adapted the functions.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n http.c | 62 ++++++++++++++++++++++++++++++----------------------------\n 1 file changed, 32 insertions(+), 30 deletions(-)\n\ndiff --git a/http.c b/http.c\nindex 752c879c1f..db2e2f1d39 100644\n--- a/http.c\n+++ b/http.c\n@@ -39,8 +39,8 @@ char curl_errorstr[CURL_ERROR_SIZE];\n static int curl_ssl_verify = -1;\n static int curl_ssl_try;\n static const char *curl_http_version = NULL;\n-static const char *ssl_cert;\n-static const char *ssl_cert_type;\n+static char *ssl_cert;\n+static char *ssl_cert_type;\n static const char *ssl_cipherlist;\n static const char *ssl_version;\n static struct {\n@@ -59,23 +59,23 @@ static struct {\n \t{ \"tlsv1.3\", CURL_SSLVERSION_TLSv1_3 },\n #endif\n };\n-static const char *ssl_key;\n-static const char *ssl_key_type;\n-static const char *ssl_capath;\n-static const char *curl_no_proxy;\n+static char *ssl_key;\n+static char *ssl_key_type;\n+static char *ssl_capath;\n+static char *curl_no_proxy;\n #ifdef GIT_CURL_HAVE_CURLOPT_PINNEDPUBLICKEY\n static const char *ssl_pinnedkey;\n #endif\n-static const char *ssl_cainfo;\n+static char *ssl_cainfo;\n static long curl_low_speed_limit = -1;\n static long curl_low_speed_time = -1;\n static int curl_ftp_no_epsv;\n-static const char *curl_http_proxy;\n-static const char *http_proxy_authmethod;\n+static char *curl_http_proxy;\n+static char *http_proxy_authmethod;\n \n-static const char *http_proxy_ssl_cert;\n-static const char *http_proxy_ssl_key;\n-static const char *http_proxy_ssl_ca_info;\n+static char *http_proxy_ssl_cert;\n+static char *http_proxy_ssl_key;\n+static char *http_proxy_ssl_ca_info;\n static struct credential proxy_cert_auth = CREDENTIAL_INIT;\n static int proxy_ssl_cert_password_required;\n \n@@ -112,7 +112,7 @@ static const char *curl_cookie_file;\n static int curl_save_cookies;\n struct credential http_auth = CREDENTIAL_INIT;\n static int http_proactive_auth;\n-static const char *user_agent;\n+static char *user_agent;\n static int curl_empty_auth = -1;\n \n enum http_follow_config http_follow_config = HTTP_FOLLOW_INITIAL;\n@@ -381,17 +381,17 @@ static int http_options(const char *var, const char *value,\n \tif (!strcmp(\"http.sslversion\", var))\n \t\treturn git_config_string(&ssl_version, var, value);\n \tif (!strcmp(\"http.sslcert\", var))\n-\t\treturn git_config_pathname(&ssl_cert, var, value);\n+\t\treturn git_config_pathname((const char **)&ssl_cert, var, value);\n \tif (!strcmp(\"http.sslcerttype\", var))\n-\t\treturn git_config_string(&ssl_cert_type, var, value);\n+\t\treturn git_config_string((const char **)&ssl_cert_type, var, value);\n \tif (!strcmp(\"http.sslkey\", var))\n-\t\treturn git_config_pathname(&ssl_key, var, value);\n+\t\treturn git_config_pathname((const char **)&ssl_key, var, value);\n \tif (!strcmp(\"http.sslkeytype\", var))\n-\t\treturn git_config_string(&ssl_key_type, var, value);\n+\t\treturn git_config_string((const char **)&ssl_key_type, var, value);\n \tif (!strcmp(\"http.sslcapath\", var))\n-\t\treturn git_config_pathname(&ssl_capath, var, value);\n+\t\treturn git_config_pathname((const char **)&ssl_capath, var, value);\n \tif (!strcmp(\"http.sslcainfo\", var))\n-\t\treturn git_config_pathname(&ssl_cainfo, var, value);\n+\t\treturn git_config_pathname((const char **)&ssl_cainfo, var, value);\n \tif (!strcmp(\"http.sslcertpasswordprotected\", var)) {\n \t\tssl_cert_password_required = git_config_bool(var, value);\n \t\treturn 0;\n@@ -440,19 +440,19 @@ static int http_options(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \tif (!strcmp(\"http.proxy\", var))\n-\t\treturn git_config_string(&curl_http_proxy, var, value);\n+\t\treturn git_config_string((const char **)&curl_http_proxy, var, value);\n \n \tif (!strcmp(\"http.proxyauthmethod\", var))\n-\t\treturn git_config_string(&http_proxy_authmethod, var, value);\n+\t\treturn git_config_string((const char **)&http_proxy_authmethod, var, value);\n \n \tif (!strcmp(\"http.proxysslcert\", var))\n-\t\treturn git_config_string(&http_proxy_ssl_cert, var, value);\n+\t\treturn git_config_string((const char **)&http_proxy_ssl_cert, var, value);\n \n \tif (!strcmp(\"http.proxysslkey\", var))\n-\t\treturn git_config_string(&http_proxy_ssl_key, var, value);\n+\t\treturn git_config_string((const char **)&http_proxy_ssl_key, var, value);\n \n \tif (!strcmp(\"http.proxysslcainfo\", var))\n-\t\treturn git_config_string(&http_proxy_ssl_ca_info, var, value);\n+\t\treturn git_config_string((const char **)&http_proxy_ssl_ca_info, var, value);\n \n \tif (!strcmp(\"http.proxysslcertpasswordprotected\", var)) {\n \t\tproxy_ssl_cert_password_required = git_config_bool(var, value);\n@@ -476,7 +476,7 @@ static int http_options(const char *var, const char *value,\n \t}\n \n \tif (!strcmp(\"http.useragent\", var))\n-\t\treturn git_config_string(&user_agent, var, value);\n+\t\treturn git_config_string((const char **)&user_agent, var, value);\n \n \tif (!strcmp(\"http.emptyauth\", var)) {\n \t\tif (value && !strcmp(\"auto\", value))\n@@ -592,10 +592,10 @@ static void init_curl_http_auth(CURL *result)\n }\n \n /* *var must be free-able */\n-static void var_override(const char **var, char *value)\n+static void var_override(char **var, char *value)\n {\n \tif (value) {\n-\t\tfree((void *)*var);\n+\t\tfree(*var);\n \t\t*var = xstrdup(value);\n \t}\n }\n@@ -1233,11 +1233,13 @@ static CURL *get_curl_handle(void)\n \treturn result;\n }\n \n-static void set_from_env(const char **var, const char *envname)\n+static void set_from_env(char **var, const char *envname)\n {\n \tconst char *val = getenv(envname);\n-\tif (val)\n-\t\t*var = val;\n+\tif (val) {\n+\t\tFREE_AND_NULL(*var);\n+\t\t*var = xstrdup(val);\n+\t}\n }\n \n void http_init(struct remote *remote, const char *url, int proactive_auth)\n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495534","messageId":"ff5e761e5547805265b5f197213310822e7a945d.1716541556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716541556.git.ps@pks.im","subject":"[PATCH v2 07/21] config: clarify memory ownership in `git_config_pathname()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-24T10:03:43Z","receivedAt":"2024-05-24T10:03:47Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The out parameter of `git_config_pathname()` is a `const char **` even\nthough we transfer ownership of memory to the caller. This is quite\nmisleading and has led to many memory leaks all over the place. Adapt\nthe parameter to instead be `char **`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/blame.c        |  2 +-\n builtin/commit.c       |  2 +-\n builtin/config.c       |  2 +-\n builtin/log.c          |  2 +-\n builtin/receive-pack.c |  4 ++--\n config.c               | 10 +++++-----\n config.h               |  8 ++++----\n diff.c                 |  2 +-\n environment.c          |  6 +++---\n environment.h          |  6 +++---\n fetch-pack.c           |  4 ++--\n fsck.c                 |  4 ++--\n fsmonitor-settings.c   |  5 ++++-\n gpg-interface.c        |  4 +++-\n http.c                 | 12 ++++++------\n mailmap.c              |  2 +-\n mailmap.h              |  2 +-\n setup.c                |  6 +++---\n 18 files changed, 44 insertions(+), 39 deletions(-)\n\ndiff --git a/builtin/blame.c b/builtin/blame.c\nindex 6bc7aa6085..838cd476be 100644\n--- a/builtin/blame.c\n+++ b/builtin/blame.c\n@@ -718,7 +718,7 @@ static int git_blame_config(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"blame.ignorerevsfile\")) {\n-\t\tconst char *str;\n+\t\tchar *str;\n \t\tint ret;\n \n \t\tret = git_config_pathname(&str, var, value);\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 78bfae2164..1cc88e92bf 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -107,7 +107,7 @@ static enum {\n } commit_style;\n \n static const char *logfile, *force_author;\n-static const char *template_file;\n+static char *template_file;\n /*\n  * The _message variables are commit names from which to take\n  * the commit message and/or authorship.\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 80aa9d8a66..cc343f55ca 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -277,7 +277,7 @@ static int format_config(struct strbuf *buf, const char *key_,\n \t\t\telse\n \t\t\t\tstrbuf_addstr(buf, v ? \"true\" : \"false\");\n \t\t} else if (type == TYPE_PATH) {\n-\t\t\tconst char *v;\n+\t\t\tchar *v;\n \t\t\tif (git_config_pathname(&v, key_, value_) < 0)\n \t\t\t\treturn -1;\n \t\t\tstrbuf_addstr(buf, v);\ndiff --git a/builtin/log.c b/builtin/log.c\nindex b17dd8b40a..a2f5845556 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -957,7 +957,7 @@ static int do_signoff;\n static enum auto_base_setting auto_base;\n static char *from;\n static const char *signature = git_version_string;\n-static const char *signature_file;\n+static char *signature_file;\n static enum cover_setting config_cover_letter;\n static const char *config_output_directory;\n static enum cover_from_description cover_from_description_mode = COVER_FROM_MESSAGE;\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex be8969a84a..56228ad314 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -168,13 +168,13 @@ static int receive_pack_config(const char *var, const char *value,\n \t}\n \n \tif (strcmp(var, \"receive.fsck.skiplist\") == 0) {\n-\t\tconst char *path;\n+\t\tchar *path;\n \n \t\tif (git_config_pathname(&path, var, value))\n \t\t\treturn 1;\n \t\tstrbuf_addf(&fsck_msg_types, \"%cskiplist=%s\",\n \t\t\tfsck_msg_types.len ? ',' : '=', path);\n-\t\tfree((char *)path);\n+\t\tfree(path);\n \t\treturn 0;\n \t}\n \ndiff --git a/config.c b/config.c\nindex d57996240b..fb56e11276 100644\n--- a/config.c\n+++ b/config.c\n@@ -1346,7 +1346,7 @@ int git_config_string(const char **dest, const char *var, const char *value)\n \treturn 0;\n }\n \n-int git_config_pathname(const char **dest, const char *var, const char *value)\n+int git_config_pathname(char **dest, const char *var, const char *value)\n {\n \tif (!value)\n \t\treturn config_error_nonbool(var);\n@@ -1597,7 +1597,7 @@ static int git_default_core_config(const char *var, const char *value,\n \t\treturn git_config_string(&askpass_program, var, value);\n \n \tif (!strcmp(var, \"core.excludesfile\")) {\n-\t\tfree((char *)excludes_file);\n+\t\tfree(excludes_file);\n \t\treturn git_config_pathname(&excludes_file, var, value);\n \t}\n \n@@ -2494,7 +2494,7 @@ int git_configset_get_maybe_bool(struct config_set *set, const char *key, int *d\n \t\treturn 1;\n }\n \n-int git_configset_get_pathname(struct config_set *set, const char *key, const char **dest)\n+int git_configset_get_pathname(struct config_set *set, const char *key, char **dest)\n {\n \tconst char *value;\n \tif (!git_configset_get_value(set, key, &value, NULL))\n@@ -2639,7 +2639,7 @@ int repo_config_get_maybe_bool(struct repository *repo,\n }\n \n int repo_config_get_pathname(struct repository *repo,\n-\t\t\t     const char *key, const char **dest)\n+\t\t\t     const char *key, char **dest)\n {\n \tint ret;\n \tgit_config_check_init(repo);\n@@ -2738,7 +2738,7 @@ int git_config_get_maybe_bool(const char *key, int *dest)\n \treturn repo_config_get_maybe_bool(the_repository, key, dest);\n }\n \n-int git_config_get_pathname(const char *key, const char **dest)\n+int git_config_get_pathname(const char *key, char **dest)\n {\n \treturn repo_config_get_pathname(the_repository, key, dest);\n }\ndiff --git a/config.h b/config.h\nindex db8b608064..b3103bba94 100644\n--- a/config.h\n+++ b/config.h\n@@ -286,7 +286,7 @@ int git_config_string(const char **, const char *, const char *);\n  * Similar to `git_config_string`, but expands `~` or `~user` into the\n  * user's home directory when found at the beginning of the path.\n  */\n-int git_config_pathname(const char **, const char *, const char *);\n+int git_config_pathname(char **, const char *, const char *);\n \n int git_config_expiry_date(timestamp_t *, const char *, const char *);\n int git_config_color(char *, const char *, const char *);\n@@ -541,7 +541,7 @@ int git_configset_get_ulong(struct config_set *cs, const char *key, unsigned lon\n int git_configset_get_bool(struct config_set *cs, const char *key, int *dest);\n int git_configset_get_bool_or_int(struct config_set *cs, const char *key, int *is_bool, int *dest);\n int git_configset_get_maybe_bool(struct config_set *cs, const char *key, int *dest);\n-int git_configset_get_pathname(struct config_set *cs, const char *key, const char **dest);\n+int git_configset_get_pathname(struct config_set *cs, const char *key, char **dest);\n \n /* Functions for reading a repository's config */\n struct repository;\n@@ -577,7 +577,7 @@ int repo_config_get_bool_or_int(struct repository *repo,\n int repo_config_get_maybe_bool(struct repository *repo,\n \t\t\t       const char *key, int *dest);\n int repo_config_get_pathname(struct repository *repo,\n-\t\t\t     const char *key, const char **dest);\n+\t\t\t     const char *key, char **dest);\n \n /*\n  * Functions for reading protected config. By definition, protected\n@@ -687,7 +687,7 @@ int git_config_get_maybe_bool(const char *key, int *dest);\n  * Similar to `git_config_get_string`, but expands `~` or `~user` into\n  * the user's home directory when found at the beginning of the path.\n  */\n-int git_config_get_pathname(const char *key, const char **dest);\n+int git_config_get_pathname(const char *key, char **dest);\n \n int git_config_get_index_threads(int *dest);\n int git_config_get_split_index(void);\ndiff --git a/diff.c b/diff.c\nindex ded9ac70df..902df9286a 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -58,7 +58,7 @@ static int diff_context_default = 3;\n static int diff_interhunk_context_default;\n static const char *diff_word_regex_cfg;\n static const char *external_diff_cmd_cfg;\n-static const char *diff_order_file_cfg;\n+static char *diff_order_file_cfg;\n int diff_auto_refresh_index = 1;\n static int diff_mnemonic_prefix;\n static int diff_no_prefix;\ndiff --git a/environment.c b/environment.c\nindex a73ba9c12c..279ea3fd5e 100644\n--- a/environment.c\n+++ b/environment.c\n@@ -46,8 +46,8 @@ const char *git_commit_encoding;\n const char *git_log_output_encoding;\n char *apply_default_whitespace;\n char *apply_default_ignorewhitespace;\n-const char *git_attributes_file;\n-const char *git_hooks_path;\n+char *git_attributes_file;\n+char *git_hooks_path;\n int zlib_compression_level = Z_BEST_SPEED;\n int pack_compression_level = Z_DEFAULT_COMPRESSION;\n int fsync_object_files = -1;\n@@ -60,7 +60,7 @@ size_t delta_base_cache_limit = 96 * 1024 * 1024;\n unsigned long big_file_threshold = 512 * 1024 * 1024;\n const char *editor_program;\n const char *askpass_program;\n-const char *excludes_file;\n+char *excludes_file;\n enum auto_crlf auto_crlf = AUTO_CRLF_FALSE;\n enum eol core_eol = EOL_UNSET;\n int global_conv_flags_eol = CONV_EOL_RNDTRP_WARN;\ndiff --git a/environment.h b/environment.h\nindex 0b2d457f07..be1b88ad6f 100644\n--- a/environment.h\n+++ b/environment.h\n@@ -131,8 +131,8 @@ extern int warn_ambiguous_refs;\n extern int warn_on_object_refname_ambiguity;\n extern char *apply_default_whitespace;\n extern char *apply_default_ignorewhitespace;\n-extern const char *git_attributes_file;\n-extern const char *git_hooks_path;\n+extern char *git_attributes_file;\n+extern char *git_hooks_path;\n extern int zlib_compression_level;\n extern int pack_compression_level;\n extern size_t packed_git_window_size;\n@@ -229,7 +229,7 @@ extern const char *git_log_output_encoding;\n \n extern const char *editor_program;\n extern const char *askpass_program;\n-extern const char *excludes_file;\n+extern char *excludes_file;\n \n /*\n  * Should we print an ellipsis after an abbreviated SHA-1 value\ndiff --git a/fetch-pack.c b/fetch-pack.c\nindex 8e8f3bba32..d80e9c92dd 100644\n--- a/fetch-pack.c\n+++ b/fetch-pack.c\n@@ -1865,13 +1865,13 @@ static int fetch_pack_config_cb(const char *var, const char *value,\n \tconst char *msg_id;\n \n \tif (strcmp(var, \"fetch.fsck.skiplist\") == 0) {\n-\t\tconst char *path;\n+\t\tchar *path ;\n \n \t\tif (git_config_pathname(&path, var, value))\n \t\t\treturn 1;\n \t\tstrbuf_addf(&fsck_msg_types, \"%cskiplist=%s\",\n \t\t\tfsck_msg_types.len ? ',' : '=', path);\n-\t\tfree((char *)path);\n+\t\tfree(path);\n \t\treturn 0;\n \t}\n \ndiff --git a/fsck.c b/fsck.c\nindex 8ef962199f..7dff41413e 100644\n--- a/fsck.c\n+++ b/fsck.c\n@@ -1330,13 +1330,13 @@ int git_fsck_config(const char *var, const char *value,\n \tconst char *msg_id;\n \n \tif (strcmp(var, \"fsck.skiplist\") == 0) {\n-\t\tconst char *path;\n+\t\tchar *path;\n \t\tstruct strbuf sb = STRBUF_INIT;\n \n \t\tif (git_config_pathname(&path, var, value))\n \t\t\treturn 1;\n \t\tstrbuf_addf(&sb, \"skiplist=%s\", path);\n-\t\tfree((char *)path);\n+\t\tfree(path);\n \t\tfsck_set_msg_types(options, sb.buf);\n \t\tstrbuf_release(&sb);\n \t\treturn 0;\ndiff --git a/fsmonitor-settings.c b/fsmonitor-settings.c\nindex a6a9e6bc19..e818583420 100644\n--- a/fsmonitor-settings.c\n+++ b/fsmonitor-settings.c\n@@ -103,6 +103,7 @@ static struct fsmonitor_settings *alloc_settings(void)\n static void lookup_fsmonitor_settings(struct repository *r)\n {\n \tconst char *const_str;\n+\tchar *to_free = NULL;\n \tint bool_value;\n \n \tif (r->settings.fsmonitor)\n@@ -129,8 +130,9 @@ static void lookup_fsmonitor_settings(struct repository *r)\n \t\tbreak;\n \n \tcase -1: /* config value set to an arbitrary string */\n-\t\tif (repo_config_get_pathname(r, \"core.fsmonitor\", &const_str))\n+\t\tif (repo_config_get_pathname(r, \"core.fsmonitor\", &to_free))\n \t\t\treturn; /* should not happen */\n+\t\tconst_str = to_free;\n \t\tbreak;\n \n \tdefault: /* should not happen */\n@@ -141,6 +143,7 @@ static void lookup_fsmonitor_settings(struct repository *r)\n \t\tfsm_settings__set_hook(r, const_str);\n \telse\n \t\tfsm_settings__set_disabled(r);\n+\tfree(to_free);\n }\n \n enum fsmonitor_mode fsm_settings__get_mode(struct repository *r)\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 1ff94266d2..2b50ed0fa0 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -27,7 +27,9 @@ static void gpg_interface_lazy_init(void)\n }\n \n static char *configured_signing_key;\n-static const char *ssh_default_key_command, *ssh_allowed_signers, *ssh_revocation_file;\n+static const char *ssh_default_key_command;\n+static char *ssh_allowed_signers;\n+static char *ssh_revocation_file;\n static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;\n \n struct gpg_format {\ndiff --git a/http.c b/http.c\nindex db2e2f1d39..fa3ea87451 100644\n--- a/http.c\n+++ b/http.c\n@@ -64,7 +64,7 @@ static char *ssl_key_type;\n static char *ssl_capath;\n static char *curl_no_proxy;\n #ifdef GIT_CURL_HAVE_CURLOPT_PINNEDPUBLICKEY\n-static const char *ssl_pinnedkey;\n+static char *ssl_pinnedkey;\n #endif\n static char *ssl_cainfo;\n static long curl_low_speed_limit = -1;\n@@ -108,7 +108,7 @@ static struct {\n \n static struct credential proxy_auth = CREDENTIAL_INIT;\n static const char *curl_proxyuserpwd;\n-static const char *curl_cookie_file;\n+static char *curl_cookie_file;\n static int curl_save_cookies;\n struct credential http_auth = CREDENTIAL_INIT;\n static int http_proactive_auth;\n@@ -381,17 +381,17 @@ static int http_options(const char *var, const char *value,\n \tif (!strcmp(\"http.sslversion\", var))\n \t\treturn git_config_string(&ssl_version, var, value);\n \tif (!strcmp(\"http.sslcert\", var))\n-\t\treturn git_config_pathname((const char **)&ssl_cert, var, value);\n+\t\treturn git_config_pathname(&ssl_cert, var, value);\n \tif (!strcmp(\"http.sslcerttype\", var))\n \t\treturn git_config_string((const char **)&ssl_cert_type, var, value);\n \tif (!strcmp(\"http.sslkey\", var))\n-\t\treturn git_config_pathname((const char **)&ssl_key, var, value);\n+\t\treturn git_config_pathname(&ssl_key, var, value);\n \tif (!strcmp(\"http.sslkeytype\", var))\n \t\treturn git_config_string((const char **)&ssl_key_type, var, value);\n \tif (!strcmp(\"http.sslcapath\", var))\n-\t\treturn git_config_pathname((const char **)&ssl_capath, var, value);\n+\t\treturn git_config_pathname(&ssl_capath, var, value);\n \tif (!strcmp(\"http.sslcainfo\", var))\n-\t\treturn git_config_pathname((const char **)&ssl_cainfo, var, value);\n+\t\treturn git_config_pathname(&ssl_cainfo, var, value);\n \tif (!strcmp(\"http.sslcertpasswordprotected\", var)) {\n \t\tssl_cert_password_required = git_config_bool(var, value);\n \t\treturn 0;\ndiff --git a/mailmap.c b/mailmap.c\nindex 3d6a5e9400..044466b043 100644\n--- a/mailmap.c\n+++ b/mailmap.c\n@@ -6,7 +6,7 @@\n #include \"object-store-ll.h\"\n #include \"setup.h\"\n \n-const char *git_mailmap_file;\n+char *git_mailmap_file;\n const char *git_mailmap_blob;\n \n struct mailmap_info {\ndiff --git a/mailmap.h b/mailmap.h\nindex 0f8fd2c586..429a760945 100644\n--- a/mailmap.h\n+++ b/mailmap.h\n@@ -3,7 +3,7 @@\n \n struct string_list;\n \n-extern const char *git_mailmap_file;\n+extern char *git_mailmap_file;\n extern const char *git_mailmap_blob;\n \n int read_mailmap(struct string_list *map);\ndiff --git a/setup.c b/setup.c\nindex 9247cded6a..59ff3a19eb 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -1177,13 +1177,13 @@ static int safe_directory_cb(const char *key, const char *value,\n \t} else if (!strcmp(value, \"*\")) {\n \t\tdata->is_safe = 1;\n \t} else {\n-\t\tconst char *interpolated = NULL;\n+\t\tchar *interpolated = NULL;\n \n \t\tif (!git_config_pathname(&interpolated, key, value) &&\n \t\t    !fspathcmp(data->path, interpolated ? interpolated : value))\n \t\t\tdata->is_safe = 1;\n \n-\t\tfree((char *)interpolated);\n+\t\tfree(interpolated);\n \t}\n \n \treturn 0;\n@@ -1822,7 +1822,7 @@ static int template_dir_cb(const char *key, const char *value,\n \t\tchar *path = NULL;\n \n \t\tFREE_AND_NULL(data->path);\n-\t\tif (!git_config_pathname((const char **)&path, key, value))\n+\t\tif (!git_config_pathname(&path, key, value))\n \t\t\tdata->path = path ? path : xstrdup(value);\n \t}\n \n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495535","messageId":"afe69c7303c2cc4894e0f075642bfdffedb99839.1716541556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716541556.git.ps@pks.im","subject":"[PATCH v2 08/21] diff: refactor code to clarify memory ownership of prefixes","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-24T10:03:48Z","receivedAt":"2024-05-24T10:03:52Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The source and destination prefixes are tracked in a `const char *`\narray, but may at times contain allocated strings. The result is that\nthose strings may be leaking because we never free them.\n\nRefactor the code to always store allocated strings in those variables,\nfreeing them as required. This requires us to handle the default values\na bit different compared to before. But given that there is only a\nsingle callsite where we use the variables to `struct diff_options` it's\neasy to handle the defaults there.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n diff.c | 18 ++++++++++--------\n 1 file changed, 10 insertions(+), 8 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex 902df9286a..679ef472f4 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -62,8 +62,8 @@ static char *diff_order_file_cfg;\n int diff_auto_refresh_index = 1;\n static int diff_mnemonic_prefix;\n static int diff_no_prefix;\n-static const char *diff_src_prefix = \"a/\";\n-static const char *diff_dst_prefix = \"b/\";\n+static char *diff_src_prefix;\n+static char *diff_dst_prefix;\n static int diff_relative;\n static int diff_stat_name_width;\n static int diff_stat_graph_width;\n@@ -411,10 +411,12 @@ int git_diff_ui_config(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"diff.srcprefix\")) {\n-\t\treturn git_config_string(&diff_src_prefix, var, value);\n+\t\tFREE_AND_NULL(diff_src_prefix);\n+\t\treturn git_config_string((const char **) &diff_src_prefix, var, value);\n \t}\n \tif (!strcmp(var, \"diff.dstprefix\")) {\n-\t\treturn git_config_string(&diff_dst_prefix, var, value);\n+\t\tFREE_AND_NULL(diff_dst_prefix);\n+\t\treturn git_config_string((const char **) &diff_dst_prefix, var, value);\n \t}\n \tif (!strcmp(var, \"diff.relative\")) {\n \t\tdiff_relative = git_config_bool(var, value);\n@@ -3433,8 +3435,8 @@ void diff_set_noprefix(struct diff_options *options)\n \n void diff_set_default_prefix(struct diff_options *options)\n {\n-\toptions->a_prefix = diff_src_prefix;\n-\toptions->b_prefix = diff_dst_prefix;\n+\toptions->a_prefix = diff_src_prefix ? diff_src_prefix : \"a/\";\n+\toptions->b_prefix = diff_dst_prefix ? diff_dst_prefix : \"b/\";\n }\n \n struct userdiff_driver *get_textconv(struct repository *r,\n@@ -5371,8 +5373,8 @@ static int diff_opt_default_prefix(const struct option *opt,\n \n \tBUG_ON_OPT_NEG(unset);\n \tBUG_ON_OPT_ARG(optarg);\n-\tdiff_src_prefix = \"a/\";\n-\tdiff_dst_prefix = \"b/\";\n+\tFREE_AND_NULL(diff_src_prefix);\n+\tFREE_AND_NULL(diff_dst_prefix);\n \tdiff_set_default_prefix(options);\n \treturn 0;\n }\n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495536","messageId":"eb7fce55b0d2035ab93c466d24de6ad742af72a8.1716541556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716541556.git.ps@pks.im","subject":"[PATCH v2 09/21] convert: refactor code to clarify ownership of check_roundtrip_encoding","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-24T10:03:52Z","receivedAt":"2024-05-24T10:03:57Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The `check_roundtrip_encoding` variable is tracked in a `const char *`\neven though it may contain allocated strings at times. The result is\nthat those strings may be leaking because we never free them.\n\nRefactor the code to always store allocated strings in this variable.\nThe default value is handled in `check_roundtrip()` now, which is the\nonly user of the variable.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n config.c      |  6 ++++--\n convert.c     | 24 +++++++++++++-----------\n convert.h     |  2 +-\n environment.c |  2 +-\n 4 files changed, 19 insertions(+), 15 deletions(-)\n\ndiff --git a/config.c b/config.c\nindex fb56e11276..f9101045ee 100644\n--- a/config.c\n+++ b/config.c\n@@ -1564,8 +1564,10 @@ static int git_default_core_config(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \n-\tif (!strcmp(var, \"core.checkroundtripencoding\"))\n-\t\treturn git_config_string(&check_roundtrip_encoding, var, value);\n+\tif (!strcmp(var, \"core.checkroundtripencoding\")) {\n+\t\tFREE_AND_NULL(check_roundtrip_encoding);\n+\t\treturn git_config_string((const char **) &check_roundtrip_encoding, var, value);\n+\t}\n \n \tif (!strcmp(var, \"core.notesref\")) {\n \t\tif (!value)\ndiff --git a/convert.c b/convert.c\nindex 35b25eb3cb..03c3c528f9 100644\n--- a/convert.c\n+++ b/convert.c\n@@ -345,30 +345,32 @@ static int check_roundtrip(const char *enc_name)\n \t * space separated encodings (eg. \"UTF-16, ASCII, CP1125\").\n \t * Search for the given encoding in that string.\n \t */\n-\tconst char *found = strcasestr(check_roundtrip_encoding, enc_name);\n+\tconst char *encoding = check_roundtrip_encoding ?\n+\t\tcheck_roundtrip_encoding : \"SHIFT-JIS\";\n+\tconst char *found = strcasestr(encoding, enc_name);\n \tconst char *next;\n \tint len;\n \tif (!found)\n \t\treturn 0;\n \tnext = found + strlen(enc_name);\n-\tlen = strlen(check_roundtrip_encoding);\n+\tlen = strlen(encoding);\n \treturn (found && (\n \t\t\t/*\n-\t\t\t * check that the found encoding is at the\n-\t\t\t * beginning of check_roundtrip_encoding or\n-\t\t\t * that it is prefixed with a space or comma\n+\t\t\t * Check that the found encoding is at the beginning of\n+\t\t\t * encoding or that it is prefixed with a space or\n+\t\t\t * comma.\n \t\t\t */\n-\t\t\tfound == check_roundtrip_encoding || (\n+\t\t\tfound == encoding || (\n \t\t\t\t(isspace(found[-1]) || found[-1] == ',')\n \t\t\t)\n \t\t) && (\n \t\t\t/*\n-\t\t\t * check that the found encoding is at the\n-\t\t\t * end of check_roundtrip_encoding or\n-\t\t\t * that it is suffixed with a space or comma\n+\t\t\t * Check that the found encoding is at the end of\n+\t\t\t * encoding or that it is suffixed with a space\n+\t\t\t * or comma.\n \t\t\t */\n-\t\t\tnext == check_roundtrip_encoding + len || (\n-\t\t\t\tnext < check_roundtrip_encoding + len &&\n+\t\t\tnext == encoding + len || (\n+\t\t\t\tnext < encoding + len &&\n \t\t\t\t(isspace(next[0]) || next[0] == ',')\n \t\t\t)\n \t\t));\ndiff --git a/convert.h b/convert.h\nindex ab8b4fa68d..d925589444 100644\n--- a/convert.h\n+++ b/convert.h\n@@ -92,7 +92,7 @@ void convert_attrs(struct index_state *istate,\n \t\t   struct conv_attrs *ca, const char *path);\n \n extern enum eol core_eol;\n-extern const char *check_roundtrip_encoding;\n+extern char *check_roundtrip_encoding;\n const char *get_cached_convert_stats_ascii(struct index_state *istate,\n \t\t\t\t\t   const char *path);\n const char *get_wt_convert_stats_ascii(const char *path);\ndiff --git a/environment.c b/environment.c\nindex 279ea3fd5e..ab6956559e 100644\n--- a/environment.c\n+++ b/environment.c\n@@ -64,7 +64,7 @@ char *excludes_file;\n enum auto_crlf auto_crlf = AUTO_CRLF_FALSE;\n enum eol core_eol = EOL_UNSET;\n int global_conv_flags_eol = CONV_EOL_RNDTRP_WARN;\n-const char *check_roundtrip_encoding = \"SHIFT-JIS\";\n+char *check_roundtrip_encoding;\n enum branch_track git_branch_track = BRANCH_TRACK_REMOTE;\n enum rebase_setup_type autorebase = AUTOREBASE_NEVER;\n enum push_default_type push_default = PUSH_DEFAULT_UNSPECIFIED;\n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495537","messageId":"ee2fcf388c0855ec30f98eb576e9896dd90d2924.1716541556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716541556.git.ps@pks.im","subject":"[PATCH v2 10/21] builtin/log: stop using globals for log config","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-24T10:03:57Z","receivedAt":"2024-05-24T10:04:02Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We're using global variables to store the log configuration. Many of\nthese can be set both via the command line and via the config, and\ndepending on how they are being set, they may contain allocated strings.\nThis leads to hard-to-track memory ownership and memory leaks.\n\nRefactor the code to instead use a `struct log_config` that is being\nallocated on the stack. This allows us to more clearly scope the\nvariables, track memory ownership and ultimately release the memory.\n\nThis also prepares us for a change to `git_config_string()`, which will\nbe adapted to have a `char **` out parameter instead of `const char **`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/log.c | 259 ++++++++++++++++++++++++++++++--------------------\n 1 file changed, 156 insertions(+), 103 deletions(-)\n\ndiff --git a/builtin/log.c b/builtin/log.c\nindex a2f5845556..f5da29ee2a 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -48,22 +48,8 @@\n #define COVER_FROM_AUTO_MAX_SUBJECT_LEN 100\n #define FORMAT_PATCH_NAME_MAX_DEFAULT 64\n \n-/* Set a default date-time format for git log (\"log.date\" config variable) */\n-static const char *default_date_mode = NULL;\n-\n-static int default_abbrev_commit;\n-static int default_show_root = 1;\n-static int default_follow;\n-static int default_show_signature;\n-static int default_encode_email_headers = 1;\n-static int decoration_style;\n-static int decoration_given;\n-static int use_mailmap_config = 1;\n static unsigned int force_in_body_from;\n static int stdout_mboxrd;\n-static const char *fmt_patch_subject_prefix = \"PATCH\";\n-static int fmt_patch_name_max = FORMAT_PATCH_NAME_MAX_DEFAULT;\n-static const char *fmt_pretty;\n static int format_no_prefix;\n \n static const char * const builtin_log_usage[] = {\n@@ -111,6 +97,39 @@ static int parse_decoration_style(const char *value)\n \treturn -1;\n }\n \n+struct log_config {\n+\tint default_abbrev_commit;\n+\tint default_show_root;\n+\tint default_follow;\n+\tint default_show_signature;\n+\tint default_encode_email_headers;\n+\tint decoration_style;\n+\tint decoration_given;\n+\tint use_mailmap_config;\n+\tchar *fmt_patch_subject_prefix;\n+\tint fmt_patch_name_max;\n+\tchar *fmt_pretty;\n+\tchar *default_date_mode;\n+};\n+\n+static void log_config_init(struct log_config *cfg)\n+{\n+\tmemset(cfg, 0, sizeof(*cfg));\n+\tcfg->default_show_root = 1;\n+\tcfg->default_encode_email_headers = 1;\n+\tcfg->use_mailmap_config = 1;\n+\tcfg->fmt_patch_subject_prefix = xstrdup(\"PATCH\");\n+\tcfg->fmt_patch_name_max = FORMAT_PATCH_NAME_MAX_DEFAULT;\n+\tcfg->decoration_style = auto_decoration_style();\n+}\n+\n+static void log_config_release(struct log_config *cfg)\n+{\n+\tfree(cfg->default_date_mode);\n+\tfree(cfg->fmt_pretty);\n+\tfree(cfg->fmt_patch_subject_prefix);\n+}\n+\n static int use_default_decoration_filter = 1;\n static struct string_list decorate_refs_exclude = STRING_LIST_INIT_NODUP;\n static struct string_list decorate_refs_exclude_config = STRING_LIST_INIT_NODUP;\n@@ -127,20 +146,22 @@ static int clear_decorations_callback(const struct option *opt UNUSED,\n \treturn 0;\n }\n \n-static int decorate_callback(const struct option *opt UNUSED, const char *arg,\n+static int decorate_callback(const struct option *opt, const char *arg,\n \t\t\t     int unset)\n {\n+\tstruct log_config *cfg = opt->value;\n+\n \tif (unset)\n-\t\tdecoration_style = 0;\n+\t\tcfg->decoration_style = 0;\n \telse if (arg)\n-\t\tdecoration_style = parse_decoration_style(arg);\n+\t\tcfg->decoration_style = parse_decoration_style(arg);\n \telse\n-\t\tdecoration_style = DECORATE_SHORT_REFS;\n+\t\tcfg->decoration_style = DECORATE_SHORT_REFS;\n \n-\tif (decoration_style < 0)\n+\tif (cfg->decoration_style < 0)\n \t\tdie(_(\"invalid --decorate option: %s\"), arg);\n \n-\tdecoration_given = 1;\n+\tcfg->decoration_given = 1;\n \n \treturn 0;\n }\n@@ -160,32 +181,26 @@ static int log_line_range_callback(const struct option *option, const char *arg,\n \treturn 0;\n }\n \n-static void init_log_defaults(void)\n+static void cmd_log_init_defaults(struct rev_info *rev,\n+\t\t\t\t  struct log_config *cfg)\n {\n-\tinit_diff_ui_defaults();\n-\n-\tdecoration_style = auto_decoration_style();\n-}\n-\n-static void cmd_log_init_defaults(struct rev_info *rev)\n-{\n-\tif (fmt_pretty)\n-\t\tget_commit_format(fmt_pretty, rev);\n-\tif (default_follow)\n+\tif (cfg->fmt_pretty)\n+\t\tget_commit_format(cfg->fmt_pretty, rev);\n+\tif (cfg->default_follow)\n \t\trev->diffopt.flags.default_follow_renames = 1;\n \trev->verbose_header = 1;\n \tinit_diffstat_widths(&rev->diffopt);\n \trev->diffopt.flags.recursive = 1;\n \trev->diffopt.flags.allow_textconv = 1;\n-\trev->abbrev_commit = default_abbrev_commit;\n-\trev->show_root_diff = default_show_root;\n-\trev->subject_prefix = fmt_patch_subject_prefix;\n-\trev->patch_name_max = fmt_patch_name_max;\n-\trev->show_signature = default_show_signature;\n-\trev->encode_email_headers = default_encode_email_headers;\n+\trev->abbrev_commit = cfg->default_abbrev_commit;\n+\trev->show_root_diff = cfg->default_show_root;\n+\trev->subject_prefix = cfg->fmt_patch_subject_prefix;\n+\trev->patch_name_max = cfg->fmt_patch_name_max;\n+\trev->show_signature = cfg->default_show_signature;\n+\trev->encode_email_headers = cfg->default_encode_email_headers;\n \n-\tif (default_date_mode)\n-\t\tparse_date_format(default_date_mode, &rev->date_mode);\n+\tif (cfg->default_date_mode)\n+\t\tparse_date_format(cfg->default_date_mode, &rev->date_mode);\n }\n \n static void set_default_decoration_filter(struct decoration_filter *decoration_filter)\n@@ -233,7 +248,8 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f\n }\n \n static void cmd_log_init_finish(int argc, const char **argv, const char *prefix,\n-\t\t\t struct rev_info *rev, struct setup_revision_opt *opt)\n+\t\t\t struct rev_info *rev, struct setup_revision_opt *opt,\n+\t\t\t struct log_config *cfg)\n {\n \tstruct userformat_want w;\n \tint quiet = 0, source = 0, mailmap;\n@@ -258,7 +274,7 @@ static void cmd_log_init_finish(int argc, const char **argv, const char *prefix,\n \t\t\t\tN_(\"pattern\"), N_(\"only decorate refs that match <pattern>\")),\n \t\tOPT_STRING_LIST(0, \"decorate-refs-exclude\", &decorate_refs_exclude,\n \t\t\t\tN_(\"pattern\"), N_(\"do not decorate refs that match <pattern>\")),\n-\t\tOPT_CALLBACK_F(0, \"decorate\", NULL, NULL, N_(\"decorate options\"),\n+\t\tOPT_CALLBACK_F(0, \"decorate\", cfg, NULL, N_(\"decorate options\"),\n \t\t\t       PARSE_OPT_OPTARG, decorate_callback),\n \t\tOPT_CALLBACK('L', NULL, &line_cb, \"range:file\",\n \t\t\t     N_(\"trace the evolution of line range <start>,<end> or function :<funcname> in <file>\"),\n@@ -269,7 +285,7 @@ static void cmd_log_init_finish(int argc, const char **argv, const char *prefix,\n \tline_cb.rev = rev;\n \tline_cb.prefix = prefix;\n \n-\tmailmap = use_mailmap_config;\n+\tmailmap = cfg->use_mailmap_config;\n \targc = parse_options(argc, argv, prefix,\n \t\t\t     builtin_log_options, builtin_log_usage,\n \t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT |\n@@ -314,8 +330,8 @@ static void cmd_log_init_finish(int argc, const char **argv, const char *prefix,\n \t\t * \"log --pretty=raw\" is special; ignore UI oriented\n \t\t * configuration variables such as decoration.\n \t\t */\n-\t\tif (!decoration_given)\n-\t\t\tdecoration_style = 0;\n+\t\tif (!cfg->decoration_given)\n+\t\t\tcfg->decoration_style = 0;\n \t\tif (!rev->abbrev_commit_given)\n \t\t\trev->abbrev_commit = 0;\n \t}\n@@ -326,24 +342,24 @@ static void cmd_log_init_finish(int argc, const char **argv, const char *prefix,\n \t\t\t * Disable decoration loading if the format will not\n \t\t\t * show them anyway.\n \t\t\t */\n-\t\t\tdecoration_style = 0;\n-\t\t} else if (!decoration_style) {\n+\t\t\tcfg->decoration_style = 0;\n+\t\t} else if (!cfg->decoration_style) {\n \t\t\t/*\n \t\t\t * If we are going to show them, make sure we do load\n \t\t\t * them here, but taking care not to override a\n \t\t\t * specific style set by config or --decorate.\n \t\t\t */\n-\t\t\tdecoration_style = DECORATE_SHORT_REFS;\n+\t\t\tcfg->decoration_style = DECORATE_SHORT_REFS;\n \t\t}\n \t}\n \n-\tif (decoration_style || rev->simplify_by_decoration) {\n+\tif (cfg->decoration_style || rev->simplify_by_decoration) {\n \t\tset_default_decoration_filter(&decoration_filter);\n \n-\t\tif (decoration_style)\n+\t\tif (cfg->decoration_style)\n \t\t\trev->show_decorations = 1;\n \n-\t\tload_ref_decorations(&decoration_filter, decoration_style);\n+\t\tload_ref_decorations(&decoration_filter, cfg->decoration_style);\n \t}\n \n \tif (rev->line_level_traverse)\n@@ -353,16 +369,11 @@ static void cmd_log_init_finish(int argc, const char **argv, const char *prefix,\n }\n \n static void cmd_log_init(int argc, const char **argv, const char *prefix,\n-\t\t\t struct rev_info *rev, struct setup_revision_opt *opt)\n+\t\t\t struct rev_info *rev, struct setup_revision_opt *opt,\n+\t\t\t struct log_config *cfg)\n {\n-\tcmd_log_init_defaults(rev);\n-\tcmd_log_init_finish(argc, argv, prefix, rev, opt);\n-}\n-\n-static int cmd_log_deinit(int ret, struct rev_info *rev)\n-{\n-\trelease_revisions(rev);\n-\treturn ret;\n+\tcmd_log_init_defaults(rev, cfg);\n+\tcmd_log_init_finish(argc, argv, prefix, rev, opt, cfg);\n }\n \n /*\n@@ -566,30 +577,37 @@ static int cmd_log_walk(struct rev_info *rev)\n static int git_log_config(const char *var, const char *value,\n \t\t\t  const struct config_context *ctx, void *cb)\n {\n+\tstruct log_config *cfg = cb;\n \tconst char *slot_name;\n \n-\tif (!strcmp(var, \"format.pretty\"))\n-\t\treturn git_config_string(&fmt_pretty, var, value);\n-\tif (!strcmp(var, \"format.subjectprefix\"))\n-\t\treturn git_config_string(&fmt_patch_subject_prefix, var, value);\n+\tif (!strcmp(var, \"format.pretty\")) {\n+\t\tFREE_AND_NULL(cfg->fmt_pretty);\n+\t\treturn git_config_string((const char **) &cfg->fmt_pretty, var, value);\n+\t}\n+\tif (!strcmp(var, \"format.subjectprefix\")) {\n+\t\tFREE_AND_NULL(cfg->fmt_patch_subject_prefix);\n+\t\treturn git_config_string((const char **) &cfg->fmt_patch_subject_prefix, var, value);\n+\t}\n \tif (!strcmp(var, \"format.filenamemaxlength\")) {\n-\t\tfmt_patch_name_max = git_config_int(var, value, ctx->kvi);\n+\t\tcfg->fmt_patch_name_max = git_config_int(var, value, ctx->kvi);\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"format.encodeemailheaders\")) {\n-\t\tdefault_encode_email_headers = git_config_bool(var, value);\n+\t\tcfg->default_encode_email_headers = git_config_bool(var, value);\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"log.abbrevcommit\")) {\n-\t\tdefault_abbrev_commit = git_config_bool(var, value);\n+\t\tcfg->default_abbrev_commit = git_config_bool(var, value);\n \t\treturn 0;\n \t}\n-\tif (!strcmp(var, \"log.date\"))\n-\t\treturn git_config_string(&default_date_mode, var, value);\n+\tif (!strcmp(var, \"log.date\")) {\n+\t\tFREE_AND_NULL(cfg->default_date_mode);\n+\t\treturn git_config_string((const char **) &cfg->default_date_mode, var, value);\n+\t}\n \tif (!strcmp(var, \"log.decorate\")) {\n-\t\tdecoration_style = parse_decoration_style(value);\n-\t\tif (decoration_style < 0)\n-\t\t\tdecoration_style = 0; /* maybe warn? */\n+\t\tcfg->decoration_style = parse_decoration_style(value);\n+\t\tif (cfg->decoration_style < 0)\n+\t\t\tcfg->decoration_style = 0; /* maybe warn? */\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"log.diffmerges\")) {\n@@ -598,21 +616,21 @@ static int git_log_config(const char *var, const char *value,\n \t\treturn diff_merges_config(value);\n \t}\n \tif (!strcmp(var, \"log.showroot\")) {\n-\t\tdefault_show_root = git_config_bool(var, value);\n+\t\tcfg->default_show_root = git_config_bool(var, value);\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"log.follow\")) {\n-\t\tdefault_follow = git_config_bool(var, value);\n+\t\tcfg->default_follow = git_config_bool(var, value);\n \t\treturn 0;\n \t}\n \tif (skip_prefix(var, \"color.decorate.\", &slot_name))\n \t\treturn parse_decorate_color_config(var, slot_name, value);\n \tif (!strcmp(var, \"log.mailmap\")) {\n-\t\tuse_mailmap_config = git_config_bool(var, value);\n+\t\tcfg->use_mailmap_config = git_config_bool(var, value);\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"log.showsignature\")) {\n-\t\tdefault_show_signature = git_config_bool(var, value);\n+\t\tcfg->default_show_signature = git_config_bool(var, value);\n \t\treturn 0;\n \t}\n \n@@ -621,11 +639,14 @@ static int git_log_config(const char *var, const char *value,\n \n int cmd_whatchanged(int argc, const char **argv, const char *prefix)\n {\n+\tstruct log_config cfg;\n \tstruct rev_info rev;\n \tstruct setup_revision_opt opt;\n+\tint ret;\n \n-\tinit_log_defaults();\n-\tgit_config(git_log_config, NULL);\n+\tlog_config_init(&cfg);\n+\tinit_diff_ui_defaults();\n+\tgit_config(git_log_config, &cfg);\n \n \trepo_init_revisions(the_repository, &rev, prefix);\n \tgit_config(grep_config, &rev.grep_filter);\n@@ -635,10 +656,15 @@ int cmd_whatchanged(int argc, const char **argv, const char *prefix)\n \tmemset(&opt, 0, sizeof(opt));\n \topt.def = \"HEAD\";\n \topt.revarg_opt = REVARG_COMMITTISH;\n-\tcmd_log_init(argc, argv, prefix, &rev, &opt);\n+\tcmd_log_init(argc, argv, prefix, &rev, &opt, &cfg);\n \tif (!rev.diffopt.output_format)\n \t\trev.diffopt.output_format = DIFF_FORMAT_RAW;\n-\treturn cmd_log_deinit(cmd_log_walk(&rev), &rev);\n+\n+\tret = cmd_log_walk(&rev);\n+\n+\trelease_revisions(&rev);\n+\tlog_config_release(&cfg);\n+\treturn ret;\n }\n \n static void show_tagger(const char *buf, struct rev_info *rev)\n@@ -733,14 +759,16 @@ static void show_setup_revisions_tweak(struct rev_info *rev)\n \n int cmd_show(int argc, const char **argv, const char *prefix)\n {\n+\tstruct log_config cfg;\n \tstruct rev_info rev;\n \tunsigned int i;\n \tstruct setup_revision_opt opt;\n \tstruct pathspec match_all;\n \tint ret = 0;\n \n-\tinit_log_defaults();\n-\tgit_config(git_log_config, NULL);\n+\tlog_config_init(&cfg);\n+\tinit_diff_ui_defaults();\n+\tgit_config(git_log_config, &cfg);\n \n \tif (the_repository->gitdir) {\n \t\tprepare_repo_settings(the_repository);\n@@ -759,10 +787,14 @@ int cmd_show(int argc, const char **argv, const char *prefix)\n \tmemset(&opt, 0, sizeof(opt));\n \topt.def = \"HEAD\";\n \topt.tweak = show_setup_revisions_tweak;\n-\tcmd_log_init(argc, argv, prefix, &rev, &opt);\n+\tcmd_log_init(argc, argv, prefix, &rev, &opt, &cfg);\n \n-\tif (!rev.no_walk)\n-\t\treturn cmd_log_deinit(cmd_log_walk(&rev), &rev);\n+\tif (!rev.no_walk) {\n+\t\tret = cmd_log_walk(&rev);\n+\t\trelease_revisions(&rev);\n+\t\tlog_config_release(&cfg);\n+\t\treturn ret;\n+\t}\n \n \trev.diffopt.no_free = 1;\n \tfor (i = 0; i < rev.pending.nr && !ret; i++) {\n@@ -832,8 +864,10 @@ int cmd_show(int argc, const char **argv, const char *prefix)\n \n \trev.diffopt.no_free = 0;\n \tdiff_free(&rev.diffopt);\n+\trelease_revisions(&rev);\n+\tlog_config_release(&cfg);\n \n-\treturn cmd_log_deinit(ret, &rev);\n+\treturn ret;\n }\n \n /*\n@@ -841,11 +875,14 @@ int cmd_show(int argc, const char **argv, const char *prefix)\n  */\n int cmd_log_reflog(int argc, const char **argv, const char *prefix)\n {\n+\tstruct log_config cfg;\n \tstruct rev_info rev;\n \tstruct setup_revision_opt opt;\n+\tint ret;\n \n-\tinit_log_defaults();\n-\tgit_config(git_log_config, NULL);\n+\tlog_config_init(&cfg);\n+\tinit_diff_ui_defaults();\n+\tgit_config(git_log_config, &cfg);\n \n \trepo_init_revisions(the_repository, &rev, prefix);\n \tinit_reflog_walk(&rev.reflog_info);\n@@ -854,14 +891,18 @@ int cmd_log_reflog(int argc, const char **argv, const char *prefix)\n \trev.verbose_header = 1;\n \tmemset(&opt, 0, sizeof(opt));\n \topt.def = \"HEAD\";\n-\tcmd_log_init_defaults(&rev);\n+\tcmd_log_init_defaults(&rev, &cfg);\n \trev.abbrev_commit = 1;\n \trev.commit_format = CMIT_FMT_ONELINE;\n \trev.use_terminator = 1;\n \trev.always_show_header = 1;\n-\tcmd_log_init_finish(argc, argv, prefix, &rev, &opt);\n+\tcmd_log_init_finish(argc, argv, prefix, &rev, &opt, &cfg);\n+\n+\tret = cmd_log_walk(&rev);\n \n-\treturn cmd_log_deinit(cmd_log_walk(&rev), &rev);\n+\trelease_revisions(&rev);\n+\tlog_config_release(&cfg);\n+\treturn ret;\n }\n \n static void log_setup_revisions_tweak(struct rev_info *rev)\n@@ -876,11 +917,14 @@ static void log_setup_revisions_tweak(struct rev_info *rev)\n \n int cmd_log(int argc, const char **argv, const char *prefix)\n {\n+\tstruct log_config cfg;\n \tstruct rev_info rev;\n \tstruct setup_revision_opt opt;\n+\tint ret;\n \n-\tinit_log_defaults();\n-\tgit_config(git_log_config, NULL);\n+\tlog_config_init(&cfg);\n+\tinit_diff_ui_defaults();\n+\tgit_config(git_log_config, &cfg);\n \n \trepo_init_revisions(the_repository, &rev, prefix);\n \tgit_config(grep_config, &rev.grep_filter);\n@@ -890,8 +934,13 @@ int cmd_log(int argc, const char **argv, const char *prefix)\n \topt.def = \"HEAD\";\n \topt.revarg_opt = REVARG_COMMITTISH;\n \topt.tweak = log_setup_revisions_tweak;\n-\tcmd_log_init(argc, argv, prefix, &rev, &opt);\n-\treturn cmd_log_deinit(cmd_log_walk(&rev), &rev);\n+\tcmd_log_init(argc, argv, prefix, &rev, &opt, &cfg);\n+\n+\tret = cmd_log_walk(&rev);\n+\n+\trelease_revisions(&rev);\n+\tlog_config_release(&cfg);\n+\treturn ret;\n }\n \n /* format-patch */\n@@ -1884,6 +1933,7 @@ static void infer_range_diff_ranges(struct strbuf *r1,\n \n int cmd_format_patch(int argc, const char **argv, const char *prefix)\n {\n+\tstruct log_config cfg;\n \tstruct commit *commit;\n \tstruct commit **list = NULL;\n \tstruct rev_info rev;\n@@ -1943,7 +1993,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\t\t    N_(\"start numbering patches at <n> instead of 1\")),\n \t\tOPT_STRING('v', \"reroll-count\", &reroll_count, N_(\"reroll-count\"),\n \t\t\t    N_(\"mark the series as Nth re-roll\")),\n-\t\tOPT_INTEGER(0, \"filename-max-length\", &fmt_patch_name_max,\n+\t\tOPT_INTEGER(0, \"filename-max-length\", &cfg.fmt_patch_name_max,\n \t\t\t    N_(\"max length of output filename\")),\n \t\tOPT_CALLBACK_F(0, \"rfc\", &rfc, N_(\"rfc\"),\n \t\t\t       N_(\"add <rfc> (default 'RFC') before 'PATCH'\"),\n@@ -2017,16 +2067,17 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \textra_to.strdup_strings = 1;\n \textra_cc.strdup_strings = 1;\n \n-\tinit_log_defaults();\n+\tlog_config_init(&cfg);\n+\tinit_diff_ui_defaults();\n \tinit_display_notes(&notes_opt);\n-\tgit_config(git_format_config, NULL);\n+\tgit_config(git_format_config, &cfg);\n \trepo_init_revisions(the_repository, &rev, prefix);\n \tgit_config(grep_config, &rev.grep_filter);\n \n \trev.show_notes = show_notes;\n \tmemcpy(&rev.notes_opt, &notes_opt, sizeof(notes_opt));\n \trev.commit_format = CMIT_FMT_EMAIL;\n-\trev.encode_email_headers = default_encode_email_headers;\n+\trev.encode_email_headers = cfg.default_encode_email_headers;\n \trev.expand_tabs_in_log_default = 0;\n \trev.verbose_header = 1;\n \trev.diff = 1;\n@@ -2037,7 +2088,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \ts_r_opt.def = \"HEAD\";\n \ts_r_opt.revarg_opt = REVARG_COMMITTISH;\n \n-\tstrbuf_addstr(&sprefix, fmt_patch_subject_prefix);\n+\tstrbuf_addstr(&sprefix, cfg.fmt_patch_subject_prefix);\n \tif (format_no_prefix)\n \t\tdiff_set_noprefix(&rev.diffopt);\n \n@@ -2059,8 +2110,8 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \trev.force_in_body_from = force_in_body_from;\n \n \t/* Make sure \"0000-$sub.patch\" gives non-negative length for $sub */\n-\tif (fmt_patch_name_max <= strlen(\"0000-\") + strlen(fmt_patch_suffix))\n-\t\tfmt_patch_name_max = strlen(\"0000-\") + strlen(fmt_patch_suffix);\n+\tif (cfg.fmt_patch_name_max <= strlen(\"0000-\") + strlen(fmt_patch_suffix))\n+\t\tcfg.fmt_patch_name_max = strlen(\"0000-\") + strlen(fmt_patch_suffix);\n \n \tif (cover_from_description_arg)\n \t\tcover_from_description_mode = parse_cover_from_description(cover_from_description_arg);\n@@ -2156,7 +2207,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \trev.always_show_header = 1;\n \n \trev.zero_commit = zero_commit;\n-\trev.patch_name_max = fmt_patch_name_max;\n+\trev.patch_name_max = cfg.fmt_patch_name_max;\n \n \tif (!rev.diffopt.flags.text && !no_binary_diff)\n \t\trev.diffopt.flags.binary = 1;\n@@ -2450,7 +2501,9 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \tif (rev.ref_message_ids)\n \t\tstring_list_clear(rev.ref_message_ids, 0);\n \tfree(rev.ref_message_ids);\n-\treturn cmd_log_deinit(0, &rev);\n+\trelease_revisions(&rev);\n+\tlog_config_release(&cfg);\n+\treturn 0;\n }\n \n static int add_pending_commit(const char *arg, struct rev_info *revs, int flags)\n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495538","messageId":"3490ad3a02f1ad26fbb8860d9dae16b28c72c567.1716541556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716541556.git.ps@pks.im","subject":"[PATCH v2 11/21] builtin/log: stop using globals for format config","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-24T10:04:02Z","receivedAt":"2024-05-24T10:04:06Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"This commit does the exact same as the preceding commit, only for the\nformat configuration instead of the log configuration.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/log.c | 467 ++++++++++++++++++++++++++++----------------------\n 1 file changed, 265 insertions(+), 202 deletions(-)\n\ndiff --git a/builtin/log.c b/builtin/log.c\nindex f5da29ee2a..890bf0c425 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -945,36 +945,6 @@ int cmd_log(int argc, const char **argv, const char *prefix)\n \n /* format-patch */\n \n-static const char *fmt_patch_suffix = \".patch\";\n-static int numbered = 0;\n-static int auto_number = 1;\n-\n-static char *default_attach = NULL;\n-\n-static struct string_list extra_hdr = STRING_LIST_INIT_NODUP;\n-static struct string_list extra_to = STRING_LIST_INIT_NODUP;\n-static struct string_list extra_cc = STRING_LIST_INIT_NODUP;\n-\n-static void add_header(const char *value)\n-{\n-\tstruct string_list_item *item;\n-\tint len = strlen(value);\n-\twhile (len && value[len - 1] == '\\n')\n-\t\tlen--;\n-\n-\tif (!strncasecmp(value, \"to: \", 4)) {\n-\t\titem = string_list_append(&extra_to, value + 4);\n-\t\tlen -= 4;\n-\t} else if (!strncasecmp(value, \"cc: \", 4)) {\n-\t\titem = string_list_append(&extra_cc, value + 4);\n-\t\tlen -= 4;\n-\t} else {\n-\t\titem = string_list_append(&extra_hdr, value);\n-\t}\n-\n-\titem->string[len] = '\\0';\n-}\n-\n enum cover_setting {\n \tCOVER_UNSET,\n \tCOVER_OFF,\n@@ -1001,17 +971,61 @@ enum auto_base_setting {\n \tAUTO_BASE_WHEN_ABLE\n };\n \n-static enum thread_level thread;\n-static int do_signoff;\n-static enum auto_base_setting auto_base;\n-static char *from;\n-static const char *signature = git_version_string;\n-static char *signature_file;\n-static enum cover_setting config_cover_letter;\n-static const char *config_output_directory;\n-static enum cover_from_description cover_from_description_mode = COVER_FROM_MESSAGE;\n-static int show_notes;\n-static struct display_notes_opt notes_opt;\n+struct format_config {\n+\tstruct log_config log;\n+\tenum thread_level thread;\n+\tint do_signoff;\n+\tenum auto_base_setting auto_base;\n+\tchar *base_commit;\n+\tchar *from;\n+\tchar *signature;\n+\tchar *signature_file;\n+\tenum cover_setting config_cover_letter;\n+\tchar *config_output_directory;\n+\tenum cover_from_description cover_from_description_mode;\n+\tint show_notes;\n+\tstruct display_notes_opt notes_opt;\n+\tint numbered_cmdline_opt;\n+\tint numbered;\n+\tint auto_number;\n+\tchar *default_attach;\n+\tstruct string_list extra_hdr;\n+\tstruct string_list extra_to;\n+\tstruct string_list extra_cc;\n+\tint keep_subject;\n+\tint subject_prefix;\n+\tstruct strbuf sprefix;\n+\tchar *fmt_patch_suffix;\n+};\n+\n+static void format_config_init(struct format_config *cfg)\n+{\n+\tmemset(cfg, 0, sizeof(*cfg));\n+\tlog_config_init(&cfg->log);\n+\tcfg->cover_from_description_mode = COVER_FROM_MESSAGE;\n+\tcfg->auto_number = 1;\n+\tstring_list_init_dup(&cfg->extra_hdr);\n+\tstring_list_init_dup(&cfg->extra_to);\n+\tstring_list_init_dup(&cfg->extra_cc);\n+\tstrbuf_init(&cfg->sprefix, 0);\n+\tcfg->fmt_patch_suffix = xstrdup(\".patch\");\n+}\n+\n+static void format_config_release(struct format_config *cfg)\n+{\n+\tlog_config_release(&cfg->log);\n+\tfree(cfg->base_commit);\n+\tfree(cfg->from);\n+\tfree(cfg->signature);\n+\tfree(cfg->signature_file);\n+\tfree(cfg->config_output_directory);\n+\tfree(cfg->default_attach);\n+\tstring_list_clear(&cfg->extra_hdr, 0);\n+\tstring_list_clear(&cfg->extra_to, 0);\n+\tstring_list_clear(&cfg->extra_cc, 0);\n+\tstrbuf_release(&cfg->sprefix);\n+\tfree(cfg->fmt_patch_suffix);\n+}\n \n static enum cover_from_description parse_cover_from_description(const char *arg)\n {\n@@ -1029,27 +1043,51 @@ static enum cover_from_description parse_cover_from_description(const char *arg)\n \t\tdie(_(\"%s: invalid cover from description mode\"), arg);\n }\n \n+static void add_header(struct format_config *cfg, const char *value)\n+{\n+\tstruct string_list_item *item;\n+\tint len = strlen(value);\n+\twhile (len && value[len - 1] == '\\n')\n+\t\tlen--;\n+\n+\tif (!strncasecmp(value, \"to: \", 4)) {\n+\t\titem = string_list_append(&cfg->extra_to, value + 4);\n+\t\tlen -= 4;\n+\t} else if (!strncasecmp(value, \"cc: \", 4)) {\n+\t\titem = string_list_append(&cfg->extra_cc, value + 4);\n+\t\tlen -= 4;\n+\t} else {\n+\t\titem = string_list_append(&cfg->extra_hdr, value);\n+\t}\n+\n+\titem->string[len] = '\\0';\n+}\n+\n static int git_format_config(const char *var, const char *value,\n \t\t\t     const struct config_context *ctx, void *cb)\n {\n+\tstruct format_config *cfg = cb;\n+\n \tif (!strcmp(var, \"format.headers\")) {\n \t\tif (!value)\n \t\t\tdie(_(\"format.headers without value\"));\n-\t\tadd_header(value);\n+\t\tadd_header(cfg, value);\n \t\treturn 0;\n \t}\n-\tif (!strcmp(var, \"format.suffix\"))\n-\t\treturn git_config_string(&fmt_patch_suffix, var, value);\n+\tif (!strcmp(var, \"format.suffix\")) {\n+\t\tFREE_AND_NULL(cfg->fmt_patch_suffix);\n+\t\treturn git_config_string((const char **) &cfg->fmt_patch_suffix, var, value);\n+\t}\n \tif (!strcmp(var, \"format.to\")) {\n \t\tif (!value)\n \t\t\treturn config_error_nonbool(var);\n-\t\tstring_list_append(&extra_to, value);\n+\t\tstring_list_append(&cfg->extra_to, value);\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"format.cc\")) {\n \t\tif (!value)\n \t\t\treturn config_error_nonbool(var);\n-\t\tstring_list_append(&extra_cc, value);\n+\t\tstring_list_append(&cfg->extra_cc, value);\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"diff.color\") || !strcmp(var, \"color.diff\") ||\n@@ -1058,69 +1096,76 @@ static int git_format_config(const char *var, const char *value,\n \t}\n \tif (!strcmp(var, \"format.numbered\")) {\n \t\tif (value && !strcasecmp(value, \"auto\")) {\n-\t\t\tauto_number = 1;\n+\t\t\tcfg->auto_number = 1;\n \t\t\treturn 0;\n \t\t}\n-\t\tnumbered = git_config_bool(var, value);\n-\t\tauto_number = auto_number && numbered;\n+\t\tcfg->numbered = git_config_bool(var, value);\n+\t\tcfg->auto_number = cfg->auto_number && cfg->numbered;\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"format.attach\")) {\n-\t\tif (value && *value)\n-\t\t\tdefault_attach = xstrdup(value);\n-\t\telse if (value && !*value)\n-\t\t\tFREE_AND_NULL(default_attach);\n-\t\telse\n-\t\t\tdefault_attach = xstrdup(git_version_string);\n+\t\tif (value && *value) {\n+\t\t\tFREE_AND_NULL(cfg->default_attach);\n+\t\t\tcfg->default_attach = xstrdup(value);\n+\t\t} else if (value && !*value) {\n+\t\t\tFREE_AND_NULL(cfg->default_attach);\n+\t\t} else {\n+\t\t\tFREE_AND_NULL(cfg->default_attach);\n+\t\t\tcfg->default_attach = xstrdup(git_version_string);\n+\t\t}\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"format.thread\")) {\n \t\tif (value && !strcasecmp(value, \"deep\")) {\n-\t\t\tthread = THREAD_DEEP;\n+\t\t\tcfg->thread = THREAD_DEEP;\n \t\t\treturn 0;\n \t\t}\n \t\tif (value && !strcasecmp(value, \"shallow\")) {\n-\t\t\tthread = THREAD_SHALLOW;\n+\t\t\tcfg->thread = THREAD_SHALLOW;\n \t\t\treturn 0;\n \t\t}\n-\t\tthread = git_config_bool(var, value) ? THREAD_SHALLOW : THREAD_UNSET;\n+\t\tcfg->thread = git_config_bool(var, value) ? THREAD_SHALLOW : THREAD_UNSET;\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"format.signoff\")) {\n-\t\tdo_signoff = git_config_bool(var, value);\n+\t\tcfg->do_signoff = git_config_bool(var, value);\n \t\treturn 0;\n \t}\n-\tif (!strcmp(var, \"format.signature\"))\n-\t\treturn git_config_string(&signature, var, value);\n-\tif (!strcmp(var, \"format.signaturefile\"))\n-\t\treturn git_config_pathname(&signature_file, var, value);\n+\tif (!strcmp(var, \"format.signature\")) {\n+\t\tFREE_AND_NULL(cfg->signature);\n+\t\treturn git_config_string((const char **) &cfg->signature, var, value);\n+\t}\n+\tif (!strcmp(var, \"format.signaturefile\")) {\n+\t\tFREE_AND_NULL(cfg->signature_file);\n+\t\treturn git_config_pathname(&cfg->signature_file, var, value);\n+\t}\n \tif (!strcmp(var, \"format.coverletter\")) {\n \t\tif (value && !strcasecmp(value, \"auto\")) {\n-\t\t\tconfig_cover_letter = COVER_AUTO;\n+\t\t\tcfg->config_cover_letter = COVER_AUTO;\n \t\t\treturn 0;\n \t\t}\n-\t\tconfig_cover_letter = git_config_bool(var, value) ? COVER_ON : COVER_OFF;\n+\t\tcfg->config_cover_letter = git_config_bool(var, value) ? COVER_ON : COVER_OFF;\n \t\treturn 0;\n \t}\n-\tif (!strcmp(var, \"format.outputdirectory\"))\n-\t\treturn git_config_string(&config_output_directory, var, value);\n+\tif (!strcmp(var, \"format.outputdirectory\")) {\n+\t\tFREE_AND_NULL(cfg->config_output_directory);\n+\t\treturn git_config_string((const char **) &cfg->config_output_directory, var, value);\n+\t}\n \tif (!strcmp(var, \"format.useautobase\")) {\n \t\tif (value && !strcasecmp(value, \"whenAble\")) {\n-\t\t\tauto_base = AUTO_BASE_WHEN_ABLE;\n+\t\t\tcfg->auto_base = AUTO_BASE_WHEN_ABLE;\n \t\t\treturn 0;\n \t\t}\n-\t\tauto_base = git_config_bool(var, value) ? AUTO_BASE_ALWAYS : AUTO_BASE_NEVER;\n+\t\tcfg->auto_base = git_config_bool(var, value) ? AUTO_BASE_ALWAYS : AUTO_BASE_NEVER;\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"format.from\")) {\n \t\tint b = git_parse_maybe_bool(value);\n-\t\tfree(from);\n+\t\tFREE_AND_NULL(cfg->from);\n \t\tif (b < 0)\n-\t\t\tfrom = xstrdup(value);\n+\t\t\tcfg->from = xstrdup(value);\n \t\telse if (b)\n-\t\t\tfrom = xstrdup(git_committer_info(IDENT_NO_DATE));\n-\t\telse\n-\t\t\tfrom = NULL;\n+\t\t\tcfg->from = xstrdup(git_committer_info(IDENT_NO_DATE));\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"format.forceinbodyfrom\")) {\n@@ -1130,15 +1175,15 @@ static int git_format_config(const char *var, const char *value,\n \tif (!strcmp(var, \"format.notes\")) {\n \t\tint b = git_parse_maybe_bool(value);\n \t\tif (b < 0)\n-\t\t\tenable_ref_display_notes(&notes_opt, &show_notes, value);\n+\t\t\tenable_ref_display_notes(&cfg->notes_opt, &cfg->show_notes, value);\n \t\telse if (b)\n-\t\t\tenable_default_display_notes(&notes_opt, &show_notes);\n+\t\t\tenable_default_display_notes(&cfg->notes_opt, &cfg->show_notes);\n \t\telse\n-\t\t\tdisable_display_notes(&notes_opt, &show_notes);\n+\t\t\tdisable_display_notes(&cfg->notes_opt, &cfg->show_notes);\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"format.coverfromdescription\")) {\n-\t\tcover_from_description_mode = parse_cover_from_description(value);\n+\t\tcfg->cover_from_description_mode = parse_cover_from_description(value);\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"format.mboxrd\")) {\n@@ -1159,7 +1204,7 @@ static int git_format_config(const char *var, const char *value,\n \tif (!strcmp(var, \"diff.noprefix\"))\n \t\treturn 0;\n \n-\treturn git_log_config(var, value, ctx, cb);\n+\treturn git_log_config(var, value, ctx, &cfg->log);\n }\n \n static const char *output_directory = NULL;\n@@ -1247,7 +1292,7 @@ static void gen_message_id(struct rev_info *info, char *base)\n \tinfo->message_id = strbuf_detach(&buf, NULL);\n }\n \n-static void print_signature(FILE *file)\n+static void print_signature(const char *signature, FILE *file)\n {\n \tif (!signature || !*signature)\n \t\treturn;\n@@ -1317,14 +1362,15 @@ static void prepare_cover_text(struct pretty_print_context *pp,\n \t\t\t       const char *branch_name,\n \t\t\t       struct strbuf *sb,\n \t\t\t       const char *encoding,\n-\t\t\t       int need_8bit_cte)\n+\t\t\t       int need_8bit_cte,\n+\t\t\t       const struct format_config *cfg)\n {\n \tconst char *subject = \"*** SUBJECT HERE ***\";\n \tconst char *body = \"*** BLURB HERE ***\";\n \tstruct strbuf description_sb = STRBUF_INIT;\n \tstruct strbuf subject_sb = STRBUF_INIT;\n \n-\tif (cover_from_description_mode == COVER_FROM_NONE)\n+\tif (cfg->cover_from_description_mode == COVER_FROM_NONE)\n \t\tgoto do_pp;\n \n \tif (description_file && *description_file)\n@@ -1334,13 +1380,13 @@ static void prepare_cover_text(struct pretty_print_context *pp,\n \tif (!description_sb.len)\n \t\tgoto do_pp;\n \n-\tif (cover_from_description_mode == COVER_FROM_SUBJECT ||\n-\t\t\tcover_from_description_mode == COVER_FROM_AUTO)\n+\tif (cfg->cover_from_description_mode == COVER_FROM_SUBJECT ||\n+\t    cfg->cover_from_description_mode == COVER_FROM_AUTO)\n \t\tbody = format_subject(&subject_sb, description_sb.buf, \" \");\n \n-\tif (cover_from_description_mode == COVER_FROM_MESSAGE ||\n-\t\t\t(cover_from_description_mode == COVER_FROM_AUTO &&\n-\t\t\t subject_sb.len > COVER_FROM_AUTO_MAX_SUBJECT_LEN))\n+\tif (cfg->cover_from_description_mode == COVER_FROM_MESSAGE ||\n+\t    (cfg->cover_from_description_mode == COVER_FROM_AUTO &&\n+\t     subject_sb.len > COVER_FROM_AUTO_MAX_SUBJECT_LEN))\n \t\tbody = description_sb.buf;\n \telse\n \t\tsubject = subject_sb.buf;\n@@ -1377,7 +1423,8 @@ static void make_cover_letter(struct rev_info *rev, int use_separate_file,\n \t\t\t      int nr, struct commit **list,\n \t\t\t      const char *description_file,\n \t\t\t      const char *branch_name,\n-\t\t\t      int quiet)\n+\t\t\t      int quiet,\n+\t\t\t      const struct format_config *cfg)\n {\n \tconst char *committer;\n \tstruct shortlog log;\n@@ -1416,7 +1463,7 @@ static void make_cover_letter(struct rev_info *rev, int use_separate_file,\n \tpp.encode_email_headers = rev->encode_email_headers;\n \tpp_user_info(&pp, NULL, &sb, committer, encoding);\n \tprepare_cover_text(&pp, description_file, branch_name, &sb,\n-\t\t\t   encoding, need_8bit_cte);\n+\t\t\t   encoding, need_8bit_cte, cfg);\n \tfprintf(rev->diffopt.file, \"%s\\n\", sb.buf);\n \n \tfree(pp.after_subject);\n@@ -1517,29 +1564,30 @@ static const char * const builtin_format_patch_usage[] = {\n \tNULL\n };\n \n-static int keep_subject = 0;\n+struct keep_callback_data {\n+\tstruct format_config *cfg;\n+\tstruct rev_info *revs;\n+};\n \n static int keep_callback(const struct option *opt, const char *arg, int unset)\n {\n+\tstruct keep_callback_data *data = opt->value;\n \tBUG_ON_OPT_NEG(unset);\n \tBUG_ON_OPT_ARG(arg);\n-\t((struct rev_info *)opt->value)->total = -1;\n-\tkeep_subject = 1;\n+\tdata->revs->total = -1;\n+\tdata->cfg->keep_subject = 1;\n \treturn 0;\n }\n \n-static int subject_prefix = 0;\n-\n static int subject_prefix_callback(const struct option *opt, const char *arg,\n \t\t\t    int unset)\n {\n-\tstruct strbuf *sprefix;\n+\tstruct format_config *cfg = opt->value;\n \n \tBUG_ON_OPT_NEG(unset);\n-\tsprefix = opt->value;\n-\tsubject_prefix = 1;\n-\tstrbuf_reset(sprefix);\n-\tstrbuf_addstr(sprefix, arg);\n+\tcfg->subject_prefix = 1;\n+\tstrbuf_reset(&cfg->sprefix);\n+\tstrbuf_addstr(&cfg->sprefix, arg);\n \treturn 0;\n }\n \n@@ -1556,15 +1604,14 @@ static int rfc_callback(const struct option *opt, const char *arg,\n \treturn 0;\n }\n \n-static int numbered_cmdline_opt = 0;\n-\n static int numbered_callback(const struct option *opt, const char *arg,\n \t\t\t     int unset)\n {\n+\tstruct format_config *cfg = opt->value;\n \tBUG_ON_OPT_ARG(arg);\n-\t*(int *)opt->value = numbered_cmdline_opt = unset ? 0 : 1;\n+\tcfg->numbered = cfg->numbered_cmdline_opt = unset ? 0 : 1;\n \tif (unset)\n-\t\tauto_number =  0;\n+\t\tcfg->auto_number =  0;\n \treturn 0;\n }\n \n@@ -1588,13 +1635,14 @@ static int output_directory_callback(const struct option *opt, const char *arg,\n \n static int thread_callback(const struct option *opt, const char *arg, int unset)\n {\n-\tenum thread_level *thread = (enum thread_level *)opt->value;\n+\tstruct format_config *cfg = opt->value;\n+\n \tif (unset)\n-\t\t*thread = THREAD_UNSET;\n+\t\tcfg->thread = THREAD_UNSET;\n \telse if (!arg || !strcmp(arg, \"shallow\"))\n-\t\t*thread = THREAD_SHALLOW;\n+\t\tcfg->thread = THREAD_SHALLOW;\n \telse if (!strcmp(arg, \"deep\"))\n-\t\t*thread = THREAD_DEEP;\n+\t\tcfg->thread = THREAD_DEEP;\n \t/*\n \t * Please update _git_formatpatch() in git-completion.bash\n \t * when you add new options.\n@@ -1630,15 +1678,17 @@ static int inline_callback(const struct option *opt, const char *arg, int unset)\n \treturn 0;\n }\n \n-static int header_callback(const struct option *opt UNUSED, const char *arg,\n+static int header_callback(const struct option *opt, const char *arg,\n \t\t\t   int unset)\n {\n+\tstruct format_config *cfg = opt->value;\n+\n \tif (unset) {\n-\t\tstring_list_clear(&extra_hdr, 0);\n-\t\tstring_list_clear(&extra_to, 0);\n-\t\tstring_list_clear(&extra_cc, 0);\n+\t\tstring_list_clear(&cfg->extra_hdr, 0);\n+\t\tstring_list_clear(&cfg->extra_to, 0);\n+\t\tstring_list_clear(&cfg->extra_cc, 0);\n \t} else {\n-\t\tadd_header(arg);\n+\t\tadd_header(cfg, arg);\n \t}\n \treturn 0;\n }\n@@ -1660,17 +1710,17 @@ static int from_callback(const struct option *opt, const char *arg, int unset)\n \n static int base_callback(const struct option *opt, const char *arg, int unset)\n {\n-\tconst char **base_commit = opt->value;\n+\tstruct format_config *cfg = opt->value;\n \n \tif (unset) {\n-\t\tauto_base = AUTO_BASE_NEVER;\n-\t\t*base_commit = NULL;\n+\t\tcfg->auto_base = AUTO_BASE_NEVER;\n+\t\tFREE_AND_NULL(cfg->base_commit);\n \t} else if (!strcmp(arg, \"auto\")) {\n-\t\tauto_base = AUTO_BASE_ALWAYS;\n-\t\t*base_commit = NULL;\n+\t\tcfg->auto_base = AUTO_BASE_ALWAYS;\n+\t\tFREE_AND_NULL(cfg->base_commit);\n \t} else {\n-\t\tauto_base = AUTO_BASE_NEVER;\n-\t\t*base_commit = arg;\n+\t\tcfg->auto_base = AUTO_BASE_NEVER;\n+\t\tcfg->base_commit = xstrdup(arg);\n \t}\n \treturn 0;\n }\n@@ -1681,7 +1731,7 @@ struct base_tree_info {\n \tstruct object_id *patch_id;\n };\n \n-static struct commit *get_base_commit(const char *base_commit,\n+static struct commit *get_base_commit(const struct format_config *cfg,\n \t\t\t\t      struct commit **list,\n \t\t\t\t      int total)\n {\n@@ -1689,9 +1739,9 @@ static struct commit *get_base_commit(const char *base_commit,\n \tstruct commit **rev;\n \tint i = 0, rev_nr = 0, auto_select, die_on_failure, ret;\n \n-\tswitch (auto_base) {\n+\tswitch (cfg->auto_base) {\n \tcase AUTO_BASE_NEVER:\n-\t\tif (base_commit) {\n+\t\tif (cfg->base_commit) {\n \t\t\tauto_select = 0;\n \t\t\tdie_on_failure = 1;\n \t\t} else {\n@@ -1701,11 +1751,11 @@ static struct commit *get_base_commit(const char *base_commit,\n \t\tbreak;\n \tcase AUTO_BASE_ALWAYS:\n \tcase AUTO_BASE_WHEN_ABLE:\n-\t\tif (base_commit) {\n+\t\tif (cfg->base_commit) {\n \t\t\tBUG(\"requested automatic base selection but a commit was provided\");\n \t\t} else {\n \t\t\tauto_select = 1;\n-\t\t\tdie_on_failure = auto_base == AUTO_BASE_ALWAYS;\n+\t\t\tdie_on_failure = cfg->auto_base == AUTO_BASE_ALWAYS;\n \t\t}\n \t\tbreak;\n \tdefault:\n@@ -1713,9 +1763,9 @@ static struct commit *get_base_commit(const char *base_commit,\n \t}\n \n \tif (!auto_select) {\n-\t\tbase = lookup_commit_reference_by_name(base_commit);\n+\t\tbase = lookup_commit_reference_by_name(cfg->base_commit);\n \t\tif (!base)\n-\t\t\tdie(_(\"unknown commit %s\"), base_commit);\n+\t\t\tdie(_(\"unknown commit %s\"), cfg->base_commit);\n \t} else {\n \t\tstruct branch *curr_branch = branch_get(NULL);\n \t\tconst char *upstream = branch_get_upstream(curr_branch, NULL);\n@@ -1933,7 +1983,7 @@ static void infer_range_diff_ranges(struct strbuf *r1,\n \n int cmd_format_patch(int argc, const char **argv, const char *prefix)\n {\n-\tstruct log_config cfg;\n+\tstruct format_config cfg;\n \tstruct commit *commit;\n \tstruct commit **list = NULL;\n \tstruct rev_info rev;\n@@ -1958,7 +2008,6 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \tchar *cover_from_description_arg = NULL;\n \tchar *description_file = NULL;\n \tchar *branch_name = NULL;\n-\tchar *base_commit = NULL;\n \tstruct base_tree_info bases;\n \tstruct commit *base;\n \tint show_progress = 0;\n@@ -1969,18 +2018,24 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \tstruct strbuf rdiff1 = STRBUF_INIT;\n \tstruct strbuf rdiff2 = STRBUF_INIT;\n \tstruct strbuf rdiff_title = STRBUF_INIT;\n-\tstruct strbuf sprefix = STRBUF_INIT;\n \tconst char *rfc = NULL;\n \tint creation_factor = -1;\n+\tconst char *signature = git_version_string;\n+\tconst char *signature_file_arg = NULL;\n+\tstruct keep_callback_data keep_callback_data = {\n+\t\t.cfg = &cfg,\n+\t\t.revs = &rev,\n+\t};\n+\tconst char *fmt_patch_suffix = NULL;\n \n \tconst struct option builtin_format_patch_options[] = {\n-\t\tOPT_CALLBACK_F('n', \"numbered\", &numbered, NULL,\n+\t\tOPT_CALLBACK_F('n', \"numbered\", &cfg, NULL,\n \t\t\t    N_(\"use [PATCH n/m] even with a single patch\"),\n \t\t\t    PARSE_OPT_NOARG, numbered_callback),\n-\t\tOPT_CALLBACK_F('N', \"no-numbered\", &numbered, NULL,\n+\t\tOPT_CALLBACK_F('N', \"no-numbered\", &cfg, NULL,\n \t\t\t    N_(\"use [PATCH] even with multiple patches\"),\n \t\t\t    PARSE_OPT_NOARG | PARSE_OPT_NONEG, no_numbered_callback),\n-\t\tOPT_BOOL('s', \"signoff\", &do_signoff, N_(\"add a Signed-off-by trailer\")),\n+\t\tOPT_BOOL('s', \"signoff\", &cfg.do_signoff, N_(\"add a Signed-off-by trailer\")),\n \t\tOPT_BOOL(0, \"stdout\", &use_stdout,\n \t\t\t    N_(\"print patches to standard out\")),\n \t\tOPT_BOOL(0, \"cover-letter\", &cover_letter,\n@@ -1993,7 +2048,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\t\t    N_(\"start numbering patches at <n> instead of 1\")),\n \t\tOPT_STRING('v', \"reroll-count\", &reroll_count, N_(\"reroll-count\"),\n \t\t\t    N_(\"mark the series as Nth re-roll\")),\n-\t\tOPT_INTEGER(0, \"filename-max-length\", &cfg.fmt_patch_name_max,\n+\t\tOPT_INTEGER(0, \"filename-max-length\", &cfg.log.fmt_patch_name_max,\n \t\t\t    N_(\"max length of output filename\")),\n \t\tOPT_CALLBACK_F(0, \"rfc\", &rfc, N_(\"rfc\"),\n \t\t\t       N_(\"add <rfc> (default 'RFC') before 'PATCH'\"),\n@@ -2003,13 +2058,13 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\t\t    N_(\"generate parts of a cover letter based on a branch's description\")),\n \t\tOPT_FILENAME(0, \"description-file\", &description_file,\n \t\t\t     N_(\"use branch description from file\")),\n-\t\tOPT_CALLBACK_F(0, \"subject-prefix\", &sprefix, N_(\"prefix\"),\n+\t\tOPT_CALLBACK_F(0, \"subject-prefix\", &cfg, N_(\"prefix\"),\n \t\t\t    N_(\"use [<prefix>] instead of [PATCH]\"),\n \t\t\t    PARSE_OPT_NONEG, subject_prefix_callback),\n \t\tOPT_CALLBACK_F('o', \"output-directory\", &output_directory,\n \t\t\t    N_(\"dir\"), N_(\"store resulting files in <dir>\"),\n \t\t\t    PARSE_OPT_NONEG, output_directory_callback),\n-\t\tOPT_CALLBACK_F('k', \"keep-subject\", &rev, NULL,\n+\t\tOPT_CALLBACK_F('k', \"keep-subject\", &keep_callback_data, NULL,\n \t\t\t    N_(\"don't strip/add [PATCH]\"),\n \t\t\t    PARSE_OPT_NOARG | PARSE_OPT_NONEG, keep_callback),\n \t\tOPT_BOOL(0, \"no-binary\", &no_binary_diff,\n@@ -2022,11 +2077,11 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\t\t      N_(\"show patch format instead of default (patch + stat)\"),\n \t\t\t      1, PARSE_OPT_NONEG),\n \t\tOPT_GROUP(N_(\"Messaging\")),\n-\t\tOPT_CALLBACK(0, \"add-header\", NULL, N_(\"header\"),\n+\t\tOPT_CALLBACK(0, \"add-header\", &cfg, N_(\"header\"),\n \t\t\t    N_(\"add email header\"), header_callback),\n-\t\tOPT_STRING_LIST(0, \"to\", &extra_to, N_(\"email\"), N_(\"add To: header\")),\n-\t\tOPT_STRING_LIST(0, \"cc\", &extra_cc, N_(\"email\"), N_(\"add Cc: header\")),\n-\t\tOPT_CALLBACK_F(0, \"from\", &from, N_(\"ident\"),\n+\t\tOPT_STRING_LIST(0, \"to\", &cfg.extra_to, N_(\"email\"), N_(\"add To: header\")),\n+\t\tOPT_STRING_LIST(0, \"cc\", &cfg.extra_cc, N_(\"email\"), N_(\"add Cc: header\")),\n+\t\tOPT_CALLBACK_F(0, \"from\", &cfg.from, N_(\"ident\"),\n \t\t\t    N_(\"set From address to <ident> (or committer ident if absent)\"),\n \t\t\t    PARSE_OPT_OPTARG, from_callback),\n \t\tOPT_STRING(0, \"in-reply-to\", &in_reply_to, N_(\"message-id\"),\n@@ -2038,15 +2093,15 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\t\t    N_(\"inline the patch\"),\n \t\t\t    PARSE_OPT_OPTARG | PARSE_OPT_NONEG,\n \t\t\t    inline_callback),\n-\t\tOPT_CALLBACK_F(0, \"thread\", &thread, N_(\"style\"),\n+\t\tOPT_CALLBACK_F(0, \"thread\", &cfg, N_(\"style\"),\n \t\t\t    N_(\"enable message threading, styles: shallow, deep\"),\n \t\t\t    PARSE_OPT_OPTARG, thread_callback),\n \t\tOPT_STRING(0, \"signature\", &signature, N_(\"signature\"),\n \t\t\t    N_(\"add a signature\")),\n-\t\tOPT_CALLBACK_F(0, \"base\", &base_commit, N_(\"base-commit\"),\n+\t\tOPT_CALLBACK_F(0, \"base\", &cfg, N_(\"base-commit\"),\n \t\t\t       N_(\"add prerequisite tree info to the patch series\"),\n \t\t\t       0, base_callback),\n-\t\tOPT_FILENAME(0, \"signature-file\", &signature_file,\n+\t\tOPT_FILENAME(0, \"signature-file\", &signature_file_arg,\n \t\t\t\tN_(\"add a signature from a file\")),\n \t\tOPT__QUIET(&quiet, N_(\"don't print the patch filenames\")),\n \t\tOPT_BOOL(0, \"progress\", &show_progress,\n@@ -2063,21 +2118,17 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\tOPT_END()\n \t};\n \n-\textra_hdr.strdup_strings = 1;\n-\textra_to.strdup_strings = 1;\n-\textra_cc.strdup_strings = 1;\n-\n-\tlog_config_init(&cfg);\n+\tformat_config_init(&cfg);\n \tinit_diff_ui_defaults();\n-\tinit_display_notes(&notes_opt);\n+\tinit_display_notes(&cfg.notes_opt);\n \tgit_config(git_format_config, &cfg);\n \trepo_init_revisions(the_repository, &rev, prefix);\n \tgit_config(grep_config, &rev.grep_filter);\n \n-\trev.show_notes = show_notes;\n-\tmemcpy(&rev.notes_opt, &notes_opt, sizeof(notes_opt));\n+\trev.show_notes = cfg.show_notes;\n+\tmemcpy(&rev.notes_opt, &cfg.notes_opt, sizeof(cfg.notes_opt));\n \trev.commit_format = CMIT_FMT_EMAIL;\n-\trev.encode_email_headers = cfg.default_encode_email_headers;\n+\trev.encode_email_headers = cfg.log.default_encode_email_headers;\n \trev.expand_tabs_in_log_default = 0;\n \trev.verbose_header = 1;\n \trev.diff = 1;\n@@ -2088,12 +2139,12 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \ts_r_opt.def = \"HEAD\";\n \ts_r_opt.revarg_opt = REVARG_COMMITTISH;\n \n-\tstrbuf_addstr(&sprefix, cfg.fmt_patch_subject_prefix);\n+\tstrbuf_addstr(&cfg.sprefix, cfg.log.fmt_patch_subject_prefix);\n \tif (format_no_prefix)\n \t\tdiff_set_noprefix(&rev.diffopt);\n \n-\tif (default_attach) {\n-\t\trev.mime_boundary = default_attach;\n+\tif (cfg.default_attach) {\n+\t\trev.mime_boundary = cfg.default_attach;\n \t\trev.no_inline = 1;\n \t}\n \n@@ -2109,60 +2160,63 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \n \trev.force_in_body_from = force_in_body_from;\n \n+\tif (!fmt_patch_suffix)\n+\t\tfmt_patch_suffix = cfg.fmt_patch_suffix;\n+\n \t/* Make sure \"0000-$sub.patch\" gives non-negative length for $sub */\n-\tif (cfg.fmt_patch_name_max <= strlen(\"0000-\") + strlen(fmt_patch_suffix))\n-\t\tcfg.fmt_patch_name_max = strlen(\"0000-\") + strlen(fmt_patch_suffix);\n+\tif (cfg.log.fmt_patch_name_max <= strlen(\"0000-\") + strlen(fmt_patch_suffix))\n+\t\tcfg.log.fmt_patch_name_max = strlen(\"0000-\") + strlen(fmt_patch_suffix);\n \n \tif (cover_from_description_arg)\n-\t\tcover_from_description_mode = parse_cover_from_description(cover_from_description_arg);\n+\t\tcfg.cover_from_description_mode = parse_cover_from_description(cover_from_description_arg);\n \n \tif (rfc && rfc[0]) {\n-\t\tsubject_prefix = 1;\n+\t\tcfg.subject_prefix = 1;\n \t\tif (rfc[0] == '-')\n-\t\t\tstrbuf_addf(&sprefix, \" %s\", rfc + 1);\n+\t\t\tstrbuf_addf(&cfg.sprefix, \" %s\", rfc + 1);\n \t\telse\n-\t\t\tstrbuf_insertf(&sprefix, 0, \"%s \", rfc);\n+\t\t\tstrbuf_insertf(&cfg.sprefix, 0, \"%s \", rfc);\n \t}\n \n \tif (reroll_count) {\n-\t\tstrbuf_addf(&sprefix, \" v%s\", reroll_count);\n+\t\tstrbuf_addf(&cfg.sprefix, \" v%s\", reroll_count);\n \t\trev.reroll_count = reroll_count;\n \t}\n \n-\trev.subject_prefix = sprefix.buf;\n+\trev.subject_prefix = cfg.sprefix.buf;\n \n-\tfor (i = 0; i < extra_hdr.nr; i++) {\n-\t\tstrbuf_addstr(&buf, extra_hdr.items[i].string);\n+\tfor (i = 0; i < cfg.extra_hdr.nr; i++) {\n+\t\tstrbuf_addstr(&buf, cfg.extra_hdr.items[i].string);\n \t\tstrbuf_addch(&buf, '\\n');\n \t}\n \n-\tif (extra_to.nr)\n+\tif (cfg.extra_to.nr)\n \t\tstrbuf_addstr(&buf, \"To: \");\n-\tfor (i = 0; i < extra_to.nr; i++) {\n+\tfor (i = 0; i < cfg.extra_to.nr; i++) {\n \t\tif (i)\n \t\t\tstrbuf_addstr(&buf, \"    \");\n-\t\tstrbuf_addstr(&buf, extra_to.items[i].string);\n-\t\tif (i + 1 < extra_to.nr)\n+\t\tstrbuf_addstr(&buf, cfg.extra_to.items[i].string);\n+\t\tif (i + 1 < cfg.extra_to.nr)\n \t\t\tstrbuf_addch(&buf, ',');\n \t\tstrbuf_addch(&buf, '\\n');\n \t}\n \n-\tif (extra_cc.nr)\n+\tif (cfg.extra_cc.nr)\n \t\tstrbuf_addstr(&buf, \"Cc: \");\n-\tfor (i = 0; i < extra_cc.nr; i++) {\n+\tfor (i = 0; i < cfg.extra_cc.nr; i++) {\n \t\tif (i)\n \t\t\tstrbuf_addstr(&buf, \"    \");\n-\t\tstrbuf_addstr(&buf, extra_cc.items[i].string);\n-\t\tif (i + 1 < extra_cc.nr)\n+\t\tstrbuf_addstr(&buf, cfg.extra_cc.items[i].string);\n+\t\tif (i + 1 < cfg.extra_cc.nr)\n \t\t\tstrbuf_addch(&buf, ',');\n \t\tstrbuf_addch(&buf, '\\n');\n \t}\n \n \trev.extra_headers = to_free = strbuf_detach(&buf, NULL);\n \n-\tif (from) {\n-\t\tif (split_ident_line(&rev.from_ident, from, strlen(from)))\n-\t\t\tdie(_(\"invalid ident line: %s\"), from);\n+\tif (cfg.from) {\n+\t\tif (split_ident_line(&rev.from_ident, cfg.from, strlen(cfg.from)))\n+\t\t\tdie(_(\"invalid ident line: %s\"), cfg.from);\n \t}\n \n \tif (start_number < 0)\n@@ -2173,14 +2227,14 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t * and it would conflict with --keep-subject (-k) from the\n \t * command line, reset \"numbered\".\n \t */\n-\tif (numbered && keep_subject && !numbered_cmdline_opt)\n-\t\tnumbered = 0;\n+\tif (cfg.numbered && cfg.keep_subject && !cfg.numbered_cmdline_opt)\n+\t\tcfg.numbered = 0;\n \n-\tif (numbered && keep_subject)\n+\tif (cfg.numbered && cfg.keep_subject)\n \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"-n\", \"-k\");\n-\tif (keep_subject && subject_prefix)\n+\tif (cfg.keep_subject && cfg.subject_prefix)\n \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--subject-prefix/--rfc\", \"-k\");\n-\trev.preserve_subject = keep_subject;\n+\trev.preserve_subject = cfg.keep_subject;\n \n \targc = setup_revisions(argc, argv, &rev, &s_r_opt);\n \tif (argc > 1)\n@@ -2207,7 +2261,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \trev.always_show_header = 1;\n \n \trev.zero_commit = zero_commit;\n-\trev.patch_name_max = cfg.fmt_patch_name_max;\n+\trev.patch_name_max = cfg.log.fmt_patch_name_max;\n \n \tif (!rev.diffopt.flags.text && !no_binary_diff)\n \t\trev.diffopt.flags.binary = 1;\n@@ -2228,7 +2282,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\tint saved;\n \n \t\tif (!output_directory)\n-\t\t\toutput_directory = config_output_directory;\n+\t\t\toutput_directory = cfg.config_output_directory;\n \t\toutput_directory = set_outdir(prefix, output_directory);\n \n \t\tif (rev.diffopt.use_color != GIT_COLOR_ALWAYS)\n@@ -2326,14 +2380,14 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\tgoto done;\n \ttotal = nr;\n \tif (cover_letter == -1) {\n-\t\tif (config_cover_letter == COVER_AUTO)\n+\t\tif (cfg.config_cover_letter == COVER_AUTO)\n \t\t\tcover_letter = (total > 1);\n \t\telse\n-\t\t\tcover_letter = (config_cover_letter == COVER_ON);\n+\t\t\tcover_letter = (cfg.config_cover_letter == COVER_ON);\n \t}\n-\tif (!keep_subject && auto_number && (total > 1 || cover_letter))\n-\t\tnumbered = 1;\n-\tif (numbered)\n+\tif (!cfg.keep_subject && cfg.auto_number && (total > 1 || cover_letter))\n+\t\tcfg.numbered = 1;\n+\tif (cfg.numbered)\n \t\trev.total = total + start_number - 1;\n \n \tif (idiff_prev.nr) {\n@@ -2365,27 +2419,40 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\t\t\t\t     _(\"Range-diff against v%d:\"));\n \t}\n \n+\t/*\n+\t * The order of precedence is:\n+\t *\n+\t *   1. The `--signature` and `--no-signature` options.\n+\t *   2. The `--signature-file` option.\n+\t *   3. The `format.signature` config.\n+\t *   4. The `format.signatureFile` config.\n+\t *   5. Default `git_version_string`.\n+\t */\n \tif (!signature) {\n \t\t; /* --no-signature inhibits all signatures */\n \t} else if (signature && signature != git_version_string) {\n \t\t; /* non-default signature already set */\n-\t} else if (signature_file) {\n+\t} else if (signature_file_arg || (cfg.signature_file && !cfg.signature)) {\n \t\tstruct strbuf buf = STRBUF_INIT;\n+\t\tconst char *signature_file = signature_file_arg ?\n+\t\t\tsignature_file_arg : cfg.signature_file;\n \n \t\tif (strbuf_read_file(&buf, signature_file, 128) < 0)\n \t\t\tdie_errno(_(\"unable to read signature file '%s'\"), signature_file);\n \t\tsignature = strbuf_detach(&buf, NULL);\n+\t} else if (cfg.signature) {\n+\t\tsignature = cfg.signature;\n \t}\n \n \tmemset(&bases, 0, sizeof(bases));\n-\tbase = get_base_commit(base_commit, list, nr);\n+\tbase = get_base_commit(&cfg, list, nr);\n \tif (base) {\n \t\treset_revision_walk();\n \t\tclear_object_flags(UNINTERESTING);\n \t\tprepare_bases(&bases, base, list, nr);\n \t}\n \n-\tif (in_reply_to || thread || cover_letter) {\n+\tif (in_reply_to || cfg.thread || cover_letter) {\n \t\trev.ref_message_ids = xmalloc(sizeof(*rev.ref_message_ids));\n \t\tstring_list_init_dup(rev.ref_message_ids);\n \t}\n@@ -2396,19 +2463,19 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \trev.numbered_files = just_numbers;\n \trev.patch_suffix = fmt_patch_suffix;\n \tif (cover_letter) {\n-\t\tif (thread)\n+\t\tif (cfg.thread)\n \t\t\tgen_message_id(&rev, \"cover\");\n \t\tmake_cover_letter(&rev, !!output_directory,\n-\t\t\t\t  origin, nr, list, description_file, branch_name, quiet);\n+\t\t\t\t  origin, nr, list, description_file, branch_name, quiet, &cfg);\n \t\tprint_bases(&bases, rev.diffopt.file);\n-\t\tprint_signature(rev.diffopt.file);\n+\t\tprint_signature(signature, rev.diffopt.file);\n \t\ttotal++;\n \t\tstart_number--;\n \t\t/* interdiff/range-diff in cover-letter; omit from patches */\n \t\trev.idiff_oid1 = NULL;\n \t\trev.rdiff1 = NULL;\n \t}\n-\trev.add_signoff = do_signoff;\n+\trev.add_signoff = cfg.do_signoff;\n \n \tif (show_progress)\n \t\tprogress = start_delayed_progress(_(\"Generating patches\"), total);\n@@ -2418,7 +2485,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\tcommit = list[nr];\n \t\trev.nr = total - nr + (start_number - 1);\n \t\t/* Make the second and subsequent mails replies to the first */\n-\t\tif (thread) {\n+\t\tif (cfg.thread) {\n \t\t\t/* Have we already had a message ID? */\n \t\t\tif (rev.message_id) {\n \t\t\t\t/*\n@@ -2442,7 +2509,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\t\t\t * letter is a reply to the\n \t\t\t\t * --in-reply-to, if specified.\n \t\t\t\t */\n-\t\t\t\tif (thread == THREAD_SHALLOW\n+\t\t\t\tif (cfg.thread == THREAD_SHALLOW\n \t\t\t\t    && rev.ref_message_ids->nr > 0\n \t\t\t\t    && (!cover_letter || rev.nr > 1))\n \t\t\t\t\tfree(rev.message_id);\n@@ -2475,7 +2542,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\t\t\t       mime_boundary_leader,\n \t\t\t\t       rev.mime_boundary);\n \t\t\telse\n-\t\t\t\tprint_signature(rev.diffopt.file);\n+\t\t\t\tprint_signature(signature, rev.diffopt.file);\n \t\t}\n \t\tif (output_directory)\n \t\t\tfclose(rev.diffopt.file);\n@@ -2483,9 +2550,6 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \tstop_progress(&progress);\n \tfree(list);\n \tfree(branch_name);\n-\tstring_list_clear(&extra_to, 0);\n-\tstring_list_clear(&extra_cc, 0);\n-\tstring_list_clear(&extra_hdr, 0);\n \tif (ignore_if_in_upstream)\n \t\tfree_patch_ids(&ids);\n \n@@ -2495,14 +2559,13 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \tstrbuf_release(&rdiff1);\n \tstrbuf_release(&rdiff2);\n \tstrbuf_release(&rdiff_title);\n-\tstrbuf_release(&sprefix);\n \tfree(to_free);\n \tfree(rev.message_id);\n \tif (rev.ref_message_ids)\n \t\tstring_list_clear(rev.ref_message_ids, 0);\n \tfree(rev.ref_message_ids);\n \trelease_revisions(&rev);\n-\tlog_config_release(&cfg);\n+\tformat_config_release(&cfg);\n \treturn 0;\n }\n \n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495539","messageId":"6cfc28c7e2d976ae6f0e786ff2ebbf93621b53a3.1716541556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716541556.git.ps@pks.im","subject":"[PATCH v2 12/21] config: clarify memory ownership in `git_config_string()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-24T10:04:07Z","receivedAt":"2024-05-24T10:04:11Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The out parameter of `git_config_string()` is a `const char **` even\nthough we transfer ownership of memory to the caller. This is quite\nmisleading and has led to many memory leaks all over the place. Adapt\nthe parameter to instead be `char **`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n alias.c                |  2 +-\n attr.c                 |  2 +-\n attr.h                 |  2 +-\n builtin/commit.c       |  2 +-\n builtin/log.c          | 12 ++++++------\n builtin/merge.c        |  4 ++--\n builtin/rebase.c       |  2 +-\n builtin/receive-pack.c |  2 +-\n builtin/repack.c       |  8 ++++----\n config.c               |  4 ++--\n config.h               |  2 +-\n convert.c              |  6 +++---\n delta-islands.c        |  2 +-\n diff.c                 |  8 ++++----\n environment.c          |  8 ++++----\n environment.h          |  8 ++++----\n gpg-interface.c        |  4 ++--\n http.c                 | 24 ++++++++++++------------\n imap-send.c            | 12 ++++++------\n mailmap.c              |  2 +-\n mailmap.h              |  2 +-\n merge-ll.c             |  6 +++---\n pager.c                |  2 +-\n pretty.c               | 14 +++++++++-----\n promisor-remote.h      |  2 +-\n remote.c               | 20 ++++++++++----------\n remote.h               |  8 ++++----\n sequencer.c            |  2 +-\n upload-pack.c          |  2 +-\n userdiff.h             | 12 ++++++------\n 30 files changed, 95 insertions(+), 91 deletions(-)\n\ndiff --git a/alias.c b/alias.c\nindex 5a238f2e30..269892c356 100644\n--- a/alias.c\n+++ b/alias.c\n@@ -22,7 +22,7 @@ static int config_alias_cb(const char *key, const char *value,\n \n \tif (data->alias) {\n \t\tif (!strcasecmp(p, data->alias))\n-\t\t\treturn git_config_string((const char **)&data->v,\n+\t\t\treturn git_config_string(&data->v,\n \t\t\t\t\t\t key, value);\n \t} else if (data->list) {\n \t\tstring_list_append(data->list, p);\ndiff --git a/attr.c b/attr.c\nindex 33473bdce0..5607db2bbe 100644\n--- a/attr.c\n+++ b/attr.c\n@@ -25,7 +25,7 @@\n #include \"tree-walk.h\"\n #include \"object-name.h\"\n \n-const char *git_attr_tree;\n+char *git_attr_tree;\n \n const char git_attr__true[] = \"(builtin)true\";\n const char git_attr__false[] = \"\\0(builtin)false\";\ndiff --git a/attr.h b/attr.h\nindex 127998ae01..e7cc318b0c 100644\n--- a/attr.h\n+++ b/attr.h\n@@ -236,6 +236,6 @@ const char *git_attr_global_file(void);\n /* Return whether the system gitattributes file is enabled and should be used. */\n int git_attr_system_is_enabled(void);\n \n-extern const char *git_attr_tree;\n+extern char *git_attr_tree;\n \n #endif /* ATTR_H */\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 1cc88e92bf..f53e7e86ff 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -133,7 +133,7 @@ static struct strvec trailer_args = STRVEC_INIT;\n  * is specified explicitly.\n  */\n static enum commit_msg_cleanup_mode cleanup_mode;\n-static const char *cleanup_arg;\n+static char *cleanup_arg;\n \n static enum commit_whence whence;\n static int use_editor = 1, include_status = 1;\ndiff --git a/builtin/log.c b/builtin/log.c\nindex 890bf0c425..517d7982f1 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -582,11 +582,11 @@ static int git_log_config(const char *var, const char *value,\n \n \tif (!strcmp(var, \"format.pretty\")) {\n \t\tFREE_AND_NULL(cfg->fmt_pretty);\n-\t\treturn git_config_string((const char **) &cfg->fmt_pretty, var, value);\n+\t\treturn git_config_string(&cfg->fmt_pretty, var, value);\n \t}\n \tif (!strcmp(var, \"format.subjectprefix\")) {\n \t\tFREE_AND_NULL(cfg->fmt_patch_subject_prefix);\n-\t\treturn git_config_string((const char **) &cfg->fmt_patch_subject_prefix, var, value);\n+\t\treturn git_config_string(&cfg->fmt_patch_subject_prefix, var, value);\n \t}\n \tif (!strcmp(var, \"format.filenamemaxlength\")) {\n \t\tcfg->fmt_patch_name_max = git_config_int(var, value, ctx->kvi);\n@@ -602,7 +602,7 @@ static int git_log_config(const char *var, const char *value,\n \t}\n \tif (!strcmp(var, \"log.date\")) {\n \t\tFREE_AND_NULL(cfg->default_date_mode);\n-\t\treturn git_config_string((const char **) &cfg->default_date_mode, var, value);\n+\t\treturn git_config_string(&cfg->default_date_mode, var, value);\n \t}\n \tif (!strcmp(var, \"log.decorate\")) {\n \t\tcfg->decoration_style = parse_decoration_style(value);\n@@ -1076,7 +1076,7 @@ static int git_format_config(const char *var, const char *value,\n \t}\n \tif (!strcmp(var, \"format.suffix\")) {\n \t\tFREE_AND_NULL(cfg->fmt_patch_suffix);\n-\t\treturn git_config_string((const char **) &cfg->fmt_patch_suffix, var, value);\n+\t\treturn git_config_string(&cfg->fmt_patch_suffix, var, value);\n \t}\n \tif (!strcmp(var, \"format.to\")) {\n \t\tif (!value)\n@@ -1133,7 +1133,7 @@ static int git_format_config(const char *var, const char *value,\n \t}\n \tif (!strcmp(var, \"format.signature\")) {\n \t\tFREE_AND_NULL(cfg->signature);\n-\t\treturn git_config_string((const char **) &cfg->signature, var, value);\n+\t\treturn git_config_string(&cfg->signature, var, value);\n \t}\n \tif (!strcmp(var, \"format.signaturefile\")) {\n \t\tFREE_AND_NULL(cfg->signature_file);\n@@ -1149,7 +1149,7 @@ static int git_format_config(const char *var, const char *value,\n \t}\n \tif (!strcmp(var, \"format.outputdirectory\")) {\n \t\tFREE_AND_NULL(cfg->config_output_directory);\n-\t\treturn git_config_string((const char **) &cfg->config_output_directory, var, value);\n+\t\treturn git_config_string(&cfg->config_output_directory, var, value);\n \t}\n \tif (!strcmp(var, \"format.useautobase\")) {\n \t\tif (value && !strcasecmp(value, \"whenAble\")) {\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex e4bd65eeba..daed2d4e1e 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -100,7 +100,7 @@ static struct strategy all_strategy[] = {\n \t{ \"subtree\",    NO_FAST_FORWARD | NO_TRIVIAL },\n };\n \n-static const char *pull_twohead, *pull_octopus;\n+static char *pull_twohead, *pull_octopus;\n \n enum ff_type {\n \tFF_NO,\n@@ -110,7 +110,7 @@ enum ff_type {\n \n static enum ff_type fast_forward = FF_ALLOW;\n \n-static const char *cleanup_arg;\n+static char *cleanup_arg;\n static enum commit_msg_cleanup_mode cleanup_mode;\n \n static int option_parse_message(const struct option *opt,\ndiff --git a/builtin/rebase.c b/builtin/rebase.c\nindex 0466d9414a..14d4f0a5e6 100644\n--- a/builtin/rebase.c\n+++ b/builtin/rebase.c\n@@ -83,7 +83,7 @@ static const char *action_names[] = {\n struct rebase_options {\n \tenum rebase_type type;\n \tenum empty_type empty;\n-\tconst char *default_backend;\n+\tchar *default_backend;\n \tconst char *state_dir;\n \tstruct commit *upstream;\n \tconst char *upstream_name;\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 56228ad314..01c1f04ece 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -88,7 +88,7 @@ static struct strbuf push_cert = STRBUF_INIT;\n static struct object_id push_cert_oid;\n static struct signature_check sigcheck;\n static const char *push_cert_nonce;\n-static const char *cert_nonce_seed;\n+static char *cert_nonce_seed;\n static struct strvec hidden_refs = STRVEC_INIT;\n \n static const char *NONCE_UNSOLICITED = \"UNSOLICITED\";\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex 43491a4cbf..e40dceaada 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -48,10 +48,10 @@ static const char incremental_bitmap_conflict_error[] = N_(\n );\n \n struct pack_objects_args {\n-\tconst char *window;\n-\tconst char *window_memory;\n-\tconst char *depth;\n-\tconst char *threads;\n+\tchar *window;\n+\tchar *window_memory;\n+\tchar *depth;\n+\tchar *threads;\n \tunsigned long max_pack_size;\n \tint no_reuse_delta;\n \tint no_reuse_object;\ndiff --git a/config.c b/config.c\nindex f9101045ee..a025cfafe0 100644\n--- a/config.c\n+++ b/config.c\n@@ -1338,7 +1338,7 @@ int git_config_bool(const char *name, const char *value)\n \treturn v;\n }\n \n-int git_config_string(const char **dest, const char *var, const char *value)\n+int git_config_string(char **dest, const char *var, const char *value)\n {\n \tif (!value)\n \t\treturn config_error_nonbool(var);\n@@ -2418,7 +2418,7 @@ int git_configset_get_string(struct config_set *set, const char *key, char **des\n {\n \tconst char *value;\n \tif (!git_configset_get_value(set, key, &value, NULL))\n-\t\treturn git_config_string((const char **)dest, key, value);\n+\t\treturn git_config_string(dest, key, value);\n \telse\n \t\treturn 1;\n }\ndiff --git a/config.h b/config.h\nindex b3103bba94..2d2e22ed64 100644\n--- a/config.h\n+++ b/config.h\n@@ -280,7 +280,7 @@ int git_config_bool(const char *, const char *);\n  * Allocates and copies the value string into the `dest` parameter; if no\n  * string is given, prints an error message and returns -1.\n  */\n-int git_config_string(const char **, const char *, const char *);\n+int git_config_string(char **, const char *, const char *);\n \n /**\n  * Similar to `git_config_string`, but expands `~` or `~user` into the\ndiff --git a/convert.c b/convert.c\nindex 03c3c528f9..f2b9f01354 100644\n--- a/convert.c\n+++ b/convert.c\n@@ -981,9 +981,9 @@ int async_query_available_blobs(const char *cmd, struct string_list *available_p\n static struct convert_driver {\n \tconst char *name;\n \tstruct convert_driver *next;\n-\tconst char *smudge;\n-\tconst char *clean;\n-\tconst char *process;\n+\tchar *smudge;\n+\tchar *clean;\n+\tchar *process;\n \tint required;\n } *user_convert, **user_convert_tail;\n \ndiff --git a/delta-islands.c b/delta-islands.c\nindex 4ac3c10551..89d51b72e3 100644\n--- a/delta-islands.c\n+++ b/delta-islands.c\n@@ -313,7 +313,7 @@ struct island_load_data {\n \tsize_t nr;\n \tsize_t alloc;\n };\n-static const char *core_island_name;\n+static char *core_island_name;\n \n static void free_config_regexes(struct island_load_data *ild)\n {\ndiff --git a/diff.c b/diff.c\nindex 679ef472f4..e70301df76 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -56,8 +56,8 @@ static int diff_color_moved_default;\n static int diff_color_moved_ws_default;\n static int diff_context_default = 3;\n static int diff_interhunk_context_default;\n-static const char *diff_word_regex_cfg;\n-static const char *external_diff_cmd_cfg;\n+static char *diff_word_regex_cfg;\n+static char *external_diff_cmd_cfg;\n static char *diff_order_file_cfg;\n int diff_auto_refresh_index = 1;\n static int diff_mnemonic_prefix;\n@@ -412,11 +412,11 @@ int git_diff_ui_config(const char *var, const char *value,\n \t}\n \tif (!strcmp(var, \"diff.srcprefix\")) {\n \t\tFREE_AND_NULL(diff_src_prefix);\n-\t\treturn git_config_string((const char **) &diff_src_prefix, var, value);\n+\t\treturn git_config_string(&diff_src_prefix, var, value);\n \t}\n \tif (!strcmp(var, \"diff.dstprefix\")) {\n \t\tFREE_AND_NULL(diff_dst_prefix);\n-\t\treturn git_config_string((const char **) &diff_dst_prefix, var, value);\n+\t\treturn git_config_string(&diff_dst_prefix, var, value);\n \t}\n \tif (!strcmp(var, \"diff.relative\")) {\n \t\tdiff_relative = git_config_bool(var, value);\ndiff --git a/environment.c b/environment.c\nindex ab6956559e..701d515135 100644\n--- a/environment.c\n+++ b/environment.c\n@@ -42,8 +42,8 @@ int is_bare_repository_cfg = -1; /* unspecified */\n int warn_ambiguous_refs = 1;\n int warn_on_object_refname_ambiguity = 1;\n int repository_format_precious_objects;\n-const char *git_commit_encoding;\n-const char *git_log_output_encoding;\n+char *git_commit_encoding;\n+char *git_log_output_encoding;\n char *apply_default_whitespace;\n char *apply_default_ignorewhitespace;\n char *git_attributes_file;\n@@ -58,8 +58,8 @@ size_t packed_git_window_size = DEFAULT_PACKED_GIT_WINDOW_SIZE;\n size_t packed_git_limit = DEFAULT_PACKED_GIT_LIMIT;\n size_t delta_base_cache_limit = 96 * 1024 * 1024;\n unsigned long big_file_threshold = 512 * 1024 * 1024;\n-const char *editor_program;\n-const char *askpass_program;\n+char *editor_program;\n+char *askpass_program;\n char *excludes_file;\n enum auto_crlf auto_crlf = AUTO_CRLF_FALSE;\n enum eol core_eol = EOL_UNSET;\ndiff --git a/environment.h b/environment.h\nindex be1b88ad6f..e9f01d4d11 100644\n--- a/environment.h\n+++ b/environment.h\n@@ -224,11 +224,11 @@ int odb_pack_keep(const char *name);\n const char *get_log_output_encoding(void);\n const char *get_commit_output_encoding(void);\n \n-extern const char *git_commit_encoding;\n-extern const char *git_log_output_encoding;\n+extern char *git_commit_encoding;\n+extern char *git_log_output_encoding;\n \n-extern const char *editor_program;\n-extern const char *askpass_program;\n+extern char *editor_program;\n+extern char *askpass_program;\n extern char *excludes_file;\n \n /*\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 2b50ed0fa0..5193223714 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -27,14 +27,14 @@ static void gpg_interface_lazy_init(void)\n }\n \n static char *configured_signing_key;\n-static const char *ssh_default_key_command;\n+static char *ssh_default_key_command;\n static char *ssh_allowed_signers;\n static char *ssh_revocation_file;\n static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;\n \n struct gpg_format {\n \tconst char *name;\n-\tconst char *program;\n+\tchar *program;\n \tconst char **verify_args;\n \tconst char **sigs;\n \tint (*verify_signed_buffer)(struct signature_check *sigc,\ndiff --git a/http.c b/http.c\nindex fa3ea87451..67cc47d28f 100644\n--- a/http.c\n+++ b/http.c\n@@ -38,11 +38,11 @@ char curl_errorstr[CURL_ERROR_SIZE];\n \n static int curl_ssl_verify = -1;\n static int curl_ssl_try;\n-static const char *curl_http_version = NULL;\n+static char *curl_http_version;\n static char *ssl_cert;\n static char *ssl_cert_type;\n-static const char *ssl_cipherlist;\n-static const char *ssl_version;\n+static char *ssl_cipherlist;\n+static char *ssl_version;\n static struct {\n \tconst char *name;\n \tlong ssl_version;\n@@ -95,7 +95,7 @@ static struct {\n \t */\n };\n #ifdef CURLGSSAPI_DELEGATION_FLAG\n-static const char *curl_deleg;\n+static char *curl_deleg;\n static struct {\n \tconst char *name;\n \tlong curl_deleg_param;\n@@ -383,11 +383,11 @@ static int http_options(const char *var, const char *value,\n \tif (!strcmp(\"http.sslcert\", var))\n \t\treturn git_config_pathname(&ssl_cert, var, value);\n \tif (!strcmp(\"http.sslcerttype\", var))\n-\t\treturn git_config_string((const char **)&ssl_cert_type, var, value);\n+\t\treturn git_config_string(&ssl_cert_type, var, value);\n \tif (!strcmp(\"http.sslkey\", var))\n \t\treturn git_config_pathname(&ssl_key, var, value);\n \tif (!strcmp(\"http.sslkeytype\", var))\n-\t\treturn git_config_string((const char **)&ssl_key_type, var, value);\n+\t\treturn git_config_string(&ssl_key_type, var, value);\n \tif (!strcmp(\"http.sslcapath\", var))\n \t\treturn git_config_pathname(&ssl_capath, var, value);\n \tif (!strcmp(\"http.sslcainfo\", var))\n@@ -440,19 +440,19 @@ static int http_options(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \tif (!strcmp(\"http.proxy\", var))\n-\t\treturn git_config_string((const char **)&curl_http_proxy, var, value);\n+\t\treturn git_config_string(&curl_http_proxy, var, value);\n \n \tif (!strcmp(\"http.proxyauthmethod\", var))\n-\t\treturn git_config_string((const char **)&http_proxy_authmethod, var, value);\n+\t\treturn git_config_string(&http_proxy_authmethod, var, value);\n \n \tif (!strcmp(\"http.proxysslcert\", var))\n-\t\treturn git_config_string((const char **)&http_proxy_ssl_cert, var, value);\n+\t\treturn git_config_string(&http_proxy_ssl_cert, var, value);\n \n \tif (!strcmp(\"http.proxysslkey\", var))\n-\t\treturn git_config_string((const char **)&http_proxy_ssl_key, var, value);\n+\t\treturn git_config_string(&http_proxy_ssl_key, var, value);\n \n \tif (!strcmp(\"http.proxysslcainfo\", var))\n-\t\treturn git_config_string((const char **)&http_proxy_ssl_ca_info, var, value);\n+\t\treturn git_config_string(&http_proxy_ssl_ca_info, var, value);\n \n \tif (!strcmp(\"http.proxysslcertpasswordprotected\", var)) {\n \t\tproxy_ssl_cert_password_required = git_config_bool(var, value);\n@@ -476,7 +476,7 @@ static int http_options(const char *var, const char *value,\n \t}\n \n \tif (!strcmp(\"http.useragent\", var))\n-\t\treturn git_config_string((const char **)&user_agent, var, value);\n+\t\treturn git_config_string(&user_agent, var, value);\n \n \tif (!strcmp(\"http.emptyauth\", var)) {\n \t\tif (value && !strcmp(\"auto\", value))\ndiff --git a/imap-send.c b/imap-send.c\nindex c0130d0e02..a5d1510180 100644\n--- a/imap-send.c\n+++ b/imap-send.c\n@@ -70,16 +70,16 @@ static char *next_arg(char **);\n \n struct imap_server_conf {\n \tconst char *name;\n-\tconst char *tunnel;\n-\tconst char *host;\n+\tchar *tunnel;\n+\tchar *host;\n \tint port;\n-\tconst char *folder;\n-\tconst char *user;\n-\tconst char *pass;\n+\tchar *folder;\n+\tchar *user;\n+\tchar *pass;\n \tint use_ssl;\n \tint ssl_verify;\n \tint use_html;\n-\tconst char *auth_method;\n+\tchar *auth_method;\n };\n \n static struct imap_server_conf server = {\ndiff --git a/mailmap.c b/mailmap.c\nindex 044466b043..b2efe29b3d 100644\n--- a/mailmap.c\n+++ b/mailmap.c\n@@ -7,7 +7,7 @@\n #include \"setup.h\"\n \n char *git_mailmap_file;\n-const char *git_mailmap_blob;\n+char *git_mailmap_blob;\n \n struct mailmap_info {\n \tchar *name;\ndiff --git a/mailmap.h b/mailmap.h\nindex 429a760945..cbda9bc5e0 100644\n--- a/mailmap.h\n+++ b/mailmap.h\n@@ -4,7 +4,7 @@\n struct string_list;\n \n extern char *git_mailmap_file;\n-extern const char *git_mailmap_blob;\n+extern char *git_mailmap_blob;\n \n int read_mailmap(struct string_list *map);\n void clear_mailmap(struct string_list *map);\ndiff --git a/merge-ll.c b/merge-ll.c\nindex bf1077ae09..e29b15fa4a 100644\n--- a/merge-ll.c\n+++ b/merge-ll.c\n@@ -27,9 +27,9 @@ typedef enum ll_merge_result (*ll_merge_fn)(const struct ll_merge_driver *,\n \n struct ll_merge_driver {\n \tconst char *name;\n-\tconst char *description;\n+\tchar *description;\n \tll_merge_fn fn;\n-\tconst char *recursive;\n+\tchar *recursive;\n \tstruct ll_merge_driver *next;\n \tchar *cmdline;\n };\n@@ -268,7 +268,7 @@ static enum ll_merge_result ll_ext_merge(const struct ll_merge_driver *fn,\n  * merge.default and merge.driver configuration items\n  */\n static struct ll_merge_driver *ll_user_merge, **ll_user_merge_tail;\n-static const char *default_ll_merge;\n+static char *default_ll_merge;\n \n static int read_merge_config(const char *var, const char *value,\n \t\t\t     const struct config_context *ctx UNUSED,\ndiff --git a/pager.c b/pager.c\nindex b8822a9381..e9e121db69 100644\n--- a/pager.c\n+++ b/pager.c\n@@ -13,7 +13,7 @@ int pager_use_color = 1;\n #endif\n \n static struct child_process pager_process;\n-static const char *pager_program;\n+static char *pager_program;\n \n /* Is the value coming back from term_columns() just a guess? */\n static int term_columns_guessed;\ndiff --git a/pretty.c b/pretty.c\nindex 7ead078998..22a81506b7 100644\n--- a/pretty.c\n+++ b/pretty.c\n@@ -62,7 +62,7 @@ static int git_pretty_formats_config(const char *var, const char *value,\n {\n \tstruct cmt_fmt_map *commit_format = NULL;\n \tconst char *name;\n-\tconst char *fmt;\n+\tchar *fmt;\n \tint i;\n \n \tif (!skip_prefix(var, \"pretty.\", &name))\n@@ -93,13 +93,17 @@ static int git_pretty_formats_config(const char *var, const char *value,\n \tif (git_config_string(&fmt, var, value))\n \t\treturn -1;\n \n-\tif (skip_prefix(fmt, \"format:\", &fmt))\n+\tif (skip_prefix(fmt, \"format:\", &commit_format->user_format)) {\n \t\tcommit_format->is_tformat = 0;\n-\telse if (skip_prefix(fmt, \"tformat:\", &fmt) || strchr(fmt, '%'))\n+\t} else if (skip_prefix(fmt, \"tformat:\", &commit_format->user_format)) {\n \t\tcommit_format->is_tformat = 1;\n-\telse\n+\t} else if (strchr(fmt, '%')) {\n+\t\tcommit_format->is_tformat = 1;\n+\t\tcommit_format->user_format = fmt;\n+\t} else {\n \t\tcommit_format->is_alias = 1;\n-\tcommit_format->user_format = fmt;\n+\t\tcommit_format->user_format = fmt;\n+\t}\n \n \treturn 0;\n }\ndiff --git a/promisor-remote.h b/promisor-remote.h\nindex 2cb9eda9ea..88cb599c39 100644\n--- a/promisor-remote.h\n+++ b/promisor-remote.h\n@@ -13,7 +13,7 @@ struct object_id;\n  */\n struct promisor_remote {\n \tstruct promisor_remote *next;\n-\tconst char *partial_clone_filter;\n+\tchar *partial_clone_filter;\n \tconst char name[FLEX_ARRAY];\n };\n \ndiff --git a/remote.c b/remote.c\nindex ec8c158e60..d319f28757 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -428,29 +428,29 @@ static int handle_config(const char *key, const char *value,\n \telse if (!strcmp(subkey, \"prunetags\"))\n \t\tremote->prune_tags = git_config_bool(key, value);\n \telse if (!strcmp(subkey, \"url\")) {\n-\t\tconst char *v;\n+\t\tchar *v;\n \t\tif (git_config_string(&v, key, value))\n \t\t\treturn -1;\n \t\tadd_url(remote, v);\n \t} else if (!strcmp(subkey, \"pushurl\")) {\n-\t\tconst char *v;\n+\t\tchar *v;\n \t\tif (git_config_string(&v, key, value))\n \t\t\treturn -1;\n \t\tadd_pushurl(remote, v);\n \t} else if (!strcmp(subkey, \"push\")) {\n-\t\tconst char *v;\n+\t\tchar *v;\n \t\tif (git_config_string(&v, key, value))\n \t\t\treturn -1;\n \t\trefspec_append(&remote->push, v);\n-\t\tfree((char *)v);\n+\t\tfree(v);\n \t} else if (!strcmp(subkey, \"fetch\")) {\n-\t\tconst char *v;\n+\t\tchar *v;\n \t\tif (git_config_string(&v, key, value))\n \t\t\treturn -1;\n \t\trefspec_append(&remote->fetch, v);\n-\t\tfree((char *)v);\n+\t\tfree(v);\n \t} else if (!strcmp(subkey, \"receivepack\")) {\n-\t\tconst char *v;\n+\t\tchar *v;\n \t\tif (git_config_string(&v, key, value))\n \t\t\treturn -1;\n \t\tif (!remote->receivepack)\n@@ -458,7 +458,7 @@ static int handle_config(const char *key, const char *value,\n \t\telse\n \t\t\terror(_(\"more than one receivepack given, using the first\"));\n \t} else if (!strcmp(subkey, \"uploadpack\")) {\n-\t\tconst char *v;\n+\t\tchar *v;\n \t\tif (git_config_string(&v, key, value))\n \t\t\treturn -1;\n \t\tif (!remote->uploadpack)\n@@ -471,10 +471,10 @@ static int handle_config(const char *key, const char *value,\n \t\telse if (!strcmp(value, \"--tags\"))\n \t\t\tremote->fetch_tags = 2;\n \t} else if (!strcmp(subkey, \"proxy\")) {\n-\t\treturn git_config_string((const char **)&remote->http_proxy,\n+\t\treturn git_config_string(&remote->http_proxy,\n \t\t\t\t\t key, value);\n \t} else if (!strcmp(subkey, \"proxyauthmethod\")) {\n-\t\treturn git_config_string((const char **)&remote->http_proxy_authmethod,\n+\t\treturn git_config_string(&remote->http_proxy_authmethod,\n \t\t\t\t\t key, value);\n \t} else if (!strcmp(subkey, \"vcs\")) {\n \t\treturn git_config_string(&remote->foreign_vcs, key, value);\ndiff --git a/remote.h b/remote.h\nindex dfd4837e60..e8c6655e42 100644\n--- a/remote.h\n+++ b/remote.h\n@@ -46,7 +46,7 @@ struct remote_state {\n \tstruct hashmap branches_hash;\n \n \tstruct branch *current_branch;\n-\tconst char *pushremote_name;\n+\tchar *pushremote_name;\n \n \tstruct rewrites rewrites;\n \tstruct rewrites rewrites_push;\n@@ -65,7 +65,7 @@ struct remote {\n \n \tint origin, configured_in_repo;\n \n-\tconst char *foreign_vcs;\n+\tchar *foreign_vcs;\n \n \t/* An array of all of the url_nr URLs configured for the remote */\n \tconst char **url;\n@@ -309,9 +309,9 @@ struct branch {\n \tconst char *refname;\n \n \t/* The name of the remote listed in the configuration. */\n-\tconst char *remote_name;\n+\tchar *remote_name;\n \n-\tconst char *pushremote_name;\n+\tchar *pushremote_name;\n \n \t/* An array of the \"merge\" lines in the configuration. */\n \tconst char **merge_name;\ndiff --git a/sequencer.c b/sequencer.c\nindex dbd60d79b9..3c81ef9ca5 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -306,7 +306,7 @@ static int git_sequencer_config(const char *k, const char *v,\n \t}\n \n \tif (!opts->default_strategy && !strcmp(k, \"pull.twohead\")) {\n-\t\tint ret = git_config_string((const char**)&opts->default_strategy, k, v);\n+\t\tint ret = git_config_string(&opts->default_strategy, k, v);\n \t\tif (ret == 0) {\n \t\t\t/*\n \t\t\t * pull.twohead is allowed to be multi-valued; we only\ndiff --git a/upload-pack.c b/upload-pack.c\nindex 8fbd138515..5801eb2639 100644\n--- a/upload-pack.c\n+++ b/upload-pack.c\n@@ -94,7 +94,7 @@ struct upload_pack_data {\n \n \tstruct packet_writer writer;\n \n-\tconst char *pack_objects_hook;\n+\tchar *pack_objects_hook;\n \n \tunsigned stateless_rpc : 1;\t\t\t\t/* v0 only */\n \tunsigned no_done : 1;\t\t\t\t\t/* v0 only */\ndiff --git a/userdiff.h b/userdiff.h\nindex d726804c3e..cc8e5abfef 100644\n--- a/userdiff.h\n+++ b/userdiff.h\n@@ -7,19 +7,19 @@ struct index_state;\n struct repository;\n \n struct userdiff_funcname {\n-\tconst char *pattern;\n+\tchar *pattern;\n \tint cflags;\n };\n \n struct userdiff_driver {\n \tconst char *name;\n-\tconst char *external;\n-\tconst char *algorithm;\n+\tchar *external;\n+\tchar *algorithm;\n \tint binary;\n \tstruct userdiff_funcname funcname;\n-\tconst char *word_regex;\n-\tconst char *word_regex_multi_byte;\n-\tconst char *textconv;\n+\tchar *word_regex;\n+\tchar *word_regex_multi_byte;\n+\tchar *textconv;\n \tstruct notes_cache *textconv_cache;\n \tint textconv_want_cache;\n };\n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495540","messageId":"70e8e2651306e9d221e5e472720a7610947580a7.1716541556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716541556.git.ps@pks.im","subject":"[PATCH v2 13/21] config: plug various memory leaks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-24T10:04:12Z","receivedAt":"2024-05-24T10:04:16Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Now that memory ownership rules around `git_config_string()` and\n`git_config_pathname()` are clearer, it also got easier to spot that\nthe returned memory needs to be free'd. Plug a subset of those cases and\nmark now-passing tests as leak free.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n alias.c                                      |  4 ++-\n config.c                                     | 38 ++++++++++++++------\n t/t1306-xdg-files.sh                         |  1 +\n t/t1350-config-hooks-path.sh                 |  1 +\n t/t3415-rebase-autosquash.sh                 |  1 +\n t/t4041-diff-submodule-option.sh             |  1 +\n t/t4060-diff-submodule-option-diff-format.sh |  1 +\n t/t4210-log-i18n.sh                          |  2 ++\n t/t6006-rev-list-format.sh                   |  1 +\n t/t7005-editor.sh                            |  1 +\n t/t7102-reset.sh                             |  1 +\n t/t9129-git-svn-i18n-commitencoding.sh       |  1 -\n t/t9139-git-svn-non-utf8-commitencoding.sh   |  1 -\n 13 files changed, 41 insertions(+), 13 deletions(-)\n\ndiff --git a/alias.c b/alias.c\nindex 269892c356..4daafd9bda 100644\n--- a/alias.c\n+++ b/alias.c\n@@ -21,9 +21,11 @@ static int config_alias_cb(const char *key, const char *value,\n \t\treturn 0;\n \n \tif (data->alias) {\n-\t\tif (!strcasecmp(p, data->alias))\n+\t\tif (!strcasecmp(p, data->alias)) {\n+\t\t\tFREE_AND_NULL(data->v);\n \t\t\treturn git_config_string(&data->v,\n \t\t\t\t\t\t key, value);\n+\t\t}\n \t} else if (data->list) {\n \t\tstring_list_append(data->list, p);\n \t}\ndiff --git a/config.c b/config.c\nindex a025cfafe0..496cd1a61e 100644\n--- a/config.c\n+++ b/config.c\n@@ -1414,8 +1414,10 @@ static int git_default_core_config(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \n-\tif (!strcmp(var, \"core.attributesfile\"))\n+\tif (!strcmp(var, \"core.attributesfile\")) {\n+\t\tFREE_AND_NULL(git_attributes_file);\n \t\treturn git_config_pathname(&git_attributes_file, var, value);\n+\t}\n \n \tif (!strcmp(var, \"core.hookspath\")) {\n \t\tif (ctx->kvi && ctx->kvi->scope == CONFIG_SCOPE_LOCAL &&\n@@ -1428,6 +1430,7 @@ static int git_default_core_config(const char *var, const char *value,\n \t\t\t      \"again with \"\n \t\t\t      \"`GIT_CLONE_PROTECTION_ACTIVE=false`\"),\n \t\t\t    value);\n+\t\tFREE_AND_NULL(git_hooks_path);\n \t\treturn git_config_pathname(&git_hooks_path, var, value);\n \t}\n \n@@ -1566,7 +1569,7 @@ static int git_default_core_config(const char *var, const char *value,\n \n \tif (!strcmp(var, \"core.checkroundtripencoding\")) {\n \t\tFREE_AND_NULL(check_roundtrip_encoding);\n-\t\treturn git_config_string((const char **) &check_roundtrip_encoding, var, value);\n+\t\treturn git_config_string(&check_roundtrip_encoding, var, value);\n \t}\n \n \tif (!strcmp(var, \"core.notesref\")) {\n@@ -1576,8 +1579,10 @@ static int git_default_core_config(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \n-\tif (!strcmp(var, \"core.editor\"))\n+\tif (!strcmp(var, \"core.editor\")) {\n+\t\tFREE_AND_NULL(editor_program);\n \t\treturn git_config_string(&editor_program, var, value);\n+\t}\n \n \tif (!strcmp(var, \"core.commentchar\") ||\n \t    !strcmp(var, \"core.commentstring\")) {\n@@ -1595,11 +1600,13 @@ static int git_default_core_config(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \n-\tif (!strcmp(var, \"core.askpass\"))\n+\tif (!strcmp(var, \"core.askpass\")) {\n+\t\tFREE_AND_NULL(askpass_program);\n \t\treturn git_config_string(&askpass_program, var, value);\n+\t}\n \n \tif (!strcmp(var, \"core.excludesfile\")) {\n-\t\tfree(excludes_file);\n+\t\tFREE_AND_NULL(excludes_file);\n \t\treturn git_config_pathname(&excludes_file, var, value);\n \t}\n \n@@ -1702,11 +1709,15 @@ static int git_default_sparse_config(const char *var, const char *value)\n \n static int git_default_i18n_config(const char *var, const char *value)\n {\n-\tif (!strcmp(var, \"i18n.commitencoding\"))\n+\tif (!strcmp(var, \"i18n.commitencoding\")) {\n+\t\tFREE_AND_NULL(git_commit_encoding);\n \t\treturn git_config_string(&git_commit_encoding, var, value);\n+\t}\n \n-\tif (!strcmp(var, \"i18n.logoutputencoding\"))\n+\tif (!strcmp(var, \"i18n.logoutputencoding\")) {\n+\t\tFREE_AND_NULL(git_log_output_encoding);\n \t\treturn git_config_string(&git_log_output_encoding, var, value);\n+\t}\n \n \t/* Add other config variables here and to Documentation/config.txt. */\n \treturn 0;\n@@ -1779,10 +1790,15 @@ static int git_default_push_config(const char *var, const char *value)\n \n static int git_default_mailmap_config(const char *var, const char *value)\n {\n-\tif (!strcmp(var, \"mailmap.file\"))\n+\tif (!strcmp(var, \"mailmap.file\")) {\n+\t\tFREE_AND_NULL(git_mailmap_file);\n \t\treturn git_config_pathname(&git_mailmap_file, var, value);\n-\tif (!strcmp(var, \"mailmap.blob\"))\n+\t}\n+\n+\tif (!strcmp(var, \"mailmap.blob\")) {\n+\t\tFREE_AND_NULL(git_mailmap_blob);\n \t\treturn git_config_string(&git_mailmap_blob, var, value);\n+\t}\n \n \t/* Add other config variables here and to Documentation/config.txt. */\n \treturn 0;\n@@ -1790,8 +1806,10 @@ static int git_default_mailmap_config(const char *var, const char *value)\n \n static int git_default_attr_config(const char *var, const char *value)\n {\n-\tif (!strcmp(var, \"attr.tree\"))\n+\tif (!strcmp(var, \"attr.tree\")) {\n+\t\tFREE_AND_NULL(git_attr_tree);\n \t\treturn git_config_string(&git_attr_tree, var, value);\n+\t}\n \n \t/*\n \t * Add other attribute related config variables here and to\ndiff --git a/t/t1306-xdg-files.sh b/t/t1306-xdg-files.sh\nindex 40d3c42618..53e5b290b9 100755\n--- a/t/t1306-xdg-files.sh\n+++ b/t/t1306-xdg-files.sh\n@@ -7,6 +7,7 @@\n \n test_description='Compatibility with $XDG_CONFIG_HOME/git/ files'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n test_expect_success 'read config: xdg file exists and ~/.gitconfig doesn'\\''t' '\ndiff --git a/t/t1350-config-hooks-path.sh b/t/t1350-config-hooks-path.sh\nindex f6dc83e2aa..5c47f9ecc3 100755\n--- a/t/t1350-config-hooks-path.sh\n+++ b/t/t1350-config-hooks-path.sh\n@@ -2,6 +2,7 @@\n \n test_description='Test the core.hooksPath configuration variable'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n test_expect_success 'set up a pre-commit hook in core.hooksPath' '\ndiff --git a/t/t3415-rebase-autosquash.sh b/t/t3415-rebase-autosquash.sh\nindex fcc40d6fe1..22452ff84c 100755\n--- a/t/t3415-rebase-autosquash.sh\n+++ b/t/t3415-rebase-autosquash.sh\n@@ -5,6 +5,7 @@ test_description='auto squash'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n . \"$TEST_DIRECTORY\"/lib-rebase.sh\ndiff --git a/t/t4041-diff-submodule-option.sh b/t/t4041-diff-submodule-option.sh\nindex 0c1502d4b0..8fc40e75eb 100755\n--- a/t/t4041-diff-submodule-option.sh\n+++ b/t/t4041-diff-submodule-option.sh\n@@ -12,6 +12,7 @@ This test tries to verify the sanity of the --submodule option of git diff.\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n # Tested non-UTF-8 encoding\ndiff --git a/t/t4060-diff-submodule-option-diff-format.sh b/t/t4060-diff-submodule-option-diff-format.sh\nindex 97c6424cd5..8ce67442d9 100755\n--- a/t/t4060-diff-submodule-option-diff-format.sh\n+++ b/t/t4060-diff-submodule-option-diff-format.sh\n@@ -10,6 +10,7 @@ test_description='Support for diff format verbose submodule difference in git di\n This test tries to verify the sanity of --submodule=diff option of git diff.\n '\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n # Tested non-UTF-8 encoding\ndiff --git a/t/t4210-log-i18n.sh b/t/t4210-log-i18n.sh\nindex 75216f19ce..7120030b5c 100755\n--- a/t/t4210-log-i18n.sh\n+++ b/t/t4210-log-i18n.sh\n@@ -1,6 +1,8 @@\n #!/bin/sh\n \n test_description='test log with i18n features'\n+\n+TEST_PASSES_SANITIZE_LEAK=true\n . ./lib-gettext.sh\n \n # two forms of é\ndiff --git a/t/t6006-rev-list-format.sh b/t/t6006-rev-list-format.sh\nindex 573eb97a0f..f1623b1c06 100755\n--- a/t/t6006-rev-list-format.sh\n+++ b/t/t6006-rev-list-format.sh\n@@ -8,6 +8,7 @@ test_description='git rev-list --pretty=format test'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-terminal.sh\n \ndiff --git a/t/t7005-editor.sh b/t/t7005-editor.sh\nindex 5fcf281dfb..b9822294fe 100755\n--- a/t/t7005-editor.sh\n+++ b/t/t7005-editor.sh\n@@ -2,6 +2,7 @@\n \n test_description='GIT_EDITOR, core.editor, and stuff'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n unset EDITOR VISUAL GIT_EDITOR\ndiff --git a/t/t7102-reset.sh b/t/t7102-reset.sh\nindex 62d9f846ce..2add26d768 100755\n--- a/t/t7102-reset.sh\n+++ b/t/t7102-reset.sh\n@@ -10,6 +10,7 @@ Documented tests for git reset'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n commit_msg () {\ndiff --git a/t/t9129-git-svn-i18n-commitencoding.sh b/t/t9129-git-svn-i18n-commitencoding.sh\nindex 185248a4cd..01e1e8a8f7 100755\n--- a/t/t9129-git-svn-i18n-commitencoding.sh\n+++ b/t/t9129-git-svn-i18n-commitencoding.sh\n@@ -4,7 +4,6 @@\n \n test_description='git svn honors i18n.commitEncoding in config'\n \n-TEST_FAILS_SANITIZE_LEAK=true\n . ./lib-git-svn.sh\n \n compare_git_head_with () {\ndiff --git a/t/t9139-git-svn-non-utf8-commitencoding.sh b/t/t9139-git-svn-non-utf8-commitencoding.sh\nindex b7f756b2b7..22d80b0be2 100755\n--- a/t/t9139-git-svn-non-utf8-commitencoding.sh\n+++ b/t/t9139-git-svn-non-utf8-commitencoding.sh\n@@ -4,7 +4,6 @@\n \n test_description='git svn refuses to dcommit non-UTF8 messages'\n \n-TEST_FAILS_SANITIZE_LEAK=true\n . ./lib-git-svn.sh\n \n # ISO-2022-JP can pass for valid UTF-8, so skipping that in this test\n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495541","messageId":"f1a1c43e76b46b44d375c659f96d9e751ea6b625.1716541556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716541556.git.ps@pks.im","subject":"[PATCH v2 14/21] builtin/credential: clear credential before exit","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-24T10:04:17Z","receivedAt":"2024-05-24T10:04:20Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We never release memory associated with `struct credential`. Fix this\nand mark the corresponding test as leak free.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/credential.c   | 2 ++\n t/t0300-credentials.sh | 2 ++\n 2 files changed, 4 insertions(+)\n\ndiff --git a/builtin/credential.c b/builtin/credential.c\nindex 5100d441f2..b72e76dd9a 100644\n--- a/builtin/credential.c\n+++ b/builtin/credential.c\n@@ -39,5 +39,7 @@ int cmd_credential(int argc, const char **argv, const char *prefix UNUSED)\n \t} else {\n \t\tusage(usage_msg);\n \t}\n+\n+\tcredential_clear(&c);\n \treturn 0;\n }\ndiff --git a/t/t0300-credentials.sh b/t/t0300-credentials.sh\nindex 432f029d48..6a76b7fdbd 100755\n--- a/t/t0300-credentials.sh\n+++ b/t/t0300-credentials.sh\n@@ -1,6 +1,8 @@\n #!/bin/sh\n \n test_description='basic credential helper tests'\n+\n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-credential.sh\n \n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495542","messageId":"64b92156f878caf91dcf75f0d562ead672ef9eea.1716541556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716541556.git.ps@pks.im","subject":"[PATCH v2 15/21] commit-reach: fix memory leak in `ahead_behind()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-24T10:04:22Z","receivedAt":"2024-05-24T10:04:26Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We use a priority queue in `ahead_behind()` to compute the ahead/behind\ncount for commits. We may not iterate through all commits part of that\nqueue though in case all of its entries are stale. Consequently, as we\nnever make the effort to release the remaining commits, we end up\nleaking bit arrays that we have allocated for each of the contained\ncommits.\n\nPlug this leak and mark the corresponding test as leak free.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n commit-reach.c           | 4 ++++\n t/t3203-branch-output.sh | 2 ++\n 2 files changed, 6 insertions(+)\n\ndiff --git a/commit-reach.c b/commit-reach.c\nindex 8f9b008f87..384aee1ab3 100644\n--- a/commit-reach.c\n+++ b/commit-reach.c\n@@ -1106,6 +1106,10 @@ void ahead_behind(struct repository *r,\n \n \t/* STALE is used here, PARENT2 is used by insert_no_dup(). */\n \trepo_clear_commit_marks(r, PARENT2 | STALE);\n+\twhile (prio_queue_peek(&queue)) {\n+\t\tstruct commit *c = prio_queue_get(&queue);\n+\t\tfree_bit_array(c);\n+\t}\n \tclear_bit_arrays(&bit_arrays);\n \tclear_prio_queue(&queue);\n }\ndiff --git a/t/t3203-branch-output.sh b/t/t3203-branch-output.sh\nindex 758963b189..e627f08a17 100755\n--- a/t/t3203-branch-output.sh\n+++ b/t/t3203-branch-output.sh\n@@ -1,6 +1,8 @@\n #!/bin/sh\n \n test_description='git branch display tests'\n+\n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-terminal.sh\n \n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495543","messageId":"cd8a992f08a0bb63d93d2ab8562a0d74129b6ee3.1716541556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716541556.git.ps@pks.im","subject":"[PATCH v2 16/21] submodule: fix leaking memory for submodule entries","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-24T10:04:26Z","receivedAt":"2024-05-24T10:04:31Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"In `free_one_config()` we never end up freeing the `url` and `ignore`\nfields and thus leak memory. Fix those leaks and mark now-passing tests\nas leak free.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n submodule-config.c                     | 2 ++\n t/t1013-read-tree-submodule.sh         | 1 +\n t/t2013-checkout-submodule.sh          | 1 +\n t/t3007-ls-files-recurse-submodules.sh | 1 +\n t/t7112-reset-submodule.sh             | 1 +\n 5 files changed, 6 insertions(+)\n\ndiff --git a/submodule-config.c b/submodule-config.c\nindex 11428b4ada..ec45ea67b9 100644\n--- a/submodule-config.c\n+++ b/submodule-config.c\n@@ -91,6 +91,8 @@ static void free_one_config(struct submodule_entry *entry)\n \tfree((void *) entry->config->path);\n \tfree((void *) entry->config->name);\n \tfree((void *) entry->config->branch);\n+\tfree((void *) entry->config->url);\n+\tfree((void *) entry->config->ignore);\n \tfree((void *) entry->config->update_strategy.command);\n \tfree(entry->config);\n }\ndiff --git a/t/t1013-read-tree-submodule.sh b/t/t1013-read-tree-submodule.sh\nindex bfc90d4cf2..cf8b94ebed 100755\n--- a/t/t1013-read-tree-submodule.sh\n+++ b/t/t1013-read-tree-submodule.sh\n@@ -2,6 +2,7 @@\n \n test_description='read-tree can handle submodules'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-submodule-update.sh\n \ndiff --git a/t/t2013-checkout-submodule.sh b/t/t2013-checkout-submodule.sh\nindex b2bdd1fcb4..3c1d663d94 100755\n--- a/t/t2013-checkout-submodule.sh\n+++ b/t/t2013-checkout-submodule.sh\n@@ -2,6 +2,7 @@\n \n test_description='checkout can handle submodules'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-submodule-update.sh\n \ndiff --git a/t/t3007-ls-files-recurse-submodules.sh b/t/t3007-ls-files-recurse-submodules.sh\nindex 61771eec83..f04bdc8c78 100755\n--- a/t/t3007-ls-files-recurse-submodules.sh\n+++ b/t/t3007-ls-files-recurse-submodules.sh\n@@ -6,6 +6,7 @@ This test verifies the recurse-submodules feature correctly lists files from\n submodules.\n '\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n test_expect_success 'setup directory structure and submodules' '\ndiff --git a/t/t7112-reset-submodule.sh b/t/t7112-reset-submodule.sh\nindex a3e2413bc3..b0d3d93b0b 100755\n--- a/t/t7112-reset-submodule.sh\n+++ b/t/t7112-reset-submodule.sh\n@@ -2,6 +2,7 @@\n \n test_description='reset can handle submodules'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-submodule-update.sh\n \n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495544","messageId":"128e2eaf7a03fe0dc203c2e505d96b11c26ff60a.1716541556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716541556.git.ps@pks.im","subject":"[PATCH v2 17/21] strvec: add functions to replace and remove strings","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-24T10:04:31Z","receivedAt":"2024-05-24T10:04:35Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Add two functions that allow to replace and remove strings contained in\nthe strvec. This will be used by a subsequent commit that refactors\ngit-mv(1).\n\nWhile at it, add a bunch of unit tests that cover both old and new\nfunctionality.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile                |   1 +\n strvec.c                |  20 +++\n strvec.h                |  13 ++\n t/unit-tests/t-strvec.c | 269 ++++++++++++++++++++++++++++++++++++++++\n t/unit-tests/test-lib.c |  13 ++\n t/unit-tests/test-lib.h |  13 ++\n 6 files changed, 329 insertions(+)\n create mode 100644 t/unit-tests/t-strvec.c\n\ndiff --git a/Makefile b/Makefile\nindex cf504963c2..d4000fb1d6 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1336,6 +1336,7 @@ THIRD_PARTY_SOURCES += sha1dc/%\n \n UNIT_TEST_PROGRAMS += t-mem-pool\n UNIT_TEST_PROGRAMS += t-strbuf\n+UNIT_TEST_PROGRAMS += t-strvec\n UNIT_TEST_PROGRAMS += t-ctype\n UNIT_TEST_PROGRAMS += t-prio-queue\n UNIT_TEST_PROGS = $(patsubst %,$(UNIT_TEST_BIN)/%$X,$(UNIT_TEST_PROGRAMS))\ndiff --git a/strvec.c b/strvec.c\nindex 178f4f3748..d4073ec9fa 100644\n--- a/strvec.c\n+++ b/strvec.c\n@@ -56,6 +56,26 @@ void strvec_pushv(struct strvec *array, const char **items)\n \t\tstrvec_push(array, *items);\n }\n \n+const char *strvec_replace(struct strvec *array, size_t idx, const char *replacement)\n+{\n+\tchar *to_free;\n+\tif (idx >= array->nr)\n+\t\tBUG(\"index outside of array boundary\");\n+\tto_free = (char *) array->v[idx];\n+\tarray->v[idx] = xstrdup(replacement);\n+\tfree(to_free);\n+\treturn array->v[idx];\n+}\n+\n+void strvec_remove(struct strvec *array, size_t idx)\n+{\n+\tif (idx >= array->nr)\n+\t\tBUG(\"index outside of array boundary\");\n+\tfree((char *)array->v[idx]);\n+\tmemmove(array->v + idx, array->v + idx + 1, (array->nr - idx) * sizeof(char *));\n+\tarray->nr--;\n+}\n+\n void strvec_pop(struct strvec *array)\n {\n \tif (!array->nr)\ndiff --git a/strvec.h b/strvec.h\nindex 4715d3e51f..6c7e8b7d50 100644\n--- a/strvec.h\n+++ b/strvec.h\n@@ -64,6 +64,19 @@ void strvec_pushl(struct strvec *, ...);\n /* Push a null-terminated array of strings onto the end of the array. */\n void strvec_pushv(struct strvec *, const char **);\n \n+/**\n+ * Replace the value at the given index with a new value. The index must be\n+ * valid. Returns a pointer to the inserted value.\n+ */\n+const char *strvec_replace(struct strvec *array, size_t idx, const char *replacement);\n+\n+/*\n+ * Remove the value at the given index. The remainder of the array will be\n+ * moved to fill the resulting gap. The provided index must point into the\n+ * array.\n+ */\n+void strvec_remove(struct strvec *array, size_t idx);\n+\n /**\n  * Remove the final element from the array. If there are no\n  * elements in the array, do nothing.\ndiff --git a/t/unit-tests/t-strvec.c b/t/unit-tests/t-strvec.c\nnew file mode 100644\nindex 0000000000..f17fb10d9e\n--- /dev/null\n+++ b/t/unit-tests/t-strvec.c\n@@ -0,0 +1,269 @@\n+#include \"test-lib.h\"\n+#include \"strbuf.h\"\n+#include \"strvec.h\"\n+\n+#define check_strvec(vec, ...) \\\n+\tcheck_strvec_loc(TEST_LOCATION(), vec, __VA_ARGS__)\n+static void check_strvec_loc(const char *loc, struct strvec *vec, ...)\n+{\n+\tva_list ap;\n+\tsize_t nr = 0;\n+\n+\tva_start(ap, vec);\n+\twhile (1) {\n+\t\tconst char *str = va_arg(ap, const char *);\n+\t\tif (!str)\n+\t\t\tbreak;\n+\n+\t\tif (!check_uint(vec->nr, >, nr) ||\n+\t\t    !check_uint(vec->alloc, >, nr) ||\n+\t\t    !check_str(vec->v[nr], str)) {\n+\t\t\tstruct strbuf msg = STRBUF_INIT;\n+\t\t\tstrbuf_addf(&msg, \"strvec index %\"PRIuMAX, (uintmax_t) nr);\n+\t\t\ttest_assert(loc, msg.buf, 0);\n+\t\t\tstrbuf_release(&msg);\n+\t\t\treturn;\n+\t\t}\n+\n+\t\tnr++;\n+\t}\n+\n+\tcheck_uint(vec->nr, ==, nr);\n+\tcheck_uint(vec->alloc, >=, nr);\n+\tcheck_pointer_eq(vec->v[nr], NULL);\n+}\n+\n+static void t_static_init(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tcheck_pointer_eq(vec.v, empty_strvec);\n+\tcheck_uint(vec.nr, ==, 0);\n+\tcheck_uint(vec.alloc, ==, 0);\n+}\n+\n+static void t_dynamic_init(void)\n+{\n+\tstruct strvec vec;\n+\tstrvec_init(&vec);\n+\tcheck_pointer_eq(vec.v, empty_strvec);\n+\tcheck_uint(vec.nr, ==, 0);\n+\tcheck_uint(vec.alloc, ==, 0);\n+}\n+\n+static void t_clear(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_push(&vec, \"foo\");\n+\tstrvec_clear(&vec);\n+\tcheck_pointer_eq(vec.v, empty_strvec);\n+\tcheck_uint(vec.nr, ==, 0);\n+\tcheck_uint(vec.alloc, ==, 0);\n+}\n+\n+static void t_push(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_push(&vec, \"foo\");\n+\tcheck_strvec(&vec, \"foo\", NULL);\n+\n+\tstrvec_push(&vec, \"bar\");\n+\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n+\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_pushf(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushf(&vec, \"foo: %d\", 1);\n+\tcheck_strvec(&vec, \"foo: 1\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_pushl(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_pushv(void)\n+{\n+\tconst char *strings[] = {\n+\t\t\"foo\", \"bar\", \"baz\", NULL,\n+\t};\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_pushv(&vec, strings);\n+\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_replace_at_head(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_replace(&vec, 0, \"replaced\");\n+\tcheck_strvec(&vec, \"replaced\", \"bar\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_replace_at_tail(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_replace(&vec, 2, \"replaced\");\n+\tcheck_strvec(&vec, \"foo\", \"bar\", \"replaced\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_replace_in_between(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_replace(&vec, 1, \"replaced\");\n+\tcheck_strvec(&vec, \"foo\", \"replaced\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_replace_with_substring(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", NULL);\n+\tstrvec_replace(&vec, 0, vec.v[0] + 1);\n+\tcheck_strvec(&vec, \"oo\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_remove_at_head(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_remove(&vec, 0);\n+\tcheck_strvec(&vec, \"bar\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_remove_at_tail(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_remove(&vec, 2);\n+\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_remove_in_between(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_remove(&vec, 1);\n+\tcheck_strvec(&vec, \"foo\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_pop_empty_array(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pop(&vec);\n+\tcheck_strvec(&vec, NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_pop_non_empty_array(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_pop(&vec);\n+\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_split_empty_string(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_split(&vec, \"\");\n+\tcheck_strvec(&vec, NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_split_single_item(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_split(&vec, \"foo\");\n+\tcheck_strvec(&vec, \"foo\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_split_multiple_items(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_split(&vec, \"foo bar baz\");\n+\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_split_whitespace_only(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_split(&vec, \" \\t\\n\");\n+\tcheck_strvec(&vec, NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_split_multiple_consecutive_whitespaces(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_split(&vec, \"foo\\n\\t bar\");\n+\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_detach(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tconst char **detached;\n+\n+\tstrvec_push(&vec, \"foo\");\n+\n+\tdetached = strvec_detach(&vec);\n+\tcheck_str(detached[0], \"foo\");\n+\tcheck_pointer_eq(detached[1], NULL);\n+\n+\tcheck_pointer_eq(vec.v, empty_strvec);\n+\tcheck_uint(vec.nr, ==, 0);\n+\tcheck_uint(vec.alloc, ==, 0);\n+\n+\tfree((char *) detached[0]);\n+\tfree(detached);\n+}\n+\n+int cmd_main(int argc, const char **argv)\n+{\n+\tTEST(t_static_init(), \"static initialization\");\n+\tTEST(t_dynamic_init(), \"dynamic initialization\");\n+\tTEST(t_clear(), \"clear\");\n+\tTEST(t_push(), \"push\");\n+\tTEST(t_pushf(), \"pushf\");\n+\tTEST(t_pushl(), \"pushl\");\n+\tTEST(t_pushv(), \"pushv\");\n+\tTEST(t_replace_at_head(), \"replace at head\");\n+\tTEST(t_replace_in_between(), \"replace in between\");\n+\tTEST(t_replace_at_tail(), \"replace at tail\");\n+\tTEST(t_replace_with_substring(), \"replace with substring\");\n+\tTEST(t_remove_at_head(), \"remove at head\");\n+\tTEST(t_remove_in_between(), \"remove in between\");\n+\tTEST(t_remove_at_tail(), \"remove at tail\");\n+\tTEST(t_pop_empty_array(), \"pop with empty array\");\n+\tTEST(t_pop_non_empty_array(), \"pop with non-empty array\");\n+\tTEST(t_split_empty_string(), \"split empty string\");\n+\tTEST(t_split_single_item(), \"split single item\");\n+\tTEST(t_split_multiple_items(), \"split multiple items\");\n+\tTEST(t_split_whitespace_only(), \"split whitespace only\");\n+\tTEST(t_split_multiple_consecutive_whitespaces(), \"split multiple consecutive whitespaces\");\n+\tTEST(t_detach(), \"detach\");\n+\treturn test_done();\n+}\ndiff --git a/t/unit-tests/test-lib.c b/t/unit-tests/test-lib.c\nindex 66d6980ffb..3c513ce59a 100644\n--- a/t/unit-tests/test-lib.c\n+++ b/t/unit-tests/test-lib.c\n@@ -318,6 +318,19 @@ int check_bool_loc(const char *loc, const char *check, int ok)\n \n union test__tmp test__tmp[2];\n \n+int check_pointer_eq_loc(const char *loc, const char *check, int ok,\n+\t\t\t const void *a, const void *b)\n+{\n+\tint ret = test_assert(loc, check, ok);\n+\n+\tif (!ret) {\n+\t\ttest_msg(\"   left: %p\", a);\n+\t\ttest_msg(\"  right: %p\", b);\n+\t}\n+\n+\treturn ret;\n+}\n+\n int check_int_loc(const char *loc, const char *check, int ok,\n \t\t  intmax_t a, intmax_t b)\n {\ndiff --git a/t/unit-tests/test-lib.h b/t/unit-tests/test-lib.h\nindex a8f07ae0b7..2de6d715d5 100644\n--- a/t/unit-tests/test-lib.h\n+++ b/t/unit-tests/test-lib.h\n@@ -75,6 +75,18 @@ int test_assert(const char *location, const char *check, int ok);\n \tcheck_bool_loc(TEST_LOCATION(), #x, x)\n int check_bool_loc(const char *loc, const char *check, int ok);\n \n+/*\n+ * Compare two integers. Prints a message with the two values if the\n+ * comparison fails. NB this is not thread safe.\n+ */\n+#define check_pointer_eq(a, b)\t\t\t\t\t\t\\\n+\t(test__tmp[0].p = (a), test__tmp[1].p = (b),\t\t\t\\\n+\t check_pointer_eq_loc(TEST_LOCATION(), #a\" == \"#b,\t\t\\\n+\t\t\t      test__tmp[0].p == test__tmp[1].p,\t\t\\\n+\t\t\t      test__tmp[0].p, test__tmp[1].p))\n+int check_pointer_eq_loc(const char *loc, const char *check, int ok,\n+\t\t\t const void *a, const void *b);\n+\n /*\n  * Compare two integers. Prints a message with the two values if the\n  * comparison fails. NB this is not thread safe.\n@@ -136,6 +148,7 @@ union test__tmp {\n \tintmax_t i;\n \tuintmax_t u;\n \tchar c;\n+\tconst void *p;\n };\n \n extern union test__tmp test__tmp[2];\n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495545","messageId":"1310b24fc2b82d567a8b9dd46c5f93ecdc87accc.1716541556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716541556.git.ps@pks.im","subject":"[PATCH v2 18/21] builtin/mv: refactor `add_slash()` to always return allocated strings","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-24T10:04:36Z","receivedAt":"2024-05-24T10:04:40Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The `add_slash()` function will only conditionally return an allocated\nstring when the passed-in string did not yet have a trailing slash. This\nmakes the memory ownership harder to track than really necessary.\n\nIt's dubious whether this optimization really buys us all that much. The\nnumber of times we execute this function is bounded by the number of\narguments to git-mv(1), so in the typical case we may end up saving an\nallocation or two.\n\nSimplify the code to unconditionally return allocated strings.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/mv.c | 38 ++++++++++++++++++++------------------\n 1 file changed, 20 insertions(+), 18 deletions(-)\n\ndiff --git a/builtin/mv.c b/builtin/mv.c\nindex 74aa9746aa..9f4c75df04 100644\n--- a/builtin/mv.c\n+++ b/builtin/mv.c\n@@ -76,7 +76,7 @@ static const char **internal_prefix_pathspec(const char *prefix,\n \treturn result;\n }\n \n-static const char *add_slash(const char *path)\n+static char *add_slash(const char *path)\n {\n \tsize_t len = strlen(path);\n \tif (len && path[len - 1] != '/') {\n@@ -86,7 +86,7 @@ static const char *add_slash(const char *path)\n \t\twith_slash[len] = 0;\n \t\treturn with_slash;\n \t}\n-\treturn path;\n+\treturn xstrdup(path);\n }\n \n #define SUBMODULE_WITH_GITDIR ((const char *)1)\n@@ -111,7 +111,7 @@ static void prepare_move_submodule(const char *src, int first,\n static int index_range_of_same_dir(const char *src, int length,\n \t\t\t\t   int *first_p, int *last_p)\n {\n-\tconst char *src_w_slash = add_slash(src);\n+\tchar *src_w_slash = add_slash(src);\n \tint first, last, len_w_slash = length + 1;\n \n \tfirst = index_name_pos(the_repository->index, src_w_slash, len_w_slash);\n@@ -124,8 +124,8 @@ static int index_range_of_same_dir(const char *src, int length,\n \t\tif (strncmp(path, src_w_slash, len_w_slash))\n \t\t\tbreak;\n \t}\n-\tif (src_w_slash != src)\n-\t\tfree((char *)src_w_slash);\n+\n+\tfree(src_w_slash);\n \t*first_p = first;\n \t*last_p = last;\n \treturn last - first;\n@@ -141,7 +141,7 @@ static int index_range_of_same_dir(const char *src, int length,\n static int empty_dir_has_sparse_contents(const char *name)\n {\n \tint ret = 0;\n-\tconst char *with_slash = add_slash(name);\n+\tchar *with_slash = add_slash(name);\n \tint length = strlen(with_slash);\n \n \tint pos = index_name_pos(the_repository->index, with_slash, length);\n@@ -159,8 +159,7 @@ static int empty_dir_has_sparse_contents(const char *name)\n \t}\n \n free_return:\n-\tif (with_slash != name)\n-\t\tfree((char *)with_slash);\n+\tfree(with_slash);\n \treturn ret;\n }\n \n@@ -178,7 +177,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\tOPT_END(),\n \t};\n \tconst char **source, **destination, **dest_path, **submodule_gitfile;\n-\tconst char *dst_w_slash;\n+\tchar *dst_w_slash = NULL;\n \tconst char **src_dir = NULL;\n \tint src_dir_nr = 0, src_dir_alloc = 0;\n \tstruct strbuf a_src_dir = STRBUF_INIT;\n@@ -243,10 +242,6 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\t\t\tdst_mode = SPARSE;\n \t\t}\n \t}\n-\tif (dst_w_slash != dest_path[0]) {\n-\t\tfree((char *)dst_w_slash);\n-\t\tdst_w_slash = NULL;\n-\t}\n \n \t/* Checking */\n \tfor (i = 0; i < argc; i++) {\n@@ -265,12 +260,14 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \n \t\t\tpos = index_name_pos(the_repository->index, src, length);\n \t\t\tif (pos < 0) {\n-\t\t\t\tconst char *src_w_slash = add_slash(src);\n+\t\t\t\tchar *src_w_slash = add_slash(src);\n \t\t\t\tif (!path_in_sparse_checkout(src_w_slash, the_repository->index) &&\n \t\t\t\t    empty_dir_has_sparse_contents(src)) {\n+\t\t\t\t\tfree(src_w_slash);\n \t\t\t\t\tmodes[i] |= SKIP_WORKTREE_DIR;\n \t\t\t\t\tgoto dir_check;\n \t\t\t\t}\n+\t\t\t\tfree(src_w_slash);\n \t\t\t\t/* only error if existence is expected. */\n \t\t\t\tif (!(modes[i] & SPARSE))\n \t\t\t\t\tbad = _(\"bad source\");\n@@ -310,7 +307,9 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \n dir_check:\n \t\tif (S_ISDIR(st.st_mode)) {\n-\t\t\tint j, dst_len, n;\n+\t\t\tchar *dst_with_slash;\n+\t\t\tsize_t dst_with_slash_len;\n+\t\t\tint j, n;\n \t\t\tint first = index_name_pos(the_repository->index, src, length), last;\n \n \t\t\tif (first >= 0) {\n@@ -335,19 +334,21 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\t\tREALLOC_ARRAY(modes, n);\n \t\t\tREALLOC_ARRAY(submodule_gitfile, n);\n \n-\t\t\tdst = add_slash(dst);\n-\t\t\tdst_len = strlen(dst);\n+\t\t\tdst_with_slash = add_slash(dst);\n+\t\t\tdst_with_slash_len = strlen(dst_with_slash);\n \n \t\t\tfor (j = 0; j < last - first; j++) {\n \t\t\t\tconst struct cache_entry *ce = the_repository->index->cache[first + j];\n \t\t\t\tconst char *path = ce->name;\n \t\t\t\tsource[argc + j] = path;\n \t\t\t\tdestination[argc + j] =\n-\t\t\t\t\tprefix_path(dst, dst_len, path + length + 1);\n+\t\t\t\t\tprefix_path(dst_with_slash, dst_with_slash_len, path + length + 1);\n \t\t\t\tmemset(modes + argc + j, 0, sizeof(enum update_mode));\n \t\t\t\tmodes[argc + j] |= ce_skip_worktree(ce) ? SPARSE : INDEX;\n \t\t\t\tsubmodule_gitfile[argc + j] = NULL;\n \t\t\t}\n+\n+\t\t\tfree(dst_with_slash);\n \t\t\targc += last - first;\n \t\t\tgoto act_on_entry;\n \t\t}\n@@ -565,6 +566,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\t\t       COMMIT_LOCK | SKIP_IF_UNCHANGED))\n \t\tdie(_(\"Unable to write new index file\"));\n \n+\tfree(dst_w_slash);\n \tstring_list_clear(&src_for_dst, 0);\n \tstring_list_clear(&dirty_paths, 0);\n \tUNLEAK(source);\n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495546","messageId":"d4fef9825a955de9b076646c8d4864791d299ae5.1716541556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716541556.git.ps@pks.im","subject":"[PATCH v2 19/21] builtin/mv duplicate string list memory","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-24T10:04:41Z","receivedAt":"2024-05-24T10:04:45Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"makes the next patch easier, where we will migrate to the paths being\nowned by a strvec. given that we are talking about command line\nparameters here it's also not like we have tons of allocations that this\nwould save\n\nwhile at it, fix a memory leak\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/mv.c | 19 +++++++++++++------\n 1 file changed, 13 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/mv.c b/builtin/mv.c\nindex 9f4c75df04..12dcc0b13c 100644\n--- a/builtin/mv.c\n+++ b/builtin/mv.c\n@@ -183,11 +183,12 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \tstruct strbuf a_src_dir = STRBUF_INIT;\n \tenum update_mode *modes, dst_mode = 0;\n \tstruct stat st, dest_st;\n-\tstruct string_list src_for_dst = STRING_LIST_INIT_NODUP;\n+\tstruct string_list src_for_dst = STRING_LIST_INIT_DUP;\n \tstruct lock_file lock_file = LOCK_INIT;\n \tstruct cache_entry *ce;\n-\tstruct string_list only_match_skip_worktree = STRING_LIST_INIT_NODUP;\n-\tstruct string_list dirty_paths = STRING_LIST_INIT_NODUP;\n+\tstruct string_list only_match_skip_worktree = STRING_LIST_INIT_DUP;\n+\tstruct string_list dirty_paths = STRING_LIST_INIT_DUP;\n+\tint ret;\n \n \tgit_config(git_default_config, NULL);\n \n@@ -440,8 +441,10 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \n \tif (only_match_skip_worktree.nr) {\n \t\tadvise_on_updating_sparse_paths(&only_match_skip_worktree);\n-\t\tif (!ignore_errors)\n-\t\t\treturn 1;\n+\t\tif (!ignore_errors) {\n+\t\t\tret = 1;\n+\t\t\tgoto out;\n+\t\t}\n \t}\n \n \tfor (i = 0; i < argc; i++) {\n@@ -566,12 +569,16 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\t\t       COMMIT_LOCK | SKIP_IF_UNCHANGED))\n \t\tdie(_(\"Unable to write new index file\"));\n \n+\tret = 0;\n+\n+out:\n \tfree(dst_w_slash);\n \tstring_list_clear(&src_for_dst, 0);\n \tstring_list_clear(&dirty_paths, 0);\n+\tstring_list_clear(&only_match_skip_worktree, 0);\n \tUNLEAK(source);\n \tUNLEAK(dest_path);\n \tfree(submodule_gitfile);\n \tfree(modes);\n-\treturn 0;\n+\treturn ret;\n }\n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495547","messageId":"797cdb286adcb577e21d8da6bf11b3f56795b0ee.1716541556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716541556.git.ps@pks.im","subject":"[PATCH v2 20/21] builtin/mv: refactor to use `struct strvec`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-24T10:04:45Z","receivedAt":"2024-05-24T10:04:49Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Memory allocation patterns in git-mv(1) are extremely hard to follow:\nWe copy around string pointers into manually-managed arrays, some of\nwhich alias each other, but only sometimes, while we also drop some of\nthose strings at other times without ever daring to free them.\n\nWhile this may be my own subjective feeling, it seems like others have\ngiven up as the code has multiple calls to `UNLEAK()`. These are not\nsufficient though, and git-mv(1) is still leaking all over the place\neven with them.\n\nRefactor the code to instead track strings in `struct strvec`. While\nthis has the effect of effectively duplicating some of the strings\nwithout an actual need, it is way easier to reason about and fixes all\nof the aliasing of memory that has been going on. It allows us to get\nrid of the `UNLEAK()` calls and also fixes leaks that those calls did\nnot paper over.\n\nMark tests which are now leak-free accordingly.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/mv.c                             | 125 +++++++++++------------\n t/t4001-diff-rename.sh                   |   4 +-\n t/t4043-diff-rename-binary.sh            |   1 +\n t/t4120-apply-popt.sh                    |   1 +\n t/t6400-merge-df.sh                      |   1 +\n t/t6412-merge-large-rename.sh            |   1 +\n t/t6426-merge-skip-unneeded-updates.sh   |   1 +\n t/t6429-merge-sequence-rename-caching.sh |   1 +\n 8 files changed, 68 insertions(+), 67 deletions(-)\n\ndiff --git a/builtin/mv.c b/builtin/mv.c\nindex 12dcc0b13c..e461d29ca1 100644\n--- a/builtin/mv.c\n+++ b/builtin/mv.c\n@@ -20,6 +20,7 @@\n #include \"read-cache-ll.h\"\n #include \"repository.h\"\n #include \"setup.h\"\n+#include \"strvec.h\"\n #include \"submodule.h\"\n #include \"entry.h\"\n \n@@ -38,42 +39,32 @@ enum update_mode {\n #define DUP_BASENAME 1\n #define KEEP_TRAILING_SLASH 2\n \n-static const char **internal_prefix_pathspec(const char *prefix,\n-\t\t\t\t\t     const char **pathspec,\n-\t\t\t\t\t     int count, unsigned flags)\n+static void internal_prefix_pathspec(struct strvec *out,\n+\t\t\t\t     const char *prefix,\n+\t\t\t\t     const char **pathspec,\n+\t\t\t\t     int count, unsigned flags)\n {\n-\tint i;\n-\tconst char **result;\n \tint prefixlen = prefix ? strlen(prefix) : 0;\n-\tALLOC_ARRAY(result, count + 1);\n \n \t/* Create an intermediate copy of the pathspec based on the flags */\n-\tfor (i = 0; i < count; i++) {\n-\t\tint length = strlen(pathspec[i]);\n-\t\tint to_copy = length;\n-\t\tchar *it;\n+\tfor (int i = 0; i < count; i++) {\n+\t\tsize_t length = strlen(pathspec[i]);\n+\t\tsize_t to_copy = length;\n+\t\tconst char *maybe_basename;\n+\t\tchar *trimmed, *prefixed_path;\n+\n \t\twhile (!(flags & KEEP_TRAILING_SLASH) &&\n \t\t       to_copy > 0 && is_dir_sep(pathspec[i][to_copy - 1]))\n \t\t\tto_copy--;\n \n-\t\tit = xmemdupz(pathspec[i], to_copy);\n-\t\tif (flags & DUP_BASENAME) {\n-\t\t\tresult[i] = xstrdup(basename(it));\n-\t\t\tfree(it);\n-\t\t} else {\n-\t\t\tresult[i] = it;\n-\t\t}\n-\t}\n-\tresult[count] = NULL;\n+\t\ttrimmed = xmemdupz(pathspec[i], to_copy);\n+\t\tmaybe_basename = (flags & DUP_BASENAME) ? basename(trimmed) : trimmed;\n+\t\tprefixed_path = prefix_path(prefix, prefixlen, maybe_basename);\n+\t\tstrvec_push(out, prefixed_path);\n \n-\t/* Prefix the pathspec and free the old intermediate strings */\n-\tfor (i = 0; i < count; i++) {\n-\t\tconst char *match = prefix_path(prefix, prefixlen, result[i]);\n-\t\tfree((char *) result[i]);\n-\t\tresult[i] = match;\n+\t\tfree(prefixed_path);\n+\t\tfree(trimmed);\n \t}\n-\n-\treturn result;\n }\n \n static char *add_slash(const char *path)\n@@ -176,7 +167,10 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\tOPT_BOOL(0, \"sparse\", &ignore_sparse, N_(\"allow updating entries outside of the sparse-checkout cone\")),\n \t\tOPT_END(),\n \t};\n-\tconst char **source, **destination, **dest_path, **submodule_gitfile;\n+\tstruct strvec sources = STRVEC_INIT;\n+\tstruct strvec dest_paths = STRVEC_INIT;\n+\tstruct strvec destinations = STRVEC_INIT;\n+\tconst char **submodule_gitfile;\n \tchar *dst_w_slash = NULL;\n \tconst char **src_dir = NULL;\n \tint src_dir_nr = 0, src_dir_alloc = 0;\n@@ -201,7 +195,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \tif (repo_read_index(the_repository) < 0)\n \t\tdie(_(\"index file corrupt\"));\n \n-\tsource = internal_prefix_pathspec(prefix, argv, argc, 0);\n+\tinternal_prefix_pathspec(&sources, prefix, argv, argc, 0);\n \tCALLOC_ARRAY(modes, argc);\n \n \t/*\n@@ -212,41 +206,39 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \tflags = KEEP_TRAILING_SLASH;\n \tif (argc == 1 && is_directory(argv[0]) && !is_directory(argv[1]))\n \t\tflags = 0;\n-\tdest_path = internal_prefix_pathspec(prefix, argv + argc, 1, flags);\n-\tdst_w_slash = add_slash(dest_path[0]);\n+\tinternal_prefix_pathspec(&dest_paths, prefix, argv + argc, 1, flags);\n+\tdst_w_slash = add_slash(dest_paths.v[0]);\n \tsubmodule_gitfile = xcalloc(argc, sizeof(char *));\n \n-\tif (dest_path[0][0] == '\\0')\n+\tif (dest_paths.v[0][0] == '\\0')\n \t\t/* special case: \".\" was normalized to \"\" */\n-\t\tdestination = internal_prefix_pathspec(dest_path[0], argv, argc, DUP_BASENAME);\n-\telse if (!lstat(dest_path[0], &st) &&\n-\t\t\tS_ISDIR(st.st_mode)) {\n-\t\tdestination = internal_prefix_pathspec(dst_w_slash, argv, argc, DUP_BASENAME);\n+\t\tinternal_prefix_pathspec(&destinations, dest_paths.v[0], argv, argc, DUP_BASENAME);\n+\telse if (!lstat(dest_paths.v[0], &st) && S_ISDIR(st.st_mode)) {\n+\t\tinternal_prefix_pathspec(&destinations, dst_w_slash, argv, argc, DUP_BASENAME);\n+\t} else if (!path_in_sparse_checkout(dst_w_slash, the_repository->index) &&\n+\t\t   empty_dir_has_sparse_contents(dst_w_slash)) {\n+\t\tinternal_prefix_pathspec(&destinations, dst_w_slash, argv, argc, DUP_BASENAME);\n+\t\tdst_mode = SKIP_WORKTREE_DIR;\n+\t} else if (argc != 1) {\n+\t\tdie(_(\"destination '%s' is not a directory\"), dest_paths.v[0]);\n \t} else {\n-\t\tif (!path_in_sparse_checkout(dst_w_slash, the_repository->index) &&\n-\t\t    empty_dir_has_sparse_contents(dst_w_slash)) {\n-\t\t\tdestination = internal_prefix_pathspec(dst_w_slash, argv, argc, DUP_BASENAME);\n-\t\t\tdst_mode = SKIP_WORKTREE_DIR;\n-\t\t} else if (argc != 1) {\n-\t\t\tdie(_(\"destination '%s' is not a directory\"), dest_path[0]);\n-\t\t} else {\n-\t\t\tdestination = dest_path;\n-\t\t\t/*\n-\t\t\t * <destination> is a file outside of sparse-checkout\n-\t\t\t * cone. Insist on cone mode here for backward\n-\t\t\t * compatibility. We don't want dst_mode to be assigned\n-\t\t\t * for a file when the repo is using no-cone mode (which\n-\t\t\t * is deprecated at this point) sparse-checkout. As\n-\t\t\t * SPARSE here is only considering cone-mode situation.\n-\t\t\t */\n-\t\t\tif (!path_in_cone_mode_sparse_checkout(destination[0], the_repository->index))\n-\t\t\t\tdst_mode = SPARSE;\n-\t\t}\n+\t\tstrvec_pushv(&destinations, dest_paths.v);\n+\n+\t\t/*\n+\t\t * <destination> is a file outside of sparse-checkout\n+\t\t * cone. Insist on cone mode here for backward\n+\t\t * compatibility. We don't want dst_mode to be assigned\n+\t\t * for a file when the repo is using no-cone mode (which\n+\t\t * is deprecated at this point) sparse-checkout. As\n+\t\t * SPARSE here is only considering cone-mode situation.\n+\t\t */\n+\t\tif (!path_in_cone_mode_sparse_checkout(destinations.v[0], the_repository->index))\n+\t\t\tdst_mode = SPARSE;\n \t}\n \n \t/* Checking */\n \tfor (i = 0; i < argc; i++) {\n-\t\tconst char *src = source[i], *dst = destination[i];\n+\t\tconst char *src = sources.v[i], *dst = destinations.v[i];\n \t\tint length;\n \t\tconst char *bad = NULL;\n \t\tint skip_sparse = 0;\n@@ -330,8 +322,6 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\t\tsrc_dir[src_dir_nr++] = src;\n \n \t\t\tn = argc + last - first;\n-\t\t\tREALLOC_ARRAY(source, n);\n-\t\t\tREALLOC_ARRAY(destination, n);\n \t\t\tREALLOC_ARRAY(modes, n);\n \t\t\tREALLOC_ARRAY(submodule_gitfile, n);\n \n@@ -341,12 +331,16 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\t\tfor (j = 0; j < last - first; j++) {\n \t\t\t\tconst struct cache_entry *ce = the_repository->index->cache[first + j];\n \t\t\t\tconst char *path = ce->name;\n-\t\t\t\tsource[argc + j] = path;\n-\t\t\t\tdestination[argc + j] =\n-\t\t\t\t\tprefix_path(dst_with_slash, dst_with_slash_len, path + length + 1);\n+\t\t\t\tchar *prefixed_path = prefix_path(dst_with_slash, dst_with_slash_len, path + length + 1);\n+\n+\t\t\t\tstrvec_push(&sources, path);\n+\t\t\t\tstrvec_push(&destinations, prefixed_path);\n+\n \t\t\t\tmemset(modes + argc + j, 0, sizeof(enum update_mode));\n \t\t\t\tmodes[argc + j] |= ce_skip_worktree(ce) ? SPARSE : INDEX;\n \t\t\t\tsubmodule_gitfile[argc + j] = NULL;\n+\n+\t\t\t\tfree(prefixed_path);\n \t\t\t}\n \n \t\t\tfree(dst_with_slash);\n@@ -430,8 +424,8 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n remove_entry:\n \t\tif (--argc > 0) {\n \t\t\tint n = argc - i;\n-\t\t\tMOVE_ARRAY(source + i, source + i + 1, n);\n-\t\t\tMOVE_ARRAY(destination + i, destination + i + 1, n);\n+\t\t\tstrvec_remove(&sources, i);\n+\t\t\tstrvec_remove(&destinations, i);\n \t\t\tMOVE_ARRAY(modes + i, modes + i + 1, n);\n \t\t\tMOVE_ARRAY(submodule_gitfile + i,\n \t\t\t\t   submodule_gitfile + i + 1, n);\n@@ -448,7 +442,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t}\n \n \tfor (i = 0; i < argc; i++) {\n-\t\tconst char *src = source[i], *dst = destination[i];\n+\t\tconst char *src = sources.v[i], *dst = destinations.v[i];\n \t\tenum update_mode mode = modes[i];\n \t\tint pos;\n \t\tint sparse_and_dirty = 0;\n@@ -576,8 +570,9 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \tstring_list_clear(&src_for_dst, 0);\n \tstring_list_clear(&dirty_paths, 0);\n \tstring_list_clear(&only_match_skip_worktree, 0);\n-\tUNLEAK(source);\n-\tUNLEAK(dest_path);\n+\tstrvec_clear(&sources);\n+\tstrvec_clear(&dest_paths);\n+\tstrvec_clear(&destinations);\n \tfree(submodule_gitfile);\n \tfree(modes);\n \treturn ret;\ndiff --git a/t/t4001-diff-rename.sh b/t/t4001-diff-rename.sh\nindex 49c042a38a..cd1931dd55 100755\n--- a/t/t4001-diff-rename.sh\n+++ b/t/t4001-diff-rename.sh\n@@ -3,9 +3,9 @@\n # Copyright (c) 2005 Junio C Hamano\n #\n \n-test_description='Test rename detection in diff engine.\n+test_description='Test rename detection in diff engine.'\n \n-'\n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-diff.sh\n \ndiff --git a/t/t4043-diff-rename-binary.sh b/t/t4043-diff-rename-binary.sh\nindex 2a2cf91352..e486493908 100755\n--- a/t/t4043-diff-rename-binary.sh\n+++ b/t/t4043-diff-rename-binary.sh\n@@ -5,6 +5,7 @@\n \n test_description='Move a binary file'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n \ndiff --git a/t/t4120-apply-popt.sh b/t/t4120-apply-popt.sh\nindex 697e86c0ff..f788428540 100755\n--- a/t/t4120-apply-popt.sh\n+++ b/t/t4120-apply-popt.sh\n@@ -5,6 +5,7 @@\n \n test_description='git apply -p handling.'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n test_expect_success setup '\ndiff --git a/t/t6400-merge-df.sh b/t/t6400-merge-df.sh\nindex 3de4ef6bd9..27d6efdc9a 100755\n--- a/t/t6400-merge-df.sh\n+++ b/t/t6400-merge-df.sh\n@@ -7,6 +7,7 @@ test_description='Test merge with directory/file conflicts'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n test_expect_success 'prepare repository' '\ndiff --git a/t/t6412-merge-large-rename.sh b/t/t6412-merge-large-rename.sh\nindex ca018d11f5..d0863a8fb5 100755\n--- a/t/t6412-merge-large-rename.sh\n+++ b/t/t6412-merge-large-rename.sh\n@@ -4,6 +4,7 @@ test_description='merging with large rename matrix'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n count() {\ndiff --git a/t/t6426-merge-skip-unneeded-updates.sh b/t/t6426-merge-skip-unneeded-updates.sh\nindex b059475ed0..62f0180325 100755\n--- a/t/t6426-merge-skip-unneeded-updates.sh\n+++ b/t/t6426-merge-skip-unneeded-updates.sh\n@@ -22,6 +22,7 @@ test_description=\"merge cases\"\n #                     underscore notation is to differentiate different\n #                     files that might be renamed into each other's paths.)\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-merge.sh\n \ndiff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh\nindex 0f39ed0d08..cb1c4ceef7 100755\n--- a/t/t6429-merge-sequence-rename-caching.sh\n+++ b/t/t6429-merge-sequence-rename-caching.sh\n@@ -2,6 +2,7 @@\n \n test_description=\"remember regular & dir renames in sequence of merges\"\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n #\n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495548","messageId":"095469193c720b76fc793c0ab00be076caf227c1.1716541556.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716541556.git.ps@pks.im","subject":"[PATCH v2 21/21] builtin/mv: fix leaks for submodule gitfile paths","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-24T10:04:50Z","receivedAt":"2024-05-24T10:04:54Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Similar to the preceding commit, we have effectively given tracking\nmemory ownership of submodule gitfile paths. Refactor the code to start\ntracking allocated strings in a separate `struct strvec` such that we\ncan easily plug those leaks. Mark now-passing tests as leak free.\n\nNote that ideally, we wouldn't require two separate data structures to\ntrack those paths. But we do need to store `NULL` pointers for the\ngitfile paths such that we can indicate that its corresponding entries\nin the other arrays do not have such a path at all. And given that\n`struct strvec`s cannot store `NULL` pointers we cannot use them to\nstore this information.\n\nThere is another small gotcha that is easy to miss: you may be wondering\nwhy we don't want to store `SUBMODULE_WITH_GITDIR` in the strvec. This\nis because this is a mere sentinel value and not actually a string at\nall.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/mv.c                              | 44 +++++++++++++----------\n t/t4059-diff-submodule-not-initialized.sh |  1 +\n t/t7001-mv.sh                             |  2 ++\n t/t7417-submodule-path-url.sh             |  1 +\n t/t7421-submodule-summary-add.sh          |  1 +\n 5 files changed, 30 insertions(+), 19 deletions(-)\n\ndiff --git a/builtin/mv.c b/builtin/mv.c\nindex e461d29ca1..81ca910de6 100644\n--- a/builtin/mv.c\n+++ b/builtin/mv.c\n@@ -82,21 +82,23 @@ static char *add_slash(const char *path)\n \n #define SUBMODULE_WITH_GITDIR ((const char *)1)\n \n-static void prepare_move_submodule(const char *src, int first,\n-\t\t\t\t   const char **submodule_gitfile)\n+static const char *submodule_gitfile_path(const char *src, int first)\n {\n \tstruct strbuf submodule_dotgit = STRBUF_INIT;\n+\tconst char *path;\n+\n \tif (!S_ISGITLINK(the_repository->index->cache[first]->ce_mode))\n \t\tdie(_(\"Directory %s is in index and no submodule?\"), src);\n \tif (!is_staging_gitmodules_ok(the_repository->index))\n \t\tdie(_(\"Please stage your changes to .gitmodules or stash them to proceed\"));\n+\n \tstrbuf_addf(&submodule_dotgit, \"%s/.git\", src);\n-\t*submodule_gitfile = read_gitfile(submodule_dotgit.buf);\n-\tif (*submodule_gitfile)\n-\t\t*submodule_gitfile = xstrdup(*submodule_gitfile);\n-\telse\n-\t\t*submodule_gitfile = SUBMODULE_WITH_GITDIR;\n+\n+\tpath = read_gitfile(submodule_dotgit.buf);\n \tstrbuf_release(&submodule_dotgit);\n+\tif (path)\n+\t\treturn path;\n+\treturn SUBMODULE_WITH_GITDIR;\n }\n \n static int index_range_of_same_dir(const char *src, int length,\n@@ -170,7 +172,8 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \tstruct strvec sources = STRVEC_INIT;\n \tstruct strvec dest_paths = STRVEC_INIT;\n \tstruct strvec destinations = STRVEC_INIT;\n-\tconst char **submodule_gitfile;\n+\tstruct strvec submodule_gitfiles_to_free = STRVEC_INIT;\n+\tconst char **submodule_gitfiles;\n \tchar *dst_w_slash = NULL;\n \tconst char **src_dir = NULL;\n \tint src_dir_nr = 0, src_dir_alloc = 0;\n@@ -208,7 +211,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\tflags = 0;\n \tinternal_prefix_pathspec(&dest_paths, prefix, argv + argc, 1, flags);\n \tdst_w_slash = add_slash(dest_paths.v[0]);\n-\tsubmodule_gitfile = xcalloc(argc, sizeof(char *));\n+\tsubmodule_gitfiles = xcalloc(argc, sizeof(char *));\n \n \tif (dest_paths.v[0][0] == '\\0')\n \t\t/* special case: \".\" was normalized to \"\" */\n@@ -306,8 +309,10 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\t\tint first = index_name_pos(the_repository->index, src, length), last;\n \n \t\t\tif (first >= 0) {\n-\t\t\t\tprepare_move_submodule(src, first,\n-\t\t\t\t\t\t       submodule_gitfile + i);\n+\t\t\t\tconst char *path = submodule_gitfile_path(src, first);\n+\t\t\t\tif (path != SUBMODULE_WITH_GITDIR)\n+\t\t\t\t\tpath = strvec_push(&submodule_gitfiles_to_free, path);\n+\t\t\t\tsubmodule_gitfiles[i] = path;\n \t\t\t\tgoto act_on_entry;\n \t\t\t} else if (index_range_of_same_dir(src, length,\n \t\t\t\t\t\t\t   &first, &last) < 1) {\n@@ -323,7 +328,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \n \t\t\tn = argc + last - first;\n \t\t\tREALLOC_ARRAY(modes, n);\n-\t\t\tREALLOC_ARRAY(submodule_gitfile, n);\n+\t\t\tREALLOC_ARRAY(submodule_gitfiles, n);\n \n \t\t\tdst_with_slash = add_slash(dst);\n \t\t\tdst_with_slash_len = strlen(dst_with_slash);\n@@ -338,7 +343,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \n \t\t\t\tmemset(modes + argc + j, 0, sizeof(enum update_mode));\n \t\t\t\tmodes[argc + j] |= ce_skip_worktree(ce) ? SPARSE : INDEX;\n-\t\t\t\tsubmodule_gitfile[argc + j] = NULL;\n+\t\t\t\tsubmodule_gitfiles[argc + j] = NULL;\n \n \t\t\t\tfree(prefixed_path);\n \t\t\t}\n@@ -427,8 +432,8 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\t\tstrvec_remove(&sources, i);\n \t\t\tstrvec_remove(&destinations, i);\n \t\t\tMOVE_ARRAY(modes + i, modes + i + 1, n);\n-\t\t\tMOVE_ARRAY(submodule_gitfile + i,\n-\t\t\t\t   submodule_gitfile + i + 1, n);\n+\t\t\tMOVE_ARRAY(submodule_gitfiles + i,\n+\t\t\t\t   submodule_gitfiles + i + 1, n);\n \t\t\ti--;\n \t\t}\n \t}\n@@ -462,12 +467,12 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\t\t\tcontinue;\n \t\t\tdie_errno(_(\"renaming '%s' failed\"), src);\n \t\t}\n-\t\tif (submodule_gitfile[i]) {\n+\t\tif (submodule_gitfiles[i]) {\n \t\t\tif (!update_path_in_gitmodules(src, dst))\n \t\t\t\tgitmodules_modified = 1;\n-\t\t\tif (submodule_gitfile[i] != SUBMODULE_WITH_GITDIR)\n+\t\t\tif (submodule_gitfiles[i] != SUBMODULE_WITH_GITDIR)\n \t\t\t\tconnect_work_tree_and_git_dir(dst,\n-\t\t\t\t\t\t\t      submodule_gitfile[i],\n+\t\t\t\t\t\t\t      submodule_gitfiles[i],\n \t\t\t\t\t\t\t      1);\n \t\t}\n \n@@ -573,7 +578,8 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \tstrvec_clear(&sources);\n \tstrvec_clear(&dest_paths);\n \tstrvec_clear(&destinations);\n-\tfree(submodule_gitfile);\n+\tstrvec_clear(&submodule_gitfiles_to_free);\n+\tfree(submodule_gitfiles);\n \tfree(modes);\n \treturn ret;\n }\ndiff --git a/t/t4059-diff-submodule-not-initialized.sh b/t/t4059-diff-submodule-not-initialized.sh\nindex d489230df8..668f526303 100755\n--- a/t/t4059-diff-submodule-not-initialized.sh\n+++ b/t/t4059-diff-submodule-not-initialized.sh\n@@ -9,6 +9,7 @@ This test tries to verify that add_submodule_odb works when the submodule was\n initialized previously but the checkout has since been removed.\n '\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n # Tested non-UTF-8 encoding\ndiff --git a/t/t7001-mv.sh b/t/t7001-mv.sh\nindex 879a6dce60..86258f9f43 100755\n--- a/t/t7001-mv.sh\n+++ b/t/t7001-mv.sh\n@@ -1,6 +1,8 @@\n #!/bin/sh\n \n test_description='git mv in subdirs'\n+\n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-diff-data.sh\n \ndiff --git a/t/t7417-submodule-path-url.sh b/t/t7417-submodule-path-url.sh\nindex 5e3051da8b..dbbb3853dc 100755\n--- a/t/t7417-submodule-path-url.sh\n+++ b/t/t7417-submodule-path-url.sh\n@@ -4,6 +4,7 @@ test_description='check handling of .gitmodule path with dash'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n test_expect_success 'setup' '\ndiff --git a/t/t7421-submodule-summary-add.sh b/t/t7421-submodule-summary-add.sh\nindex ce64d8b137..479c8fdde1 100755\n--- a/t/t7421-submodule-summary-add.sh\n+++ b/t/t7421-submodule-summary-add.sh\n@@ -10,6 +10,7 @@ while making sure to add submodules using `git submodule add` instead of\n `git add` as done in t7401.\n '\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n test_expect_success 'setup' '\n-- \n2.45.1.216.g4365c6fcf9.dirty\n\n"},{"id":"495549","messageId":"ZlBoXcJr8SJFiZbc@tanuki","threadId":"61535","inReplyTo":"70e8e2651306e9d221e5e472720a7610947580a7.1716541556.git.ps@pks.im","subject":"Re: [PATCH v2 13/21] config: plug various memory leaks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-24T10:13:49Z","receivedAt":"2024-05-24T10:13:54Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, May 24, 2024 at 12:04:12PM +0200, Patrick Steinhardt wrote:\n> Now that memory ownership rules around `git_config_string()` and\n> `git_config_pathname()` are clearer, it also got easier to spot that\n> the returned memory needs to be free'd. Plug a subset of those cases and\n> mark now-passing tests as leak free.\n\nJunio,\n\nthis mail now uses UTF-8 as encoding. Does that fix the issues for you?\n\nPatrick\n"},{"id":"495567","messageId":"xmqqh6ennqkw.fsf@gitster.g","threadId":"61535","inReplyTo":"ZlA6KHZtn4lvyFcI@tanuki","subject":"Re: [PATCH 01/20] t: mark a bunch of tests as leak-free","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-24T16:05:03Z","receivedAt":"2024-05-24T16:05:08Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Hum. Both of these skip a bunch of tests due to a missing TTY prereq on\n> my system. So I guess it's not a regression, just me missing test\n> coverage. And seemingly, the same applies to our CI systems because the\n> pipeline is green there.\n>\n> And indeed, the TTY prerequisite fails due a totally unrelated error:\n>\n>     Can't locate IO/Pty.pm in @INC\n>\n> I'll fix this locally and in our CI setup.\n\nDo you mean that you'll make IO::Pty available locally and in the CI\nsetup, which will start revealing the existing leaks in these tests?\n\nSo do we expect this step to be adjusted, not to mark these two\ntests as leak-free (yet)?\n\n> Ideally, we'd also make this thing more robust going forward, but\n> I'll leave that for a future iteraiton.\n\n--- >8 ---\nSubject: ci: make IO::Pty available\n\nWhen t/test-terminal.perl, which requires IO::Pty (and File::Copy,\nbut that comes standard with perl-modules?), does not work, the\ntests with TTY prerequisite are skipped.\n\nMake sure it is available in the CI environment.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n ci/install-dependencies.sh | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git c/ci/install-dependencies.sh w/ci/install-dependencies.sh\nindex 2e7688ae8b..b24c91a30f 100755\n--- c/ci/install-dependencies.sh\n+++ w/ci/install-dependencies.sh\n@@ -42,7 +42,7 @@ ubuntu-*)\n \t\tlanguage-pack-is libsvn-perl apache2 cvs cvsps git gnupg subversion \\\n \t\tmake libssl-dev libcurl4-openssl-dev libexpat-dev wget sudo default-jre \\\n \t\ttcl tk gettext zlib1g-dev perl-modules liberror-perl libauthen-sasl-perl \\\n-\t\tlibemail-valid-perl libio-socket-ssl-perl libnet-smtp-ssl-perl libdbd-sqlite3-perl libcgi-pm-perl \\\n+\t\tlibemail-valid-perl libio-socket-ssl-perl libnet-smtp-ssl-perl libdbd-sqlite3-perl libcgi-pm-perl libio-pty-perl \\\n \t\t${CC_PACKAGE:-${CC:-gcc}} $PYTHON_PACKAGE\n \n \tmkdir --parents \"$CUSTOM_PATH\"\n"},{"id":"495568","messageId":"xmqqcypbnqav.fsf@gitster.g","threadId":"61535","inReplyTo":"ZlA6jZmKomOu1p_S@tanuki","subject":"Re: [PATCH 12/20] config: plug various memory leaks","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-24T16:11:04Z","receivedAt":"2024-05-24T16:11:09Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n>> The \"message\" part in it says that it is encoded in iso-8859-1:\n>> \n>>     --ovDSRLSkA00eIgbN\n>>     Content-Type: text/plain; charset=iso-8859-1\n>>     Content-Disposition: inline\n>>     Content-Transfer-Encoding: quoted-printable\n>> ...\n>> \n>> But the source in t/t4210-log-i18n.sh actually is written in UTF-8.\n>> The thing is, the \"-u\" option (recode into utf-8\" is supposed to be\n>> the default for \"git am\", and it is passed down to the underlying\n>> mailinfo machinery in builtin/am.c:parse_mail().\n>> \n>> But apparently that is not working correctly.  I see in the patch an\n>> unrecoded byte E9 in the resulting patch file that is fed to the\n>> underlying \"git apply\" machinery, failing the application.\n>\n> Hm. I'll double check mail headers before sending out the next iteration.\n\nJust to make sure there is no misunderstanding, I am not saying that\nsending =E9 in a part that is marked as charset=iso-8859-1 using\nquoted-printable to express the letter é in a context line of a file\nthat is encoded in UTF-8.  In other words, I am not saying that your\ne-mail setup is suspect.  Instead, I am saying that the behaviour on\nthe receiving end of \"git am\" that eventually calls \"git mailinfo\" is\nsuspicious.\n\nThanks.\n"},{"id":"495569","messageId":"xmqq8qzznq98.fsf@gitster.g","threadId":"61535","inReplyTo":"ZlBV-QtqoNX-7m8a@tanuki","subject":"Re: [PATCH 12/20] config: plug various memory leaks","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-24T16:12:03Z","receivedAt":"2024-05-24T16:12:07Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> I've changed my mutt configuration to prefer UTF-8 over ISO-8859-1, so\n> the next iteration should hopefully work alright. But as you say, this\n> is probably something that needs to be fixed in the git-am(1) machinery.\n\nYup, you can probably keep the preference to annoy me into looking\ninto the issue ;-)\n\nThanks.\n"},{"id":"495571","messageId":"xmqqr0drmas8.fsf@gitster.g","threadId":"61535","inReplyTo":"857b8b14ce519e3c473f1808da7daca95ebff8ea.1716541556.git.ps@pks.im","subject":"Re: [PATCH v2 01/21] ci: add missing dependency for TTY prereq","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-24T16:31:35Z","receivedAt":"2024-05-24T16:31:40Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> In \"t/lib-terminal.sh\", we declare a lazy prerequisite for tests that\n> require a TTY. The prerequisite uses a Perl script to figure out whether\n> we do have a usable TTY or not and thus implicitly depends on the PERL\n> prerequisite, as well. Furthermore though, the script requires another\n> dependency that is easy to miss, namely on the IO::Pty module. If that\n> module is not installed, then the script will exit early due to an\n> reason unrelated to missing TTYs.\n>\n> This easily leads to missing test coverage. But most importantly, our CI\n> systems are missing this dependency and thus don't execute those tests\n> at all. Fix this.\n> ---\n>  ci/install-dependencies.sh | 4 ++--\n>  1 file changed, 2 insertions(+), 2 deletions(-)\n\nAh, I should have read what I received first before starting to\nrespond to them ;-)  You'd want to sign it off, but other than that\nlooks good to me.\n\n> diff --git a/ci/install-dependencies.sh b/ci/install-dependencies.sh\n> index 2e7688ae8b..6ec0f85972 100755\n> --- a/ci/install-dependencies.sh\n> +++ b/ci/install-dependencies.sh\n> @@ -27,7 +27,7 @@ alpine-*)\n>  \tapk add --update shadow sudo build-base curl-dev openssl-dev expat-dev gettext \\\n>  \t\tpcre2-dev python3 musl-libintl perl-utils ncurses \\\n>  \t\tapache2 apache2-http2 apache2-proxy apache2-ssl apache2-webdav apr-util-dbd_sqlite3 \\\n> -\t\tbash cvs gnupg perl-cgi perl-dbd-sqlite >/dev/null\n> +\t\tbash cvs gnupg perl-cgi perl-dbd-sqlite perl-io-tty >/dev/null\n>  \t;;\n>  fedora-*)\n>  \tdnf -yq update >/dev/null &&\n> @@ -42,7 +42,7 @@ ubuntu-*)\n>  \t\tlanguage-pack-is libsvn-perl apache2 cvs cvsps git gnupg subversion \\\n>  \t\tmake libssl-dev libcurl4-openssl-dev libexpat-dev wget sudo default-jre \\\n>  \t\ttcl tk gettext zlib1g-dev perl-modules liberror-perl libauthen-sasl-perl \\\n> -\t\tlibemail-valid-perl libio-socket-ssl-perl libnet-smtp-ssl-perl libdbd-sqlite3-perl libcgi-pm-perl \\\n> +\t\tlibemail-valid-perl libio-pty-perl libio-socket-ssl-perl libnet-smtp-ssl-perl libdbd-sqlite3-perl libcgi-pm-perl \\\n>  \t\t${CC_PACKAGE:-${CC:-gcc}} $PYTHON_PACKAGE\n>  \n>  \tmkdir --parents \"$CUSTOM_PATH\"\n"},{"id":"495582","messageId":"xmqqjzjjksfs.fsf@gitster.g","threadId":"61535","inReplyTo":"xmqqh6ennqkw.fsf@gitster.g","subject":"Re: [PATCH 01/20] t: mark a bunch of tests as leak-free","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-24T17:53:11Z","receivedAt":"2024-05-24T17:53:14Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> --- >8 ---\n> Subject: ci: make IO::Pty available\n>\n> When t/test-terminal.perl, which requires IO::Pty (and File::Copy,\n> but that comes standard with perl-modules?), does not work, the\n> tests with TTY prerequisite are skipped.\n\n... please ignore this, of course ;-) Your latest iteration does the\nright thing on this one, and unmarks the two that are not yet ready.\n\nThanks.\n"},{"id":"495602","messageId":"CAOLa=ZT5_E86EdCfejF9QR+P3wgsirxxyL2fvQTOf6k_+NUD9A@mail.gmail.com","threadId":"61535","inReplyTo":"0e9fa9ca7386f527903887242008b5b0443ada69.1716465556.git.ps@pks.im","subject":"Re: [PATCH 01/20] t: mark a bunch of tests as leak-free","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-24T20:34:53Z","receivedAt":"2024-05-24T20:34:55Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> There are a bunch of tests which do not have any leaks:\n>\n>   - t0411: Introduced via 5c5a4a1c05 (t0411: add tests for cloning from\n>     partial repo, 2024-01-28), passes since its inception.\n>\n>   - t0610: Introduced via 57db2a094d (refs: introduce reftable backend,\n>     2024-02-07), passes since its inception.\n>\n>   - t2405: Passes since 6741e917de (repository: avoid leaking\n>     `fsmonitor` data, 2024-04-12).\n>\n>   - t4153: Passes since 71c7916053 (apply: plug a leak in apply_data,\n>     2024-04-23).\n>\n>   - t7006: Passes since at least Git v2.40. I did not care to go back\n>     any further than that.\n>\n>   - t7423: Introduced via b20c10fd9b (t7423: add tests for symlinked\n>     submodule directories, 2024-01-28), passes since e8d0608944\n>     (submodule: require the submodule path to contain directories only,\n>     2024-03-26). The fix is not ovbiously related, but probably works\n\ns/ovbiously/obviously\n\n>     because we now die early in many code paths.\n>\n>   - t9xxx: All of these are exercising CVS-related tooling and pass\n>     since at least Git v2.40. It's likely that these pass for a long\n>     time already, but nobody ever noticed because noone has CVS on their\n\ns/noone/no one\n\n>     machine.\n>\n> Mark all of these tests as passing.\n>\n\n[snip]\n"},{"id":"495603","messageId":"CAOLa=ZS09FYrFUvUYzoBy27q3NSEXEO--4mCCqLSwZXDCD155Q@mail.gmail.com","threadId":"61535","inReplyTo":"05fbadbae2184479c87c37675dde7bd79b3e32ab.1716465556.git.ps@pks.im","subject":"Re: [PATCH 02/20] transport-helper: fix leaking helper name","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-24T20:38:58Z","receivedAt":"2024-05-24T20:39:00Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> When initializing the transport helper in `transport_get()`, we\n> allocate the name of the helper. We neither end up transferring\n> ownership of the name, nor do we free it. The associated memory thus\n> leaks.\n>\n> Fix this memory leak and mark now-passing tests as leak free.\n>\n\nSo this is done by removing the `const` from the `name` field in\n`helper_data` so we can assign and free its memory. Looks good.\n\n[snip]\n"},{"id":"495618","messageId":"xmqqwmniiqv2.fsf@gitster.g","threadId":"61535","inReplyTo":"cover.1716541556.git.ps@pks.im","subject":"Re: [PATCH v2 00/21] Various memory leak fixes","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-25T02:10:09Z","receivedAt":"2024-05-25T02:10:14Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> this is the second version of my patch series that fixes various memory\n> leaks in Git. Changes compared to v1:\n>\n>   - t4153 and t7006 aren't marked as passing anymore. I thought they\n>     pass because most of these tests were skipped because of a missing\n>     TTY prerequisite both on my local machine, but also in our CI.\n>\n>   - Add another patch to install the Perl IO:Pty module on Alpine and\n>     Ubuntu. This fulfills the TTY prerequisite and thus surfaces the\n>     memory leaks in both of the above tests.\n>\n>   - Add another unit test for strvec that exercise replacing a string in\n>     the strvec with a copy of itself.\n>\n>   - A bunch of commit message improvements.\n\nLooking very good.  This seems to reveal existing leaks when merged\nto 'seen'; other topics that are not in 'master' may be introducing\nthese leaks.  I'll see if a trial merge to 'next' is leak-free (in\nwhich case I'll merge it down to 'next') or there are other topics\nin 'next' that are leaking (in which case we'll play by ear---either\nmark the tests again as non-leak-free, or plug the leak if it seems\ntrivial).\n\n https://github.com/git/git/actions/runs/9231313414/job/25400998823\n\nsays t1400-update-ref has many \"stdin symref-update\" things are\nfailing.\n\nAlso\n\n https://github.com/git/git/actions/runs/9231313414/job/25401102951\n\nshows that t1460-refs-migrate fails on Windows.\n\nThanks.\n"},{"id":"495624","messageId":"20240525043347.GA1895047@coredump.intra.peff.net","threadId":"61535","inReplyTo":"70e8e2651306e9d221e5e472720a7610947580a7.1716541556.git.ps@pks.im","subject":"Re: [PATCH v2 13/21] config: plug various memory leaks","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-05-25T04:33:47Z","receivedAt":"2024-05-25T04:33:48Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Fri, May 24, 2024 at 12:04:12PM +0200, Patrick Steinhardt wrote:\n\n> diff --git a/alias.c b/alias.c\n> index 269892c356..4daafd9bda 100644\n> --- a/alias.c\n> +++ b/alias.c\n> @@ -21,9 +21,11 @@ static int config_alias_cb(const char *key, const char *value,\n>  \t\treturn 0;\n>  \n>  \tif (data->alias) {\n> -\t\tif (!strcasecmp(p, data->alias))\n> +\t\tif (!strcasecmp(p, data->alias)) {\n> +\t\t\tFREE_AND_NULL(data->v);\n>  \t\t\treturn git_config_string(&data->v,\n>  \t\t\t\t\t\t key, value);\n> +\t\t}\n>  \t} else if (data->list) {\n>  \t\tstring_list_append(data->list, p);\n>  \t}\n\nIMHO this should be done automatically by git_config_string(). The\ncurrent design is an accident waiting to happen, and in the long run\nevery call is going to need this FREE_AND_NULL(). By doing it in the\nfunction the calling code is shorter, and there's no way we'll forget.\n\nI posted a series along those lines a month or so ago:\n\n  https://lore.kernel.org/git/20240407005656.GA436890@coredump.intra.peff.net/\n\nThe catch is that you can't do this:\n\n  const char *foo = \"bar\";\n  git_config_string(&foo, ...);\n\nSo I introduced a new function that took a non-const pointer with the\nnew behavior, with the idea that we'd eventually migrate everything\nover. It looks like you may have already done that migration earlier in\nyour series, since the move to \"char *\" in the previous patch was OK.\n\n  Though as a side note, sadly:\n\n    char *foo = \"bar\";\n\n  does not produce an error or even a warning without -Wwrite-strings. I\n  think in the long run we should enable that, but there's a little\n  cleanup required to do so.\n\nThe main reason I didn't follow up on that earlier series is that there\nwas some discussion about maybe moving this stuff over to strbufs (after\nteaching it to handle literal initializers). But if you've managed to\nremove all of the cases that needed that, I think just sticking with\n\"char *\" is fine.\n\nThe other issue raised in that thread is that many of these config\nvariables are also passed to parse-options, which treats them as const\nstrings (and we get no compiler support because it goes through a void\npointer). So they may leak if we overwrite them, or in the unusual case\nthat we load config after parsing options, we may try to free a non-heap\nstring. The one we discussed was log's signature_file, and it looks like\nyou split that to use two variables, which works. Junio suggested an\nOPT_FILENAME_DUP() option, which I'm also OK with. The main challenge to\nme is being sure we found all such spots (and not accidentally\nintroducing new ones). But I don't have a good solution there.\n\n> @@ -1566,7 +1569,7 @@ static int git_default_core_config(const char *var, const char *value,\n>  \n>  \tif (!strcmp(var, \"core.checkroundtripencoding\")) {\n>  \t\tFREE_AND_NULL(check_roundtrip_encoding);\n> -\t\treturn git_config_string((const char **) &check_roundtrip_encoding, var, value);\n> +\t\treturn git_config_string(&check_roundtrip_encoding, var, value);\n>  \t}\n\nThis should have lost its cast in the previous commit, no? Applying up\nto patch 12 and building with DEVELOPER=1 gets a warning.\n\n-Peff\n"},{"id":"495625","messageId":"20240525044635.GB1895047@coredump.intra.peff.net","threadId":"61535","inReplyTo":"9dd8709d1b3b350008218133986befdb2ae74bae.1716541556.git.ps@pks.im","subject":"Re: [PATCH v2 04/21] strbuf: fix leak when `appendwholeline()` fails with EOF","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-05-25T04:46:35Z","receivedAt":"2024-05-25T04:46:36Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Fri, May 24, 2024 at 12:03:29PM +0200, Patrick Steinhardt wrote:\n\n> In `strbuf_appendwholeline()` we call `strbuf_getwholeline()` with a\n> temporary buffer. In case the call returns an error we indicate this by\n> returning EOF, but never release the temporary buffer. This can lead to\n> a memory leak when the line has been partially filled. Fix this.\n\nHmm, doesn't this indicate a bug in getwholeline()? Most strbuf\nfunctions on error try to leave the allocation as-is.\n\nAt the end of the getdelim() version (which is probably what you're\nrunning), when we see an error we do:\n\n        if (!sb->buf)\n                strbuf_init(sb, 0);\n        else\n                strbuf_reset(sb);\n        return EOF;\n\nSo if getdelim() returned error and left us with a buffer (but still\nreturned -1 for error!), I think this code is assuming that the buffer\nit left us with was the same one that existed beforehand.\n\nBut your commit message implies that it might allocate, hit an error,\nand then return that error along with an allocated buffer? Looks like\nthat matches the getdelim() manpage, which says:\n\n  If *lineptr was set to NULL before the call, then the buffer should be\n  freed by the user program even on failure.\n\nSo should we do something like:\n\ndiff --git a/strbuf.c b/strbuf.c\nindex e1076c9891..e37165812b 100644\n--- a/strbuf.c\n+++ b/strbuf.c\n@@ -659,7 +659,7 @@ int strbuf_getwholeline(struct strbuf *sb, FILE *fp, int term)\n \tif (!sb->buf)\n \t\tstrbuf_init(sb, 0);\n \telse\n-\t\tstrbuf_reset(sb);\n+\t\tstrbuf_release(sb);\n \treturn EOF;\n }\n #else\n\nThat assumes sb->alloc is valid after a failed call, since\nstrbuf_release() checks it. But that seems reasonable. If not, we'd need\nto free() and re-initialize it ourselves, and the code is more like:\n\ndiff --git a/strbuf.c b/strbuf.c\nindex e1076c9891..aed699c6bf 100644\n--- a/strbuf.c\n+++ b/strbuf.c\n@@ -656,10 +656,8 @@ int strbuf_getwholeline(struct strbuf *sb, FILE *fp, int term)\n \t * we can just re-init, but otherwise we should make sure that our\n \t * length is empty, and that the result is NUL-terminated.\n \t */\n-\tif (!sb->buf)\n-\t\tstrbuf_init(sb, 0);\n-\telse\n-\t\tstrbuf_reset(sb);\n+\tFREE_AND_NULL(sb->buf);\n+\tstrbuf_init(sb, 0);\n \treturn EOF;\n }\n #else\n\nBut I think either of those would solve your leak, _and_ would help with\nsimilar leaks of strbuf_getwholeline() and friends.\n\n-Peff\n"},{"id":"495664","messageId":"ZlQr11P_C-KyO2DF@tanuki","threadId":"61535","inReplyTo":"xmqqwmniiqv2.fsf@gitster.g","subject":"Re: [PATCH v2 00/21] Various memory leak fixes","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-27T06:44:39Z","receivedAt":"2024-05-27T06:44:46Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, May 24, 2024 at 07:10:09PM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > this is the second version of my patch series that fixes various memory\n> > leaks in Git. Changes compared to v1:\n> >\n> >   - t4153 and t7006 aren't marked as passing anymore. I thought they\n> >     pass because most of these tests were skipped because of a missing\n> >     TTY prerequisite both on my local machine, but also in our CI.\n> >\n> >   - Add another patch to install the Perl IO:Pty module on Alpine and\n> >     Ubuntu. This fulfills the TTY prerequisite and thus surfaces the\n> >     memory leaks in both of the above tests.\n> >\n> >   - Add another unit test for strvec that exercise replacing a string in\n> >     the strvec with a copy of itself.\n> >\n> >   - A bunch of commit message improvements.\n> \n> Looking very good.  This seems to reveal existing leaks when merged\n> to 'seen'; other topics that are not in 'master' may be introducing\n> these leaks.  I'll see if a trial merge to 'next' is leak-free (in\n> which case I'll merge it down to 'next') or there are other topics\n> in 'next' that are leaking (in which case we'll play by ear---either\n> mark the tests again as non-leak-free, or plug the leak if it seems\n> trivial).\n> \n>  https://github.com/git/git/actions/runs/9231313414/job/25400998823\n> \n> says t1400-update-ref has many \"stdin symref-update\" things are\n> failing.\n\nIndeed. The following diff fixes the leak:\n\n    diff --git a/builtin/update-ref.c b/builtin/update-ref.c\n    index 7d2a419230..e54be9c429 100644\n    --- a/builtin/update-ref.c\n    +++ b/builtin/update-ref.c\n    @@ -130,6 +130,8 @@ static char *parse_next_arg(const char **next)\n     \n        if (arg.len)\n            return strbuf_detach(&arg, NULL);\n    +\n    +\tstrbuf_release(&arg);\n        return NULL;\n     }\n     \n\nKarthik is out of office this week, so you may want to add this as a\n\"SQUASH???\" commit on top of his topic branch to make \"seen\" pass.\n\n> Also\n> \n>  https://github.com/git/git/actions/runs/9231313414/job/25401102951\n> \n> shows that t1460-refs-migrate fails on Windows.\n\nHm, this one is curious. There are no leak logs at all, and the exit\ncode is 139. Might be SIGSEGV, indicating that something else is going\non here than a memory leak.\n\nI'll investigate.\n\nPatrick\n"},{"id":"495665","messageId":"ZlQr3tsDTSOGvFUQ@tanuki","threadId":"61535","inReplyTo":"20240525044635.GB1895047@coredump.intra.peff.net","subject":"Re: [PATCH v2 04/21] strbuf: fix leak when `appendwholeline()` fails with EOF","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-27T06:44:46Z","receivedAt":"2024-05-27T06:44:51Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sat, May 25, 2024 at 12:46:35AM -0400, Jeff King wrote:\n> On Fri, May 24, 2024 at 12:03:29PM +0200, Patrick Steinhardt wrote:\n> \n> > In `strbuf_appendwholeline()` we call `strbuf_getwholeline()` with a\n> > temporary buffer. In case the call returns an error we indicate this by\n> > returning EOF, but never release the temporary buffer. This can lead to\n> > a memory leak when the line has been partially filled. Fix this.\n> \n> Hmm, doesn't this indicate a bug in getwholeline()? Most strbuf\n> functions on error try to leave the allocation as-is.\n> \n> At the end of the getdelim() version (which is probably what you're\n> running), when we see an error we do:\n> \n>         if (!sb->buf)\n>                 strbuf_init(sb, 0);\n>         else\n>                 strbuf_reset(sb);\n>         return EOF;\n> \n> So if getdelim() returned error and left us with a buffer (but still\n> returned -1 for error!), I think this code is assuming that the buffer\n> it left us with was the same one that existed beforehand.\n> \n> But your commit message implies that it might allocate, hit an error,\n> and then return that error along with an allocated buffer? Looks like\n> that matches the getdelim() manpage, which says:\n> \n>   If *lineptr was set to NULL before the call, then the buffer should be\n>   freed by the user program even on failure.\n> \n> So should we do something like:\n> \n> diff --git a/strbuf.c b/strbuf.c\n> index e1076c9891..e37165812b 100644\n> --- a/strbuf.c\n> +++ b/strbuf.c\n> @@ -659,7 +659,7 @@ int strbuf_getwholeline(struct strbuf *sb, FILE *fp, int term)\n>  \tif (!sb->buf)\n>  \t\tstrbuf_init(sb, 0);\n>  \telse\n> -\t\tstrbuf_reset(sb);\n> +\t\tstrbuf_release(sb);\n>  \treturn EOF;\n>  }\n>  #else\n> \n> That assumes sb->alloc is valid after a failed call, since\n> strbuf_release() checks it. But that seems reasonable. If not, we'd need\n> to free() and re-initialize it ourselves, and the code is more like:\n> \n> diff --git a/strbuf.c b/strbuf.c\n> index e1076c9891..aed699c6bf 100644\n> --- a/strbuf.c\n> +++ b/strbuf.c\n> @@ -656,10 +656,8 @@ int strbuf_getwholeline(struct strbuf *sb, FILE *fp, int term)\n>  \t * we can just re-init, but otherwise we should make sure that our\n>  \t * length is empty, and that the result is NUL-terminated.\n>  \t */\n> -\tif (!sb->buf)\n> -\t\tstrbuf_init(sb, 0);\n> -\telse\n> -\t\tstrbuf_reset(sb);\n> +\tFREE_AND_NULL(sb->buf);\n> +\tstrbuf_init(sb, 0);\n>  \treturn EOF;\n>  }\n>  #else\n> \n> But I think either of those would solve your leak, _and_ would help with\n> similar leaks of strbuf_getwholeline() and friends.\n\nI'm not quite convinced that `strbuf_getwholeline()` should deallocate\nthe buffer for the caller, I think that makes for quite a confusing\ncalling convention. The caller may want to reuse the buffer for other\noperations, and it feels hostile to release the buffer under their feet.\n\nThe only edge case where I think it would make sense to free allocated\ndata is when being passed a not-yet-allocated strbuf. But I wonder\nwhether the added complexity would be worth it.\n\nI've been going through all callsites and couldn't spot any that doesn't\nfree the buffer on EOF. So I'd propose to leave this as-is and revisit\nif we eventually see that this is causing more memory leaks.\n\nPatrick\n"},{"id":"495666","messageId":"ZlQsKp2IHt2wankn@tanuki","threadId":"61535","inReplyTo":"20240525043347.GA1895047@coredump.intra.peff.net","subject":"Re: [PATCH v2 13/21] config: plug various memory leaks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-27T06:46:02Z","receivedAt":"2024-05-27T06:46:08Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sat, May 25, 2024 at 12:33:47AM -0400, Jeff King wrote:\n> On Fri, May 24, 2024 at 12:04:12PM +0200, Patrick Steinhardt wrote:\n> \n> > diff --git a/alias.c b/alias.c\n> > index 269892c356..4daafd9bda 100644\n> > --- a/alias.c\n> > +++ b/alias.c\n> > @@ -21,9 +21,11 @@ static int config_alias_cb(const char *key, const char *value,\n> >  \t\treturn 0;\n> >  \n> >  \tif (data->alias) {\n> > -\t\tif (!strcasecmp(p, data->alias))\n> > +\t\tif (!strcasecmp(p, data->alias)) {\n> > +\t\t\tFREE_AND_NULL(data->v);\n> >  \t\t\treturn git_config_string(&data->v,\n> >  \t\t\t\t\t\t key, value);\n> > +\t\t}\n> >  \t} else if (data->list) {\n> >  \t\tstring_list_append(data->list, p);\n> >  \t}\n> \n> IMHO this should be done automatically by git_config_string(). The\n> current design is an accident waiting to happen, and in the long run\n> every call is going to need this FREE_AND_NULL(). By doing it in the\n> function the calling code is shorter, and there's no way we'll forget.\n\nIn fact, I had this in my first iteration. But I didn't feel comfortable\nwith it exactly due to the reasons you mention below, where often times\nwe assign string constants as default values. This requires us to be\nextremely careful, also because we do not yet have `-Wwrite-strings`\nenabled as you mention.\n\n> I posted a series along those lines a month or so ago:\n> \n>   https://lore.kernel.org/git/20240407005656.GA436890@coredump.intra.peff.net/\n> \n> The catch is that you can't do this:\n> \n>   const char *foo = \"bar\";\n>   git_config_string(&foo, ...);\n> \n> So I introduced a new function that took a non-const pointer with the\n> new behavior, with the idea that we'd eventually migrate everything\n> over. It looks like you may have already done that migration earlier in\n> your series, since the move to \"char *\" in the previous patch was OK.\n> \n>   Though as a side note, sadly:\n> \n>     char *foo = \"bar\";\n> \n>   does not produce an error or even a warning without -Wwrite-strings. I\n>   think in the long run we should enable that, but there's a little\n>   cleanup required to do so.\n\nIndeed, I had the exact same observation. I've already got a patch\nseries that enables `-Wwrite-strings` and that adapts our codebase to\ncompile cleanly with it. I'll send that series once this one here has\nlanded.\n\nSo my proposal would be to leave this patch as-is for the time being,\nbut revisit it once both patch series have landed. Does that work for\nyou?\n\n> The main reason I didn't follow up on that earlier series is that\n> there was some discussion about maybe moving this stuff over to\n> strbufs (after teaching it to handle literal initializers). But if\n> you've managed to remove all of the cases that needed that, I think\n> just sticking with \"char *\" is fine.\n\nI don't think I managed to hit every callsite yet that leaks memory. But\nI think it shouldn't be too bad, and especiall if we follow up this\npatch series with `FREE_AND_NULL()` on the out-parameter then this\nshould be fine.\n\n> The other issue raised in that thread is that many of these config\n> variables are also passed to parse-options, which treats them as const\n> strings (and we get no compiler support because it goes through a void\n> pointer). So they may leak if we overwrite them, or in the unusual\n> case that we load config after parsing options, we may try to free a\n> non-heap string. The one we discussed was log's signature_file, and it\n> looks like you split that to use two variables, which works. Junio\n> suggested an OPT_FILENAME_DUP() option, which I'm also OK with. The\n> main challenge to me is being sure we found all such spots (and not\n> accidentally introducing new ones). But I don't have a good solution\n> there.\n\nYup, I remember having some issues with `OPT_FILENAME()`, but to the\nbest of my knowledge I've fixed all of them.\n\n> > @@ -1566,7 +1569,7 @@ static int git_default_core_config(const char\n> > *var, const char *value,\n> >  \n> >  \tif (!strcmp(var, \"core.checkroundtripencoding\")) {\n> >  \tFREE_AND_NULL(check_roundtrip_encoding); -\t\treturn\n> >  \tgit_config_string((const char **) &check_roundtrip_encoding,\n> >  \tvar, value); +\t\treturn\n> >  \tgit_config_string(&check_roundtrip_encoding, var, value); }\n> \n> This should have lost its cast in the previous commit, no? Applying up\n> to patch 12 and building with DEVELOPER=1 gets a warning.\n\nAh, good catch. Will fix.\n\nPatrick\n"},{"id":"495670","messageId":"cover.1716810168.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716465556.git.ps@pks.im","subject":"[PATCH v3 00/21] Various memory leak fixes","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-27T11:45:42Z","receivedAt":"2024-05-27T11:45:50Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis is the third version of my patch series that fixes various memory\nleaks throughout Git.\n\nChanges compared to v2:\n\n    - Add a missing SOB.\n\n    - Fix some typos in commit messages.\n\n    - Explain more thoroughly why `appendwholeline()` may leak even on\n      error.\n\n    - Move removal of `const char **` cast to the correct commit.\n\nThanks!\n\nPatrick\n\nPatrick Steinhardt (21):\n  ci: add missing dependency for TTY prereq\n  t: mark a bunch of tests as leak-free\n  transport-helper: fix leaking helper name\n  strbuf: fix leak when `appendwholeline()` fails with EOF\n  checkout: clarify memory ownership in `unique_tracking_name()`\n  http: refactor code to clarify memory ownership\n  config: clarify memory ownership in `git_config_pathname()`\n  diff: refactor code to clarify memory ownership of prefixes\n  convert: refactor code to clarify ownership of\n    check_roundtrip_encoding\n  builtin/log: stop using globals for log config\n  builtin/log: stop using globals for format config\n  config: clarify memory ownership in `git_config_string()`\n  config: plug various memory leaks\n  builtin/credential: clear credential before exit\n  commit-reach: fix memory leak in `ahead_behind()`\n  submodule: fix leaking memory for submodule entries\n  strvec: add functions to replace and remove strings\n  builtin/mv: refactor `add_slash()` to always return allocated strings\n  builtin/mv duplicate string list memory\n  builtin/mv: refactor to use `struct strvec`\n  builtin/mv: fix leaks for submodule gitfile paths\n\n Makefile                                      |   1 +\n alias.c                                       |   6 +-\n attr.c                                        |   2 +-\n attr.h                                        |   2 +-\n builtin/blame.c                               |   2 +-\n builtin/checkout.c                            |  14 +-\n builtin/commit.c                              |   4 +-\n builtin/config.c                              |   2 +-\n builtin/credential.c                          |   2 +\n builtin/log.c                                 | 708 ++++++++++--------\n builtin/merge.c                               |   4 +-\n builtin/mv.c                                  | 222 +++---\n builtin/rebase.c                              |   2 +-\n builtin/receive-pack.c                        |   6 +-\n builtin/repack.c                              |   8 +-\n builtin/worktree.c                            |  20 +-\n checkout.c                                    |   4 +-\n checkout.h                                    |   6 +-\n ci/install-dependencies.sh                    |   4 +-\n commit-reach.c                                |   4 +\n config.c                                      |  52 +-\n config.h                                      |  10 +-\n convert.c                                     |  30 +-\n convert.h                                     |   2 +-\n delta-islands.c                               |   2 +-\n diff.c                                        |  20 +-\n environment.c                                 |  16 +-\n environment.h                                 |  14 +-\n fetch-pack.c                                  |   4 +-\n fsck.c                                        |   4 +-\n fsmonitor-settings.c                          |   5 +-\n gpg-interface.c                               |   6 +-\n http.c                                        |  50 +-\n imap-send.c                                   |  12 +-\n mailmap.c                                     |   4 +-\n mailmap.h                                     |   4 +-\n merge-ll.c                                    |   6 +-\n pager.c                                       |   2 +-\n pretty.c                                      |  14 +-\n promisor-remote.h                             |   2 +-\n remote.c                                      |  20 +-\n remote.h                                      |   8 +-\n sequencer.c                                   |   2 +-\n setup.c                                       |   6 +-\n strbuf.c                                      |   4 +-\n strvec.c                                      |  20 +\n strvec.h                                      |  13 +\n submodule-config.c                            |   2 +\n t/t0300-credentials.sh                        |   2 +\n t/t0411-clone-from-partial.sh                 |   1 +\n t/t0610-reftable-basics.sh                    |   1 +\n t/t0611-reftable-httpd.sh                     |   1 +\n t/t1013-read-tree-submodule.sh                |   1 +\n t/t1306-xdg-files.sh                          |   1 +\n t/t1350-config-hooks-path.sh                  |   1 +\n t/t1400-update-ref.sh                         |   2 +\n t/t2013-checkout-submodule.sh                 |   1 +\n t/t2024-checkout-dwim.sh                      |   1 +\n t/t2060-switch.sh                             |   1 +\n t/t2405-worktree-submodule.sh                 |   1 +\n t/t3007-ls-files-recurse-submodules.sh        |   1 +\n t/t3203-branch-output.sh                      |   2 +\n t/t3415-rebase-autosquash.sh                  |   1 +\n t/t3426-rebase-submodule.sh                   |   1 +\n t/t3512-cherry-pick-submodule.sh              |   1 +\n t/t3513-revert-submodule.sh                   |   1 +\n t/t3600-rm.sh                                 |   1 +\n t/t3906-stash-submodule.sh                    |   1 +\n t/t4001-diff-rename.sh                        |   4 +-\n t/t4041-diff-submodule-option.sh              |   1 +\n t/t4043-diff-rename-binary.sh                 |   1 +\n t/t4059-diff-submodule-not-initialized.sh     |   1 +\n t/t4060-diff-submodule-option-diff-format.sh  |   1 +\n t/t4120-apply-popt.sh                         |   1 +\n t/t4137-apply-submodule.sh                    |   1 +\n t/t4210-log-i18n.sh                           |   2 +\n t/t5563-simple-http-auth.sh                   |   1 +\n t/t5564-http-proxy.sh                         |   1 +\n t/t5581-http-curl-verbose.sh                  |   1 +\n t/t6006-rev-list-format.sh                    |   1 +\n t/t6041-bisect-submodule.sh                   |   1 +\n t/t6400-merge-df.sh                           |   1 +\n t/t6412-merge-large-rename.sh                 |   1 +\n t/t6426-merge-skip-unneeded-updates.sh        |   1 +\n t/t6429-merge-sequence-rename-caching.sh      |   1 +\n t/t6438-submodule-directory-file-conflicts.sh |   1 +\n t/t7001-mv.sh                                 |   2 +\n t/t7005-editor.sh                             |   1 +\n t/t7102-reset.sh                              |   1 +\n t/t7112-reset-submodule.sh                    |   1 +\n t/t7417-submodule-path-url.sh                 |   1 +\n t/t7421-submodule-summary-add.sh              |   1 +\n t/t7423-submodule-symlinks.sh                 |   1 +\n t/t9129-git-svn-i18n-commitencoding.sh        |   1 -\n t/t9139-git-svn-non-utf8-commitencoding.sh    |   1 -\n t/t9200-git-cvsexportcommit.sh                |   1 +\n t/t9401-git-cvsserver-crlf.sh                 |   1 +\n t/t9600-cvsimport.sh                          |   1 +\n t/t9601-cvsimport-vendor-branch.sh            |   1 +\n t/t9602-cvsimport-branches-tags.sh            |   1 +\n t/t9603-cvsimport-patchsets.sh                |   2 +\n t/t9604-cvsimport-timestamps.sh               |   2 +\n t/unit-tests/t-strvec.c                       | 269 +++++++\n t/unit-tests/test-lib.c                       |  13 +\n t/unit-tests/test-lib.h                       |  13 +\n transport-helper.c                            |   6 +-\n transport.c                                   |   1 +\n upload-pack.c                                 |   2 +-\n userdiff.h                                    |  12 +-\n 109 files changed, 1151 insertions(+), 586 deletions(-)\n create mode 100644 t/unit-tests/t-strvec.c\n\nRange-diff against v2:\n 1:  857b8b14ce !  1:  ded220a06b ci: add missing dependency for TTY prereq\n    @@ Commit message\n         systems are missing this dependency and thus don't execute those tests\n         at all. Fix this.\n     \n    +    Signed-off-by: Patrick Steinhardt <ps@pks.im>\n    +\n      ## ci/install-dependencies.sh ##\n     @@ ci/install-dependencies.sh: alpine-*)\n      \tapk add --update shadow sudo build-base curl-dev openssl-dev expat-dev gettext \\\n 2:  ceade7dbba !  2:  f305e4bc46 t: mark a bunch of tests as leak-free\n    @@ Commit message\n           - t7423: Introduced via b20c10fd9b (t7423: add tests for symlinked\n             submodule directories, 2024-01-28), passes since e8d0608944\n             (submodule: require the submodule path to contain directories only,\n    -        2024-03-26). The fix is not ovbiously related, but probably works\n    +        2024-03-26). The fix is not obviously related, but probably works\n             because we now die early in many code paths.\n     \n           - t9xxx: All of these are exercising CVS-related tooling and pass\n             since at least Git v2.40. It's likely that these pass for a long\n    -        time already, but nobody ever noticed because noone has CVS on their\n    -        machine.\n    +        time already, but nobody ever noticed because Git developers do not\n    +        tend to have CVS on their machines.\n     \n         Mark all of these tests as passing.\n     \n 3:  a96b5ac359 =  3:  9e946c1a83 transport-helper: fix leaking helper name\n 4:  9dd8709d1b !  4:  aa5cbd9d14 strbuf: fix leak when `appendwholeline()` fails with EOF\n    @@ Commit message\n     \n         In `strbuf_appendwholeline()` we call `strbuf_getwholeline()` with a\n         temporary buffer. In case the call returns an error we indicate this by\n    -    returning EOF, but never release the temporary buffer. This can lead to\n    -    a memory leak when the line has been partially filled. Fix this.\n    +    returning EOF, but never release the temporary buffer. This can cause a\n    +    leak though because `strbuf_getwholeline()` calls getline(3). Quoting\n    +    its documentation:\n    +\n    +        If *lineptr was set to NULL before the call, then the buffer\n    +        should be freed by the user program even on failure.\n    +\n    +    Consequently, the temporary buffer may hold allocated memory even when\n    +    the call to `strbuf_getwholeline()` fails.\n    +\n    +    Fix this by releasing the temporary buffer on error.\n     \n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n 5:  6d4e9ce706 =  5:  4a6dd9b6a8 checkout: clarify memory ownership in `unique_tracking_name()`\n 6:  141cae2de1 =  6:  fa91a3942e http: refactor code to clarify memory ownership\n 7:  ff5e761e55 =  7:  88babf1abf config: clarify memory ownership in `git_config_pathname()`\n 8:  afe69c7303 =  8:  feec7e971f diff: refactor code to clarify memory ownership of prefixes\n 9:  eb7fce55b0 =  9:  dae00f1b63 convert: refactor code to clarify ownership of check_roundtrip_encoding\n10:  ee2fcf388c = 10:  02c5be27be builtin/log: stop using globals for log config\n11:  3490ad3a02 = 11:  eeba79678a builtin/log: stop using globals for format config\n12:  6cfc28c7e2 ! 12:  a4fafcd079 config: clarify memory ownership in `git_config_string()`\n    @@ config.c: int git_config_bool(const char *name, const char *value)\n      {\n      \tif (!value)\n      \t\treturn config_error_nonbool(var);\n    +@@ config.c: static int git_default_core_config(const char *var, const char *value,\n    + \n    + \tif (!strcmp(var, \"core.checkroundtripencoding\")) {\n    + \t\tFREE_AND_NULL(check_roundtrip_encoding);\n    +-\t\treturn git_config_string((const char **) &check_roundtrip_encoding, var, value);\n    ++\t\treturn git_config_string(&check_roundtrip_encoding, var, value);\n    + \t}\n    + \n    + \tif (!strcmp(var, \"core.notesref\")) {\n     @@ config.c: int git_configset_get_string(struct config_set *set, const char *key, char **des\n      {\n      \tconst char *value;\n13:  70e8e26513 ! 13:  8b74dff678 config: plug various memory leaks\n    @@ config.c: static int git_default_core_config(const char *var, const char *value,\n      \t\treturn git_config_pathname(&git_hooks_path, var, value);\n      \t}\n      \n    -@@ config.c: static int git_default_core_config(const char *var, const char *value,\n    - \n    - \tif (!strcmp(var, \"core.checkroundtripencoding\")) {\n    - \t\tFREE_AND_NULL(check_roundtrip_encoding);\n    --\t\treturn git_config_string((const char **) &check_roundtrip_encoding, var, value);\n    -+\t\treturn git_config_string(&check_roundtrip_encoding, var, value);\n    - \t}\n    - \n    - \tif (!strcmp(var, \"core.notesref\")) {\n     @@ config.c: static int git_default_core_config(const char *var, const char *value,\n      \t\treturn 0;\n      \t}\n14:  f1a1c43e76 = 14:  265665fe6c builtin/credential: clear credential before exit\n15:  64b92156f8 = 15:  b315a5bb5c commit-reach: fix memory leak in `ahead_behind()`\n16:  cd8a992f08 = 16:  7c75a94756 submodule: fix leaking memory for submodule entries\n17:  128e2eaf7a = 17:  0f61ee9929 strvec: add functions to replace and remove strings\n18:  1310b24fc2 = 18:  1830e2a568 builtin/mv: refactor `add_slash()` to always return allocated strings\n19:  d4fef9825a = 19:  9eeafac365 builtin/mv duplicate string list memory\n20:  797cdb286a = 20:  48b9d3e343 builtin/mv: refactor to use `struct strvec`\n21:  095469193c = 21:  add7946446 builtin/mv: fix leaks for submodule gitfile paths\n-- \n2.45.1.246.gb9cfe4845c.dirty\n\n"},{"id":"495671","messageId":"ded220a06bafc0ade5cea29e9809a0cc023cb08b.1716810168.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716810168.git.ps@pks.im","subject":"[PATCH v3 01/21] ci: add missing dependency for TTY prereq","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-27T11:45:47Z","receivedAt":"2024-05-27T11:45:52Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"In \"t/lib-terminal.sh\", we declare a lazy prerequisite for tests that\nrequire a TTY. The prerequisite uses a Perl script to figure out whether\nwe do have a usable TTY or not and thus implicitly depends on the PERL\nprerequisite, as well. Furthermore though, the script requires another\ndependency that is easy to miss, namely on the IO::Pty module. If that\nmodule is not installed, then the script will exit early due to an\nreason unrelated to missing TTYs.\n\nThis easily leads to missing test coverage. But most importantly, our CI\nsystems are missing this dependency and thus don't execute those tests\nat all. Fix this.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n ci/install-dependencies.sh | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/ci/install-dependencies.sh b/ci/install-dependencies.sh\nindex 2e7688ae8b..6ec0f85972 100755\n--- a/ci/install-dependencies.sh\n+++ b/ci/install-dependencies.sh\n@@ -27,7 +27,7 @@ alpine-*)\n \tapk add --update shadow sudo build-base curl-dev openssl-dev expat-dev gettext \\\n \t\tpcre2-dev python3 musl-libintl perl-utils ncurses \\\n \t\tapache2 apache2-http2 apache2-proxy apache2-ssl apache2-webdav apr-util-dbd_sqlite3 \\\n-\t\tbash cvs gnupg perl-cgi perl-dbd-sqlite >/dev/null\n+\t\tbash cvs gnupg perl-cgi perl-dbd-sqlite perl-io-tty >/dev/null\n \t;;\n fedora-*)\n \tdnf -yq update >/dev/null &&\n@@ -42,7 +42,7 @@ ubuntu-*)\n \t\tlanguage-pack-is libsvn-perl apache2 cvs cvsps git gnupg subversion \\\n \t\tmake libssl-dev libcurl4-openssl-dev libexpat-dev wget sudo default-jre \\\n \t\ttcl tk gettext zlib1g-dev perl-modules liberror-perl libauthen-sasl-perl \\\n-\t\tlibemail-valid-perl libio-socket-ssl-perl libnet-smtp-ssl-perl libdbd-sqlite3-perl libcgi-pm-perl \\\n+\t\tlibemail-valid-perl libio-pty-perl libio-socket-ssl-perl libnet-smtp-ssl-perl libdbd-sqlite3-perl libcgi-pm-perl \\\n \t\t${CC_PACKAGE:-${CC:-gcc}} $PYTHON_PACKAGE\n \n \tmkdir --parents \"$CUSTOM_PATH\"\n-- \n2.45.1.246.gb9cfe4845c.dirty\n\n"},{"id":"495672","messageId":"f305e4bc467a90a09015884b2f364fdf872ce3eb.1716810168.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716810168.git.ps@pks.im","subject":"[PATCH v3 02/21] t: mark a bunch of tests as leak-free","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-27T11:45:52Z","receivedAt":"2024-05-27T11:45:56Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"There are a bunch of tests which do not have any leaks:\n\n  - t0411: Introduced via 5c5a4a1c05 (t0411: add tests for cloning from\n    partial repo, 2024-01-28), passes since its inception.\n\n  - t0610: Introduced via 57db2a094d (refs: introduce reftable backend,\n    2024-02-07), passes since its inception.\n\n  - t2405: Passes since 6741e917de (repository: avoid leaking\n    `fsmonitor` data, 2024-04-12).\n\n  - t7423: Introduced via b20c10fd9b (t7423: add tests for symlinked\n    submodule directories, 2024-01-28), passes since e8d0608944\n    (submodule: require the submodule path to contain directories only,\n    2024-03-26). The fix is not obviously related, but probably works\n    because we now die early in many code paths.\n\n  - t9xxx: All of these are exercising CVS-related tooling and pass\n    since at least Git v2.40. It's likely that these pass for a long\n    time already, but nobody ever noticed because Git developers do not\n    tend to have CVS on their machines.\n\nMark all of these tests as passing.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t0411-clone-from-partial.sh      | 1 +\n t/t0610-reftable-basics.sh         | 1 +\n t/t2405-worktree-submodule.sh      | 1 +\n t/t7423-submodule-symlinks.sh      | 1 +\n t/t9200-git-cvsexportcommit.sh     | 1 +\n t/t9401-git-cvsserver-crlf.sh      | 1 +\n t/t9600-cvsimport.sh               | 1 +\n t/t9601-cvsimport-vendor-branch.sh | 1 +\n t/t9602-cvsimport-branches-tags.sh | 1 +\n t/t9603-cvsimport-patchsets.sh     | 2 ++\n t/t9604-cvsimport-timestamps.sh    | 2 ++\n 11 files changed, 13 insertions(+)\n\ndiff --git a/t/t0411-clone-from-partial.sh b/t/t0411-clone-from-partial.sh\nindex c98d501869..932bf2067d 100755\n--- a/t/t0411-clone-from-partial.sh\n+++ b/t/t0411-clone-from-partial.sh\n@@ -2,6 +2,7 @@\n \n test_description='check that local clone does not fetch from promisor remotes'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n test_expect_success 'create evil repo' '\ndiff --git a/t/t0610-reftable-basics.sh b/t/t0610-reftable-basics.sh\nindex cc5bbfd732..b06c46999d 100755\n--- a/t/t0610-reftable-basics.sh\n+++ b/t/t0610-reftable-basics.sh\n@@ -10,6 +10,7 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n GIT_TEST_DEFAULT_REF_FORMAT=reftable\n export GIT_TEST_DEFAULT_REF_FORMAT\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n INVALID_OID=$(test_oid 001)\ndiff --git a/t/t2405-worktree-submodule.sh b/t/t2405-worktree-submodule.sh\nindex 11018f37c7..1d7f605633 100755\n--- a/t/t2405-worktree-submodule.sh\n+++ b/t/t2405-worktree-submodule.sh\n@@ -5,6 +5,7 @@ test_description='Combination of submodules and multiple worktrees'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n base_path=$(pwd -P)\ndiff --git a/t/t7423-submodule-symlinks.sh b/t/t7423-submodule-symlinks.sh\nindex 3d3c7af3ce..f45d806201 100755\n--- a/t/t7423-submodule-symlinks.sh\n+++ b/t/t7423-submodule-symlinks.sh\n@@ -2,6 +2,7 @@\n \n test_description='check that submodule operations do not follow symlinks'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n test_expect_success 'prepare' '\ndiff --git a/t/t9200-git-cvsexportcommit.sh b/t/t9200-git-cvsexportcommit.sh\nindex a44eabf0d8..3d4842164c 100755\n--- a/t/t9200-git-cvsexportcommit.sh\n+++ b/t/t9200-git-cvsexportcommit.sh\n@@ -4,6 +4,7 @@\n #\n test_description='Test export of commits to CVS'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n if ! test_have_prereq PERL; then\ndiff --git a/t/t9401-git-cvsserver-crlf.sh b/t/t9401-git-cvsserver-crlf.sh\nindex a34805acdc..a67e6abd49 100755\n--- a/t/t9401-git-cvsserver-crlf.sh\n+++ b/t/t9401-git-cvsserver-crlf.sh\n@@ -12,6 +12,7 @@ repository using cvs CLI client via git-cvsserver server'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n marked_as () {\ndiff --git a/t/t9600-cvsimport.sh b/t/t9600-cvsimport.sh\nindex 5680849218..41fcf3606b 100755\n--- a/t/t9600-cvsimport.sh\n+++ b/t/t9600-cvsimport.sh\n@@ -4,6 +4,7 @@ test_description='git cvsimport basic tests'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./lib-cvs.sh\n \n if ! test_have_prereq NOT_ROOT; then\ndiff --git a/t/t9601-cvsimport-vendor-branch.sh b/t/t9601-cvsimport-vendor-branch.sh\nindex 116cddba3a..e007669495 100755\n--- a/t/t9601-cvsimport-vendor-branch.sh\n+++ b/t/t9601-cvsimport-vendor-branch.sh\n@@ -35,6 +35,7 @@ test_description='git cvsimport handling of vendor branches'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./lib-cvs.sh\n \n setup_cvs_test_repository t9601\ndiff --git a/t/t9602-cvsimport-branches-tags.sh b/t/t9602-cvsimport-branches-tags.sh\nindex e5266c9a87..3768e3bd8c 100755\n--- a/t/t9602-cvsimport-branches-tags.sh\n+++ b/t/t9602-cvsimport-branches-tags.sh\n@@ -7,6 +7,7 @@ test_description='git cvsimport handling of branches and tags'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./lib-cvs.sh\n \n setup_cvs_test_repository t9602\ndiff --git a/t/t9603-cvsimport-patchsets.sh b/t/t9603-cvsimport-patchsets.sh\nindex 19f38f78f2..2a387fdbaa 100755\n--- a/t/t9603-cvsimport-patchsets.sh\n+++ b/t/t9603-cvsimport-patchsets.sh\n@@ -12,6 +12,8 @@\n # bug.\n \n test_description='git cvsimport testing for correct patchset estimation'\n+\n+TEST_PASSES_SANITIZE_LEAK=true\n . ./lib-cvs.sh\n \n setup_cvs_test_repository t9603\ndiff --git a/t/t9604-cvsimport-timestamps.sh b/t/t9604-cvsimport-timestamps.sh\nindex 2d03259729..9cf0685d56 100755\n--- a/t/t9604-cvsimport-timestamps.sh\n+++ b/t/t9604-cvsimport-timestamps.sh\n@@ -1,6 +1,8 @@\n #!/bin/sh\n \n test_description='git cvsimport timestamps'\n+\n+TEST_PASSES_SANITIZE_LEAK=true\n . ./lib-cvs.sh\n \n test_lazy_prereq POSIX_TIMEZONE '\n-- \n2.45.1.246.gb9cfe4845c.dirty\n\n"},{"id":"495673","messageId":"9e946c1a835bbdc23fbc6773b714eb2175ae33f8.1716810168.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716810168.git.ps@pks.im","subject":"[PATCH v3 03/21] transport-helper: fix leaking helper name","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-27T11:45:56Z","receivedAt":"2024-05-27T11:46:01Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When initializing the transport helper in `transport_get()`, we\nallocate the name of the helper. We neither end up transferring\nownership of the name, nor do we free it. The associated memory thus\nleaks.\n\nFix this memory leak by freeing the string at the calling side in\n`transport_get()`. `transport_helper_init()` now creates its own copy of\nthe string and thus can free it as required.\n\nAn alterantive way to fix this would be to transfer ownership of the\nstring passed into `transport_helper_init()`, which would avoid the call\nto xstrdup(1). But it does make for a more surprising calling convention\nas we do not typically transfer ownership of strings like this.\n\nMark now-passing tests as leak free.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t0611-reftable-httpd.sh    | 1 +\n t/t5563-simple-http-auth.sh  | 1 +\n t/t5564-http-proxy.sh        | 1 +\n t/t5581-http-curl-verbose.sh | 1 +\n transport-helper.c           | 6 ++++--\n transport.c                  | 1 +\n 6 files changed, 9 insertions(+), 2 deletions(-)\n\ndiff --git a/t/t0611-reftable-httpd.sh b/t/t0611-reftable-httpd.sh\nindex 5e05b9c1f2..2805995cc8 100755\n--- a/t/t0611-reftable-httpd.sh\n+++ b/t/t0611-reftable-httpd.sh\n@@ -2,6 +2,7 @@\n \n test_description='reftable HTTPD tests'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-httpd.sh\n \ndiff --git a/t/t5563-simple-http-auth.sh b/t/t5563-simple-http-auth.sh\nindex 5d5caa3f58..4af796de67 100755\n--- a/t/t5563-simple-http-auth.sh\n+++ b/t/t5563-simple-http-auth.sh\n@@ -2,6 +2,7 @@\n \n test_description='test http auth header and credential helper interop'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-httpd.sh\n \ndiff --git a/t/t5564-http-proxy.sh b/t/t5564-http-proxy.sh\nindex 9da5134614..bb35b87071 100755\n--- a/t/t5564-http-proxy.sh\n+++ b/t/t5564-http-proxy.sh\n@@ -2,6 +2,7 @@\n \n test_description=\"test fetching through http proxy\"\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-httpd.sh\n \ndiff --git a/t/t5581-http-curl-verbose.sh b/t/t5581-http-curl-verbose.sh\nindex cded79c16b..724f610054 100755\n--- a/t/t5581-http-curl-verbose.sh\n+++ b/t/t5581-http-curl-verbose.sh\n@@ -4,6 +4,7 @@ test_description='test GIT_CURL_VERBOSE'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-httpd.sh\n start_httpd\ndiff --git a/transport-helper.c b/transport-helper.c\nindex 780fcaf529..9820947ab2 100644\n--- a/transport-helper.c\n+++ b/transport-helper.c\n@@ -22,7 +22,7 @@\n static int debug;\n \n struct helper_data {\n-\tconst char *name;\n+\tchar *name;\n \tstruct child_process *helper;\n \tFILE *out;\n \tunsigned fetch : 1,\n@@ -111,6 +111,7 @@ static void do_take_over(struct transport *transport)\n \tdata = (struct helper_data *)transport->data;\n \ttransport_take_over(transport, data->helper);\n \tfclose(data->out);\n+\tfree(data->name);\n \tfree(data);\n }\n \n@@ -253,6 +254,7 @@ static int disconnect_helper(struct transport *transport)\n \t\tclose(data->helper->out);\n \t\tfclose(data->out);\n \t\tres = finish_command(data->helper);\n+\t\tFREE_AND_NULL(data->name);\n \t\tFREE_AND_NULL(data->helper);\n \t}\n \treturn res;\n@@ -1297,7 +1299,7 @@ static struct transport_vtable vtable = {\n int transport_helper_init(struct transport *transport, const char *name)\n {\n \tstruct helper_data *data = xcalloc(1, sizeof(*data));\n-\tdata->name = name;\n+\tdata->name = xstrdup(name);\n \n \ttransport_check_allowed(name);\n \ndiff --git a/transport.c b/transport.c\nindex 0ad04b77fd..83ddea8fbc 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -1176,6 +1176,7 @@ struct transport *transport_get(struct remote *remote, const char *url)\n \t\tint len = external_specification_len(url);\n \t\tchar *handler = xmemdupz(url, len);\n \t\ttransport_helper_init(ret, handler);\n+\t\tfree(handler);\n \t}\n \n \tif (ret->smart_options) {\n-- \n2.45.1.246.gb9cfe4845c.dirty\n\n"},{"id":"495674","messageId":"aa5cbd9d1404065e40474c2b55c0a8da9df308c8.1716810168.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716810168.git.ps@pks.im","subject":"[PATCH v3 04/21] strbuf: fix leak when `appendwholeline()` fails with EOF","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-27T11:46:01Z","receivedAt":"2024-05-27T11:46:06Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"In `strbuf_appendwholeline()` we call `strbuf_getwholeline()` with a\ntemporary buffer. In case the call returns an error we indicate this by\nreturning EOF, but never release the temporary buffer. This can cause a\nleak though because `strbuf_getwholeline()` calls getline(3). Quoting\nits documentation:\n\n    If *lineptr was set to NULL before the call, then the buffer\n    should be freed by the user program even on failure.\n\nConsequently, the temporary buffer may hold allocated memory even when\nthe call to `strbuf_getwholeline()` fails.\n\nFix this by releasing the temporary buffer on error.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n strbuf.c              | 4 +++-\n t/t1400-update-ref.sh | 2 ++\n 2 files changed, 5 insertions(+), 1 deletion(-)\n\ndiff --git a/strbuf.c b/strbuf.c\nindex 0d929e4e19..e1076c9891 100644\n--- a/strbuf.c\n+++ b/strbuf.c\n@@ -691,8 +691,10 @@ int strbuf_getwholeline(struct strbuf *sb, FILE *fp, int term)\n int strbuf_appendwholeline(struct strbuf *sb, FILE *fp, int term)\n {\n \tstruct strbuf line = STRBUF_INIT;\n-\tif (strbuf_getwholeline(&line, fp, term))\n+\tif (strbuf_getwholeline(&line, fp, term)) {\n+\t\tstrbuf_release(&line);\n \t\treturn EOF;\n+\t}\n \tstrbuf_addbuf(sb, &line);\n \tstrbuf_release(&line);\n \treturn 0;\ndiff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh\nindex ec3443cc87..bbee2783ab 100755\n--- a/t/t1400-update-ref.sh\n+++ b/t/t1400-update-ref.sh\n@@ -4,6 +4,8 @@\n #\n \n test_description='Test git update-ref and basic ref logging'\n+\n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n Z=$ZERO_OID\n-- \n2.45.1.246.gb9cfe4845c.dirty\n\n"},{"id":"495675","messageId":"4a6dd9b6a85d99fb0e3ba2ef3af4c0f2846560cb.1716810168.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716810168.git.ps@pks.im","subject":"[PATCH v3 05/21] checkout: clarify memory ownership in `unique_tracking_name()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-27T11:46:06Z","receivedAt":"2024-05-27T11:46:10Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The function `unique_tracking_name()` returns an allocated string, but\ndoes not clearly indicate this because its return type is `const char *`\ninstead of `char *`. This has led to various callsites where we never\nfree its returned memory at all, which causes memory leaks.\n\nPlug those leaks and mark now-passing tests as leak free.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/checkout.c                            | 14 +++++++------\n builtin/worktree.c                            | 20 ++++++++++---------\n checkout.c                                    |  4 ++--\n checkout.h                                    |  6 +++---\n t/t2024-checkout-dwim.sh                      |  1 +\n t/t2060-switch.sh                             |  1 +\n t/t3426-rebase-submodule.sh                   |  1 +\n t/t3512-cherry-pick-submodule.sh              |  1 +\n t/t3513-revert-submodule.sh                   |  1 +\n t/t3600-rm.sh                                 |  1 +\n t/t3906-stash-submodule.sh                    |  1 +\n t/t4137-apply-submodule.sh                    |  1 +\n t/t6041-bisect-submodule.sh                   |  1 +\n t/t6438-submodule-directory-file-conflicts.sh |  1 +\n 14 files changed, 34 insertions(+), 20 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex f90a4ca4b7..3cf44b4683 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -1275,12 +1275,12 @@ static void setup_new_branch_info_and_source_tree(\n \t}\n }\n \n-static const char *parse_remote_branch(const char *arg,\n-\t\t\t\t       struct object_id *rev,\n-\t\t\t\t       int could_be_checkout_paths)\n+static char *parse_remote_branch(const char *arg,\n+\t\t\t\t struct object_id *rev,\n+\t\t\t\t int could_be_checkout_paths)\n {\n \tint num_matches = 0;\n-\tconst char *remote = unique_tracking_name(arg, rev, &num_matches);\n+\tchar *remote = unique_tracking_name(arg, rev, &num_matches);\n \n \tif (remote && could_be_checkout_paths) {\n \t\tdie(_(\"'%s' could be both a local file and a tracking branch.\\n\"\n@@ -1316,6 +1316,7 @@ static int parse_branchname_arg(int argc, const char **argv,\n \tconst char **new_branch = &opts->new_branch;\n \tint argcount = 0;\n \tconst char *arg;\n+\tchar *remote = NULL;\n \tint dash_dash_pos;\n \tint has_dash_dash = 0;\n \tint i;\n@@ -1416,8 +1417,8 @@ static int parse_branchname_arg(int argc, const char **argv,\n \t\t\trecover_with_dwim = 0;\n \n \t\tif (recover_with_dwim) {\n-\t\t\tconst char *remote = parse_remote_branch(arg, rev,\n-\t\t\t\t\t\t\t\t could_be_checkout_paths);\n+\t\t\tremote = parse_remote_branch(arg, rev,\n+\t\t\t\t\t\t     could_be_checkout_paths);\n \t\t\tif (remote) {\n \t\t\t\t*new_branch = arg;\n \t\t\t\targ = remote;\n@@ -1459,6 +1460,7 @@ static int parse_branchname_arg(int argc, const char **argv,\n \t\targc--;\n \t}\n \n+\tfree(remote);\n \treturn argcount;\n }\n \ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 7e0868df72..937da6c0ee 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -736,16 +736,14 @@ static int dwim_orphan(const struct add_opts *opts, int opt_track, int remote)\n \treturn 1;\n }\n \n-static const char *dwim_branch(const char *path, const char **new_branch)\n+static char *dwim_branch(const char *path, char **new_branch)\n {\n \tint n;\n \tint branch_exists;\n \tconst char *s = worktree_basename(path, &n);\n-\tconst char *branchname = xstrndup(s, n);\n+\tchar *branchname = xstrndup(s, n);\n \tstruct strbuf ref = STRBUF_INIT;\n \n-\tUNLEAK(branchname);\n-\n \tbranch_exists = !strbuf_check_branch_ref(&ref, branchname) &&\n \t\t\trefs_ref_exists(get_main_ref_store(the_repository),\n \t\t\t\t\tref.buf);\n@@ -756,8 +754,7 @@ static const char *dwim_branch(const char *path, const char **new_branch)\n \t*new_branch = branchname;\n \tif (guess_remote) {\n \t\tstruct object_id oid;\n-\t\tconst char *remote =\n-\t\t\tunique_tracking_name(*new_branch, &oid, NULL);\n+\t\tchar *remote = unique_tracking_name(*new_branch, &oid, NULL);\n \t\treturn remote;\n \t}\n \treturn NULL;\n@@ -769,6 +766,8 @@ static int add(int ac, const char **av, const char *prefix)\n \tconst char *new_branch_force = NULL;\n \tchar *path;\n \tconst char *branch;\n+\tchar *branch_to_free = NULL;\n+\tchar *new_branch_to_free = NULL;\n \tconst char *new_branch = NULL;\n \tconst char *opt_track = NULL;\n \tconst char *lock_reason = NULL;\n@@ -859,16 +858,17 @@ static int add(int ac, const char **av, const char *prefix)\n \t\topts.orphan = dwim_orphan(&opts, !!opt_track, 0);\n \t} else if (ac < 2) {\n \t\t/* DWIM: Guess branch name from path. */\n-\t\tconst char *s = dwim_branch(path, &new_branch);\n+\t\tchar *s = dwim_branch(path, &new_branch_to_free);\n \t\tif (s)\n-\t\t\tbranch = s;\n+\t\t\tbranch = branch_to_free = s;\n+\t\tnew_branch = new_branch_to_free;\n \n \t\t/* DWIM: Infer --orphan when repo has no refs. */\n \t\topts.orphan = (!s) && dwim_orphan(&opts, !!opt_track, 1);\n \t} else if (ac == 2) {\n \t\tstruct object_id oid;\n \t\tstruct commit *commit;\n-\t\tconst char *remote;\n+\t\tchar *remote;\n \n \t\tcommit = lookup_commit_reference_by_name(branch);\n \t\tif (!commit) {\n@@ -923,6 +923,8 @@ static int add(int ac, const char **av, const char *prefix)\n \n \tret = add_worktree(path, branch, &opts);\n \tfree(path);\n+\tfree(branch_to_free);\n+\tfree(new_branch_to_free);\n \treturn ret;\n }\n \ndiff --git a/checkout.c b/checkout.c\nindex 4256e71a7c..cfaea4bd10 100644\n--- a/checkout.c\n+++ b/checkout.c\n@@ -45,8 +45,8 @@ static int check_tracking_name(struct remote *remote, void *cb_data)\n \treturn 0;\n }\n \n-const char *unique_tracking_name(const char *name, struct object_id *oid,\n-\t\t\t\t int *dwim_remotes_matched)\n+char *unique_tracking_name(const char *name, struct object_id *oid,\n+\t\t\t   int *dwim_remotes_matched)\n {\n \tstruct tracking_name_data cb_data = TRACKING_NAME_DATA_INIT;\n \tconst char *default_remote = NULL;\ndiff --git a/checkout.h b/checkout.h\nindex 3c514a5ab4..ba15a13fb3 100644\n--- a/checkout.h\n+++ b/checkout.h\n@@ -8,8 +8,8 @@\n  * tracking branch.  Return the name of the remote if such a branch\n  * exists, NULL otherwise.\n  */\n-const char *unique_tracking_name(const char *name,\n-\t\t\t\t struct object_id *oid,\n-\t\t\t\t int *dwim_remotes_matched);\n+char *unique_tracking_name(const char *name,\n+\t\t\t   struct object_id *oid,\n+\t\t\t   int *dwim_remotes_matched);\n \n #endif /* CHECKOUT_H */\ndiff --git a/t/t2024-checkout-dwim.sh b/t/t2024-checkout-dwim.sh\nindex a3b1449ef1..2caada3d83 100755\n--- a/t/t2024-checkout-dwim.sh\n+++ b/t/t2024-checkout-dwim.sh\n@@ -4,6 +4,7 @@ test_description='checkout <branch>\n \n Ensures that checkout on an unborn branch does what the user expects'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n # Is the current branch \"refs/heads/$1\"?\ndiff --git a/t/t2060-switch.sh b/t/t2060-switch.sh\nindex c91c4db936..77b2346291 100755\n--- a/t/t2060-switch.sh\n+++ b/t/t2060-switch.sh\n@@ -5,6 +5,7 @@ test_description='switch basic functionality'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n test_expect_success 'setup' '\ndiff --git a/t/t3426-rebase-submodule.sh b/t/t3426-rebase-submodule.sh\nindex ba069dccbd..94ea88e384 100755\n--- a/t/t3426-rebase-submodule.sh\n+++ b/t/t3426-rebase-submodule.sh\n@@ -2,6 +2,7 @@\n \n test_description='rebase can handle submodules'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-submodule-update.sh\n . \"$TEST_DIRECTORY\"/lib-rebase.sh\ndiff --git a/t/t3512-cherry-pick-submodule.sh b/t/t3512-cherry-pick-submodule.sh\nindex f22d1ddead..9387a22a9e 100755\n--- a/t/t3512-cherry-pick-submodule.sh\n+++ b/t/t3512-cherry-pick-submodule.sh\n@@ -5,6 +5,7 @@ test_description='cherry-pick can handle submodules'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-submodule-update.sh\n \ndiff --git a/t/t3513-revert-submodule.sh b/t/t3513-revert-submodule.sh\nindex 8bfe3ed246..e178968b40 100755\n--- a/t/t3513-revert-submodule.sh\n+++ b/t/t3513-revert-submodule.sh\n@@ -2,6 +2,7 @@\n \n test_description='revert can handle submodules'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-submodule-update.sh\n \ndiff --git a/t/t3600-rm.sh b/t/t3600-rm.sh\nindex 98259e2ada..31ac31d4bc 100755\n--- a/t/t3600-rm.sh\n+++ b/t/t3600-rm.sh\n@@ -8,6 +8,7 @@ test_description='Test of the various options to git rm.'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n # Setup some files to be removed, some with funny characters\ndiff --git a/t/t3906-stash-submodule.sh b/t/t3906-stash-submodule.sh\nindex 0f7348ec21..0f61f01ef4 100755\n--- a/t/t3906-stash-submodule.sh\n+++ b/t/t3906-stash-submodule.sh\n@@ -2,6 +2,7 @@\n \n test_description='stash can handle submodules'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-submodule-update.sh\n \ndiff --git a/t/t4137-apply-submodule.sh b/t/t4137-apply-submodule.sh\nindex 07d5262537..ebd0d4ad17 100755\n--- a/t/t4137-apply-submodule.sh\n+++ b/t/t4137-apply-submodule.sh\n@@ -2,6 +2,7 @@\n \n test_description='git apply handling submodules'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-submodule-update.sh\n \ndiff --git a/t/t6041-bisect-submodule.sh b/t/t6041-bisect-submodule.sh\nindex 82013fc903..3946e18089 100755\n--- a/t/t6041-bisect-submodule.sh\n+++ b/t/t6041-bisect-submodule.sh\n@@ -2,6 +2,7 @@\n \n test_description='bisect can handle submodules'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-submodule-update.sh\n \ndiff --git a/t/t6438-submodule-directory-file-conflicts.sh b/t/t6438-submodule-directory-file-conflicts.sh\nindex 8df67a0ef9..3594190af8 100755\n--- a/t/t6438-submodule-directory-file-conflicts.sh\n+++ b/t/t6438-submodule-directory-file-conflicts.sh\n@@ -2,6 +2,7 @@\n \n test_description='merge can handle submodules'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-submodule-update.sh\n \n-- \n2.45.1.246.gb9cfe4845c.dirty\n\n"},{"id":"495676","messageId":"fa91a3942edd2c26389a25bb2a5c1737151e7c12.1716810168.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716810168.git.ps@pks.im","subject":"[PATCH v3 06/21] http: refactor code to clarify memory ownership","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-27T11:46:10Z","receivedAt":"2024-05-27T11:46:15Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"There are various variables assigned via `git_config_string()` and\n`git_config_pathname()` which are never free'd. This bug is relatable\nbecause the out parameter of those functions are a `const char **`, even\nthough memory ownership is transferred to the caller.\n\nWe're about to adapt the functions to instead use `char **`. Prepare the\ncode accordingly. Note that the `(const char **)` casts will go away\nonce we have adapted the functions.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n http.c | 62 ++++++++++++++++++++++++++++++----------------------------\n 1 file changed, 32 insertions(+), 30 deletions(-)\n\ndiff --git a/http.c b/http.c\nindex 752c879c1f..db2e2f1d39 100644\n--- a/http.c\n+++ b/http.c\n@@ -39,8 +39,8 @@ char curl_errorstr[CURL_ERROR_SIZE];\n static int curl_ssl_verify = -1;\n static int curl_ssl_try;\n static const char *curl_http_version = NULL;\n-static const char *ssl_cert;\n-static const char *ssl_cert_type;\n+static char *ssl_cert;\n+static char *ssl_cert_type;\n static const char *ssl_cipherlist;\n static const char *ssl_version;\n static struct {\n@@ -59,23 +59,23 @@ static struct {\n \t{ \"tlsv1.3\", CURL_SSLVERSION_TLSv1_3 },\n #endif\n };\n-static const char *ssl_key;\n-static const char *ssl_key_type;\n-static const char *ssl_capath;\n-static const char *curl_no_proxy;\n+static char *ssl_key;\n+static char *ssl_key_type;\n+static char *ssl_capath;\n+static char *curl_no_proxy;\n #ifdef GIT_CURL_HAVE_CURLOPT_PINNEDPUBLICKEY\n static const char *ssl_pinnedkey;\n #endif\n-static const char *ssl_cainfo;\n+static char *ssl_cainfo;\n static long curl_low_speed_limit = -1;\n static long curl_low_speed_time = -1;\n static int curl_ftp_no_epsv;\n-static const char *curl_http_proxy;\n-static const char *http_proxy_authmethod;\n+static char *curl_http_proxy;\n+static char *http_proxy_authmethod;\n \n-static const char *http_proxy_ssl_cert;\n-static const char *http_proxy_ssl_key;\n-static const char *http_proxy_ssl_ca_info;\n+static char *http_proxy_ssl_cert;\n+static char *http_proxy_ssl_key;\n+static char *http_proxy_ssl_ca_info;\n static struct credential proxy_cert_auth = CREDENTIAL_INIT;\n static int proxy_ssl_cert_password_required;\n \n@@ -112,7 +112,7 @@ static const char *curl_cookie_file;\n static int curl_save_cookies;\n struct credential http_auth = CREDENTIAL_INIT;\n static int http_proactive_auth;\n-static const char *user_agent;\n+static char *user_agent;\n static int curl_empty_auth = -1;\n \n enum http_follow_config http_follow_config = HTTP_FOLLOW_INITIAL;\n@@ -381,17 +381,17 @@ static int http_options(const char *var, const char *value,\n \tif (!strcmp(\"http.sslversion\", var))\n \t\treturn git_config_string(&ssl_version, var, value);\n \tif (!strcmp(\"http.sslcert\", var))\n-\t\treturn git_config_pathname(&ssl_cert, var, value);\n+\t\treturn git_config_pathname((const char **)&ssl_cert, var, value);\n \tif (!strcmp(\"http.sslcerttype\", var))\n-\t\treturn git_config_string(&ssl_cert_type, var, value);\n+\t\treturn git_config_string((const char **)&ssl_cert_type, var, value);\n \tif (!strcmp(\"http.sslkey\", var))\n-\t\treturn git_config_pathname(&ssl_key, var, value);\n+\t\treturn git_config_pathname((const char **)&ssl_key, var, value);\n \tif (!strcmp(\"http.sslkeytype\", var))\n-\t\treturn git_config_string(&ssl_key_type, var, value);\n+\t\treturn git_config_string((const char **)&ssl_key_type, var, value);\n \tif (!strcmp(\"http.sslcapath\", var))\n-\t\treturn git_config_pathname(&ssl_capath, var, value);\n+\t\treturn git_config_pathname((const char **)&ssl_capath, var, value);\n \tif (!strcmp(\"http.sslcainfo\", var))\n-\t\treturn git_config_pathname(&ssl_cainfo, var, value);\n+\t\treturn git_config_pathname((const char **)&ssl_cainfo, var, value);\n \tif (!strcmp(\"http.sslcertpasswordprotected\", var)) {\n \t\tssl_cert_password_required = git_config_bool(var, value);\n \t\treturn 0;\n@@ -440,19 +440,19 @@ static int http_options(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \tif (!strcmp(\"http.proxy\", var))\n-\t\treturn git_config_string(&curl_http_proxy, var, value);\n+\t\treturn git_config_string((const char **)&curl_http_proxy, var, value);\n \n \tif (!strcmp(\"http.proxyauthmethod\", var))\n-\t\treturn git_config_string(&http_proxy_authmethod, var, value);\n+\t\treturn git_config_string((const char **)&http_proxy_authmethod, var, value);\n \n \tif (!strcmp(\"http.proxysslcert\", var))\n-\t\treturn git_config_string(&http_proxy_ssl_cert, var, value);\n+\t\treturn git_config_string((const char **)&http_proxy_ssl_cert, var, value);\n \n \tif (!strcmp(\"http.proxysslkey\", var))\n-\t\treturn git_config_string(&http_proxy_ssl_key, var, value);\n+\t\treturn git_config_string((const char **)&http_proxy_ssl_key, var, value);\n \n \tif (!strcmp(\"http.proxysslcainfo\", var))\n-\t\treturn git_config_string(&http_proxy_ssl_ca_info, var, value);\n+\t\treturn git_config_string((const char **)&http_proxy_ssl_ca_info, var, value);\n \n \tif (!strcmp(\"http.proxysslcertpasswordprotected\", var)) {\n \t\tproxy_ssl_cert_password_required = git_config_bool(var, value);\n@@ -476,7 +476,7 @@ static int http_options(const char *var, const char *value,\n \t}\n \n \tif (!strcmp(\"http.useragent\", var))\n-\t\treturn git_config_string(&user_agent, var, value);\n+\t\treturn git_config_string((const char **)&user_agent, var, value);\n \n \tif (!strcmp(\"http.emptyauth\", var)) {\n \t\tif (value && !strcmp(\"auto\", value))\n@@ -592,10 +592,10 @@ static void init_curl_http_auth(CURL *result)\n }\n \n /* *var must be free-able */\n-static void var_override(const char **var, char *value)\n+static void var_override(char **var, char *value)\n {\n \tif (value) {\n-\t\tfree((void *)*var);\n+\t\tfree(*var);\n \t\t*var = xstrdup(value);\n \t}\n }\n@@ -1233,11 +1233,13 @@ static CURL *get_curl_handle(void)\n \treturn result;\n }\n \n-static void set_from_env(const char **var, const char *envname)\n+static void set_from_env(char **var, const char *envname)\n {\n \tconst char *val = getenv(envname);\n-\tif (val)\n-\t\t*var = val;\n+\tif (val) {\n+\t\tFREE_AND_NULL(*var);\n+\t\t*var = xstrdup(val);\n+\t}\n }\n \n void http_init(struct remote *remote, const char *url, int proactive_auth)\n-- \n2.45.1.246.gb9cfe4845c.dirty\n\n"},{"id":"495677","messageId":"88babf1abf93dd589fb1bc311483f4c6852b125b.1716810168.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716810168.git.ps@pks.im","subject":"[PATCH v3 07/21] config: clarify memory ownership in `git_config_pathname()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-27T11:46:15Z","receivedAt":"2024-05-27T11:46:20Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The out parameter of `git_config_pathname()` is a `const char **` even\nthough we transfer ownership of memory to the caller. This is quite\nmisleading and has led to many memory leaks all over the place. Adapt\nthe parameter to instead be `char **`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/blame.c        |  2 +-\n builtin/commit.c       |  2 +-\n builtin/config.c       |  2 +-\n builtin/log.c          |  2 +-\n builtin/receive-pack.c |  4 ++--\n config.c               | 10 +++++-----\n config.h               |  8 ++++----\n diff.c                 |  2 +-\n environment.c          |  6 +++---\n environment.h          |  6 +++---\n fetch-pack.c           |  4 ++--\n fsck.c                 |  4 ++--\n fsmonitor-settings.c   |  5 ++++-\n gpg-interface.c        |  4 +++-\n http.c                 | 12 ++++++------\n mailmap.c              |  2 +-\n mailmap.h              |  2 +-\n setup.c                |  6 +++---\n 18 files changed, 44 insertions(+), 39 deletions(-)\n\ndiff --git a/builtin/blame.c b/builtin/blame.c\nindex 6bc7aa6085..838cd476be 100644\n--- a/builtin/blame.c\n+++ b/builtin/blame.c\n@@ -718,7 +718,7 @@ static int git_blame_config(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"blame.ignorerevsfile\")) {\n-\t\tconst char *str;\n+\t\tchar *str;\n \t\tint ret;\n \n \t\tret = git_config_pathname(&str, var, value);\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 78bfae2164..1cc88e92bf 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -107,7 +107,7 @@ static enum {\n } commit_style;\n \n static const char *logfile, *force_author;\n-static const char *template_file;\n+static char *template_file;\n /*\n  * The _message variables are commit names from which to take\n  * the commit message and/or authorship.\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 80aa9d8a66..cc343f55ca 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -277,7 +277,7 @@ static int format_config(struct strbuf *buf, const char *key_,\n \t\t\telse\n \t\t\t\tstrbuf_addstr(buf, v ? \"true\" : \"false\");\n \t\t} else if (type == TYPE_PATH) {\n-\t\t\tconst char *v;\n+\t\t\tchar *v;\n \t\t\tif (git_config_pathname(&v, key_, value_) < 0)\n \t\t\t\treturn -1;\n \t\t\tstrbuf_addstr(buf, v);\ndiff --git a/builtin/log.c b/builtin/log.c\nindex b17dd8b40a..a2f5845556 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -957,7 +957,7 @@ static int do_signoff;\n static enum auto_base_setting auto_base;\n static char *from;\n static const char *signature = git_version_string;\n-static const char *signature_file;\n+static char *signature_file;\n static enum cover_setting config_cover_letter;\n static const char *config_output_directory;\n static enum cover_from_description cover_from_description_mode = COVER_FROM_MESSAGE;\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex be8969a84a..56228ad314 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -168,13 +168,13 @@ static int receive_pack_config(const char *var, const char *value,\n \t}\n \n \tif (strcmp(var, \"receive.fsck.skiplist\") == 0) {\n-\t\tconst char *path;\n+\t\tchar *path;\n \n \t\tif (git_config_pathname(&path, var, value))\n \t\t\treturn 1;\n \t\tstrbuf_addf(&fsck_msg_types, \"%cskiplist=%s\",\n \t\t\tfsck_msg_types.len ? ',' : '=', path);\n-\t\tfree((char *)path);\n+\t\tfree(path);\n \t\treturn 0;\n \t}\n \ndiff --git a/config.c b/config.c\nindex d57996240b..fb56e11276 100644\n--- a/config.c\n+++ b/config.c\n@@ -1346,7 +1346,7 @@ int git_config_string(const char **dest, const char *var, const char *value)\n \treturn 0;\n }\n \n-int git_config_pathname(const char **dest, const char *var, const char *value)\n+int git_config_pathname(char **dest, const char *var, const char *value)\n {\n \tif (!value)\n \t\treturn config_error_nonbool(var);\n@@ -1597,7 +1597,7 @@ static int git_default_core_config(const char *var, const char *value,\n \t\treturn git_config_string(&askpass_program, var, value);\n \n \tif (!strcmp(var, \"core.excludesfile\")) {\n-\t\tfree((char *)excludes_file);\n+\t\tfree(excludes_file);\n \t\treturn git_config_pathname(&excludes_file, var, value);\n \t}\n \n@@ -2494,7 +2494,7 @@ int git_configset_get_maybe_bool(struct config_set *set, const char *key, int *d\n \t\treturn 1;\n }\n \n-int git_configset_get_pathname(struct config_set *set, const char *key, const char **dest)\n+int git_configset_get_pathname(struct config_set *set, const char *key, char **dest)\n {\n \tconst char *value;\n \tif (!git_configset_get_value(set, key, &value, NULL))\n@@ -2639,7 +2639,7 @@ int repo_config_get_maybe_bool(struct repository *repo,\n }\n \n int repo_config_get_pathname(struct repository *repo,\n-\t\t\t     const char *key, const char **dest)\n+\t\t\t     const char *key, char **dest)\n {\n \tint ret;\n \tgit_config_check_init(repo);\n@@ -2738,7 +2738,7 @@ int git_config_get_maybe_bool(const char *key, int *dest)\n \treturn repo_config_get_maybe_bool(the_repository, key, dest);\n }\n \n-int git_config_get_pathname(const char *key, const char **dest)\n+int git_config_get_pathname(const char *key, char **dest)\n {\n \treturn repo_config_get_pathname(the_repository, key, dest);\n }\ndiff --git a/config.h b/config.h\nindex db8b608064..b3103bba94 100644\n--- a/config.h\n+++ b/config.h\n@@ -286,7 +286,7 @@ int git_config_string(const char **, const char *, const char *);\n  * Similar to `git_config_string`, but expands `~` or `~user` into the\n  * user's home directory when found at the beginning of the path.\n  */\n-int git_config_pathname(const char **, const char *, const char *);\n+int git_config_pathname(char **, const char *, const char *);\n \n int git_config_expiry_date(timestamp_t *, const char *, const char *);\n int git_config_color(char *, const char *, const char *);\n@@ -541,7 +541,7 @@ int git_configset_get_ulong(struct config_set *cs, const char *key, unsigned lon\n int git_configset_get_bool(struct config_set *cs, const char *key, int *dest);\n int git_configset_get_bool_or_int(struct config_set *cs, const char *key, int *is_bool, int *dest);\n int git_configset_get_maybe_bool(struct config_set *cs, const char *key, int *dest);\n-int git_configset_get_pathname(struct config_set *cs, const char *key, const char **dest);\n+int git_configset_get_pathname(struct config_set *cs, const char *key, char **dest);\n \n /* Functions for reading a repository's config */\n struct repository;\n@@ -577,7 +577,7 @@ int repo_config_get_bool_or_int(struct repository *repo,\n int repo_config_get_maybe_bool(struct repository *repo,\n \t\t\t       const char *key, int *dest);\n int repo_config_get_pathname(struct repository *repo,\n-\t\t\t     const char *key, const char **dest);\n+\t\t\t     const char *key, char **dest);\n \n /*\n  * Functions for reading protected config. By definition, protected\n@@ -687,7 +687,7 @@ int git_config_get_maybe_bool(const char *key, int *dest);\n  * Similar to `git_config_get_string`, but expands `~` or `~user` into\n  * the user's home directory when found at the beginning of the path.\n  */\n-int git_config_get_pathname(const char *key, const char **dest);\n+int git_config_get_pathname(const char *key, char **dest);\n \n int git_config_get_index_threads(int *dest);\n int git_config_get_split_index(void);\ndiff --git a/diff.c b/diff.c\nindex ded9ac70df..902df9286a 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -58,7 +58,7 @@ static int diff_context_default = 3;\n static int diff_interhunk_context_default;\n static const char *diff_word_regex_cfg;\n static const char *external_diff_cmd_cfg;\n-static const char *diff_order_file_cfg;\n+static char *diff_order_file_cfg;\n int diff_auto_refresh_index = 1;\n static int diff_mnemonic_prefix;\n static int diff_no_prefix;\ndiff --git a/environment.c b/environment.c\nindex a73ba9c12c..279ea3fd5e 100644\n--- a/environment.c\n+++ b/environment.c\n@@ -46,8 +46,8 @@ const char *git_commit_encoding;\n const char *git_log_output_encoding;\n char *apply_default_whitespace;\n char *apply_default_ignorewhitespace;\n-const char *git_attributes_file;\n-const char *git_hooks_path;\n+char *git_attributes_file;\n+char *git_hooks_path;\n int zlib_compression_level = Z_BEST_SPEED;\n int pack_compression_level = Z_DEFAULT_COMPRESSION;\n int fsync_object_files = -1;\n@@ -60,7 +60,7 @@ size_t delta_base_cache_limit = 96 * 1024 * 1024;\n unsigned long big_file_threshold = 512 * 1024 * 1024;\n const char *editor_program;\n const char *askpass_program;\n-const char *excludes_file;\n+char *excludes_file;\n enum auto_crlf auto_crlf = AUTO_CRLF_FALSE;\n enum eol core_eol = EOL_UNSET;\n int global_conv_flags_eol = CONV_EOL_RNDTRP_WARN;\ndiff --git a/environment.h b/environment.h\nindex 0b2d457f07..be1b88ad6f 100644\n--- a/environment.h\n+++ b/environment.h\n@@ -131,8 +131,8 @@ extern int warn_ambiguous_refs;\n extern int warn_on_object_refname_ambiguity;\n extern char *apply_default_whitespace;\n extern char *apply_default_ignorewhitespace;\n-extern const char *git_attributes_file;\n-extern const char *git_hooks_path;\n+extern char *git_attributes_file;\n+extern char *git_hooks_path;\n extern int zlib_compression_level;\n extern int pack_compression_level;\n extern size_t packed_git_window_size;\n@@ -229,7 +229,7 @@ extern const char *git_log_output_encoding;\n \n extern const char *editor_program;\n extern const char *askpass_program;\n-extern const char *excludes_file;\n+extern char *excludes_file;\n \n /*\n  * Should we print an ellipsis after an abbreviated SHA-1 value\ndiff --git a/fetch-pack.c b/fetch-pack.c\nindex 8e8f3bba32..d80e9c92dd 100644\n--- a/fetch-pack.c\n+++ b/fetch-pack.c\n@@ -1865,13 +1865,13 @@ static int fetch_pack_config_cb(const char *var, const char *value,\n \tconst char *msg_id;\n \n \tif (strcmp(var, \"fetch.fsck.skiplist\") == 0) {\n-\t\tconst char *path;\n+\t\tchar *path ;\n \n \t\tif (git_config_pathname(&path, var, value))\n \t\t\treturn 1;\n \t\tstrbuf_addf(&fsck_msg_types, \"%cskiplist=%s\",\n \t\t\tfsck_msg_types.len ? ',' : '=', path);\n-\t\tfree((char *)path);\n+\t\tfree(path);\n \t\treturn 0;\n \t}\n \ndiff --git a/fsck.c b/fsck.c\nindex 8ef962199f..7dff41413e 100644\n--- a/fsck.c\n+++ b/fsck.c\n@@ -1330,13 +1330,13 @@ int git_fsck_config(const char *var, const char *value,\n \tconst char *msg_id;\n \n \tif (strcmp(var, \"fsck.skiplist\") == 0) {\n-\t\tconst char *path;\n+\t\tchar *path;\n \t\tstruct strbuf sb = STRBUF_INIT;\n \n \t\tif (git_config_pathname(&path, var, value))\n \t\t\treturn 1;\n \t\tstrbuf_addf(&sb, \"skiplist=%s\", path);\n-\t\tfree((char *)path);\n+\t\tfree(path);\n \t\tfsck_set_msg_types(options, sb.buf);\n \t\tstrbuf_release(&sb);\n \t\treturn 0;\ndiff --git a/fsmonitor-settings.c b/fsmonitor-settings.c\nindex a6a9e6bc19..e818583420 100644\n--- a/fsmonitor-settings.c\n+++ b/fsmonitor-settings.c\n@@ -103,6 +103,7 @@ static struct fsmonitor_settings *alloc_settings(void)\n static void lookup_fsmonitor_settings(struct repository *r)\n {\n \tconst char *const_str;\n+\tchar *to_free = NULL;\n \tint bool_value;\n \n \tif (r->settings.fsmonitor)\n@@ -129,8 +130,9 @@ static void lookup_fsmonitor_settings(struct repository *r)\n \t\tbreak;\n \n \tcase -1: /* config value set to an arbitrary string */\n-\t\tif (repo_config_get_pathname(r, \"core.fsmonitor\", &const_str))\n+\t\tif (repo_config_get_pathname(r, \"core.fsmonitor\", &to_free))\n \t\t\treturn; /* should not happen */\n+\t\tconst_str = to_free;\n \t\tbreak;\n \n \tdefault: /* should not happen */\n@@ -141,6 +143,7 @@ static void lookup_fsmonitor_settings(struct repository *r)\n \t\tfsm_settings__set_hook(r, const_str);\n \telse\n \t\tfsm_settings__set_disabled(r);\n+\tfree(to_free);\n }\n \n enum fsmonitor_mode fsm_settings__get_mode(struct repository *r)\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 1ff94266d2..2b50ed0fa0 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -27,7 +27,9 @@ static void gpg_interface_lazy_init(void)\n }\n \n static char *configured_signing_key;\n-static const char *ssh_default_key_command, *ssh_allowed_signers, *ssh_revocation_file;\n+static const char *ssh_default_key_command;\n+static char *ssh_allowed_signers;\n+static char *ssh_revocation_file;\n static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;\n \n struct gpg_format {\ndiff --git a/http.c b/http.c\nindex db2e2f1d39..fa3ea87451 100644\n--- a/http.c\n+++ b/http.c\n@@ -64,7 +64,7 @@ static char *ssl_key_type;\n static char *ssl_capath;\n static char *curl_no_proxy;\n #ifdef GIT_CURL_HAVE_CURLOPT_PINNEDPUBLICKEY\n-static const char *ssl_pinnedkey;\n+static char *ssl_pinnedkey;\n #endif\n static char *ssl_cainfo;\n static long curl_low_speed_limit = -1;\n@@ -108,7 +108,7 @@ static struct {\n \n static struct credential proxy_auth = CREDENTIAL_INIT;\n static const char *curl_proxyuserpwd;\n-static const char *curl_cookie_file;\n+static char *curl_cookie_file;\n static int curl_save_cookies;\n struct credential http_auth = CREDENTIAL_INIT;\n static int http_proactive_auth;\n@@ -381,17 +381,17 @@ static int http_options(const char *var, const char *value,\n \tif (!strcmp(\"http.sslversion\", var))\n \t\treturn git_config_string(&ssl_version, var, value);\n \tif (!strcmp(\"http.sslcert\", var))\n-\t\treturn git_config_pathname((const char **)&ssl_cert, var, value);\n+\t\treturn git_config_pathname(&ssl_cert, var, value);\n \tif (!strcmp(\"http.sslcerttype\", var))\n \t\treturn git_config_string((const char **)&ssl_cert_type, var, value);\n \tif (!strcmp(\"http.sslkey\", var))\n-\t\treturn git_config_pathname((const char **)&ssl_key, var, value);\n+\t\treturn git_config_pathname(&ssl_key, var, value);\n \tif (!strcmp(\"http.sslkeytype\", var))\n \t\treturn git_config_string((const char **)&ssl_key_type, var, value);\n \tif (!strcmp(\"http.sslcapath\", var))\n-\t\treturn git_config_pathname((const char **)&ssl_capath, var, value);\n+\t\treturn git_config_pathname(&ssl_capath, var, value);\n \tif (!strcmp(\"http.sslcainfo\", var))\n-\t\treturn git_config_pathname((const char **)&ssl_cainfo, var, value);\n+\t\treturn git_config_pathname(&ssl_cainfo, var, value);\n \tif (!strcmp(\"http.sslcertpasswordprotected\", var)) {\n \t\tssl_cert_password_required = git_config_bool(var, value);\n \t\treturn 0;\ndiff --git a/mailmap.c b/mailmap.c\nindex 3d6a5e9400..044466b043 100644\n--- a/mailmap.c\n+++ b/mailmap.c\n@@ -6,7 +6,7 @@\n #include \"object-store-ll.h\"\n #include \"setup.h\"\n \n-const char *git_mailmap_file;\n+char *git_mailmap_file;\n const char *git_mailmap_blob;\n \n struct mailmap_info {\ndiff --git a/mailmap.h b/mailmap.h\nindex 0f8fd2c586..429a760945 100644\n--- a/mailmap.h\n+++ b/mailmap.h\n@@ -3,7 +3,7 @@\n \n struct string_list;\n \n-extern const char *git_mailmap_file;\n+extern char *git_mailmap_file;\n extern const char *git_mailmap_blob;\n \n int read_mailmap(struct string_list *map);\ndiff --git a/setup.c b/setup.c\nindex 9247cded6a..59ff3a19eb 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -1177,13 +1177,13 @@ static int safe_directory_cb(const char *key, const char *value,\n \t} else if (!strcmp(value, \"*\")) {\n \t\tdata->is_safe = 1;\n \t} else {\n-\t\tconst char *interpolated = NULL;\n+\t\tchar *interpolated = NULL;\n \n \t\tif (!git_config_pathname(&interpolated, key, value) &&\n \t\t    !fspathcmp(data->path, interpolated ? interpolated : value))\n \t\t\tdata->is_safe = 1;\n \n-\t\tfree((char *)interpolated);\n+\t\tfree(interpolated);\n \t}\n \n \treturn 0;\n@@ -1822,7 +1822,7 @@ static int template_dir_cb(const char *key, const char *value,\n \t\tchar *path = NULL;\n \n \t\tFREE_AND_NULL(data->path);\n-\t\tif (!git_config_pathname((const char **)&path, key, value))\n+\t\tif (!git_config_pathname(&path, key, value))\n \t\t\tdata->path = path ? path : xstrdup(value);\n \t}\n \n-- \n2.45.1.246.gb9cfe4845c.dirty\n\n"},{"id":"495678","messageId":"feec7e971f04b0f8c8da429f9bf58db745b99112.1716810168.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716810168.git.ps@pks.im","subject":"[PATCH v3 08/21] diff: refactor code to clarify memory ownership of prefixes","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-27T11:46:20Z","receivedAt":"2024-05-27T11:46:25Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The source and destination prefixes are tracked in a `const char *`\narray, but may at times contain allocated strings. The result is that\nthose strings may be leaking because we never free them.\n\nRefactor the code to always store allocated strings in those variables,\nfreeing them as required. This requires us to handle the default values\na bit different compared to before. But given that there is only a\nsingle callsite where we use the variables to `struct diff_options` it's\neasy to handle the defaults there.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n diff.c | 18 ++++++++++--------\n 1 file changed, 10 insertions(+), 8 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex 902df9286a..679ef472f4 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -62,8 +62,8 @@ static char *diff_order_file_cfg;\n int diff_auto_refresh_index = 1;\n static int diff_mnemonic_prefix;\n static int diff_no_prefix;\n-static const char *diff_src_prefix = \"a/\";\n-static const char *diff_dst_prefix = \"b/\";\n+static char *diff_src_prefix;\n+static char *diff_dst_prefix;\n static int diff_relative;\n static int diff_stat_name_width;\n static int diff_stat_graph_width;\n@@ -411,10 +411,12 @@ int git_diff_ui_config(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"diff.srcprefix\")) {\n-\t\treturn git_config_string(&diff_src_prefix, var, value);\n+\t\tFREE_AND_NULL(diff_src_prefix);\n+\t\treturn git_config_string((const char **) &diff_src_prefix, var, value);\n \t}\n \tif (!strcmp(var, \"diff.dstprefix\")) {\n-\t\treturn git_config_string(&diff_dst_prefix, var, value);\n+\t\tFREE_AND_NULL(diff_dst_prefix);\n+\t\treturn git_config_string((const char **) &diff_dst_prefix, var, value);\n \t}\n \tif (!strcmp(var, \"diff.relative\")) {\n \t\tdiff_relative = git_config_bool(var, value);\n@@ -3433,8 +3435,8 @@ void diff_set_noprefix(struct diff_options *options)\n \n void diff_set_default_prefix(struct diff_options *options)\n {\n-\toptions->a_prefix = diff_src_prefix;\n-\toptions->b_prefix = diff_dst_prefix;\n+\toptions->a_prefix = diff_src_prefix ? diff_src_prefix : \"a/\";\n+\toptions->b_prefix = diff_dst_prefix ? diff_dst_prefix : \"b/\";\n }\n \n struct userdiff_driver *get_textconv(struct repository *r,\n@@ -5371,8 +5373,8 @@ static int diff_opt_default_prefix(const struct option *opt,\n \n \tBUG_ON_OPT_NEG(unset);\n \tBUG_ON_OPT_ARG(optarg);\n-\tdiff_src_prefix = \"a/\";\n-\tdiff_dst_prefix = \"b/\";\n+\tFREE_AND_NULL(diff_src_prefix);\n+\tFREE_AND_NULL(diff_dst_prefix);\n \tdiff_set_default_prefix(options);\n \treturn 0;\n }\n-- \n2.45.1.246.gb9cfe4845c.dirty\n\n"},{"id":"495679","messageId":"dae00f1b63ebaed67550aba4b7df4bb6139a164e.1716810168.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716810168.git.ps@pks.im","subject":"[PATCH v3 09/21] convert: refactor code to clarify ownership of check_roundtrip_encoding","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-27T11:46:25Z","receivedAt":"2024-05-27T11:46:30Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The `check_roundtrip_encoding` variable is tracked in a `const char *`\neven though it may contain allocated strings at times. The result is\nthat those strings may be leaking because we never free them.\n\nRefactor the code to always store allocated strings in this variable.\nThe default value is handled in `check_roundtrip()` now, which is the\nonly user of the variable.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n config.c      |  6 ++++--\n convert.c     | 24 +++++++++++++-----------\n convert.h     |  2 +-\n environment.c |  2 +-\n 4 files changed, 19 insertions(+), 15 deletions(-)\n\ndiff --git a/config.c b/config.c\nindex fb56e11276..f9101045ee 100644\n--- a/config.c\n+++ b/config.c\n@@ -1564,8 +1564,10 @@ static int git_default_core_config(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \n-\tif (!strcmp(var, \"core.checkroundtripencoding\"))\n-\t\treturn git_config_string(&check_roundtrip_encoding, var, value);\n+\tif (!strcmp(var, \"core.checkroundtripencoding\")) {\n+\t\tFREE_AND_NULL(check_roundtrip_encoding);\n+\t\treturn git_config_string((const char **) &check_roundtrip_encoding, var, value);\n+\t}\n \n \tif (!strcmp(var, \"core.notesref\")) {\n \t\tif (!value)\ndiff --git a/convert.c b/convert.c\nindex 35b25eb3cb..03c3c528f9 100644\n--- a/convert.c\n+++ b/convert.c\n@@ -345,30 +345,32 @@ static int check_roundtrip(const char *enc_name)\n \t * space separated encodings (eg. \"UTF-16, ASCII, CP1125\").\n \t * Search for the given encoding in that string.\n \t */\n-\tconst char *found = strcasestr(check_roundtrip_encoding, enc_name);\n+\tconst char *encoding = check_roundtrip_encoding ?\n+\t\tcheck_roundtrip_encoding : \"SHIFT-JIS\";\n+\tconst char *found = strcasestr(encoding, enc_name);\n \tconst char *next;\n \tint len;\n \tif (!found)\n \t\treturn 0;\n \tnext = found + strlen(enc_name);\n-\tlen = strlen(check_roundtrip_encoding);\n+\tlen = strlen(encoding);\n \treturn (found && (\n \t\t\t/*\n-\t\t\t * check that the found encoding is at the\n-\t\t\t * beginning of check_roundtrip_encoding or\n-\t\t\t * that it is prefixed with a space or comma\n+\t\t\t * Check that the found encoding is at the beginning of\n+\t\t\t * encoding or that it is prefixed with a space or\n+\t\t\t * comma.\n \t\t\t */\n-\t\t\tfound == check_roundtrip_encoding || (\n+\t\t\tfound == encoding || (\n \t\t\t\t(isspace(found[-1]) || found[-1] == ',')\n \t\t\t)\n \t\t) && (\n \t\t\t/*\n-\t\t\t * check that the found encoding is at the\n-\t\t\t * end of check_roundtrip_encoding or\n-\t\t\t * that it is suffixed with a space or comma\n+\t\t\t * Check that the found encoding is at the end of\n+\t\t\t * encoding or that it is suffixed with a space\n+\t\t\t * or comma.\n \t\t\t */\n-\t\t\tnext == check_roundtrip_encoding + len || (\n-\t\t\t\tnext < check_roundtrip_encoding + len &&\n+\t\t\tnext == encoding + len || (\n+\t\t\t\tnext < encoding + len &&\n \t\t\t\t(isspace(next[0]) || next[0] == ',')\n \t\t\t)\n \t\t));\ndiff --git a/convert.h b/convert.h\nindex ab8b4fa68d..d925589444 100644\n--- a/convert.h\n+++ b/convert.h\n@@ -92,7 +92,7 @@ void convert_attrs(struct index_state *istate,\n \t\t   struct conv_attrs *ca, const char *path);\n \n extern enum eol core_eol;\n-extern const char *check_roundtrip_encoding;\n+extern char *check_roundtrip_encoding;\n const char *get_cached_convert_stats_ascii(struct index_state *istate,\n \t\t\t\t\t   const char *path);\n const char *get_wt_convert_stats_ascii(const char *path);\ndiff --git a/environment.c b/environment.c\nindex 279ea3fd5e..ab6956559e 100644\n--- a/environment.c\n+++ b/environment.c\n@@ -64,7 +64,7 @@ char *excludes_file;\n enum auto_crlf auto_crlf = AUTO_CRLF_FALSE;\n enum eol core_eol = EOL_UNSET;\n int global_conv_flags_eol = CONV_EOL_RNDTRP_WARN;\n-const char *check_roundtrip_encoding = \"SHIFT-JIS\";\n+char *check_roundtrip_encoding;\n enum branch_track git_branch_track = BRANCH_TRACK_REMOTE;\n enum rebase_setup_type autorebase = AUTOREBASE_NEVER;\n enum push_default_type push_default = PUSH_DEFAULT_UNSPECIFIED;\n-- \n2.45.1.246.gb9cfe4845c.dirty\n\n"},{"id":"495680","messageId":"02c5be27be7d6d88bc35970ab1dd80eb9f172c26.1716810168.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716810168.git.ps@pks.im","subject":"[PATCH v3 10/21] builtin/log: stop using globals for log config","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-27T11:46:30Z","receivedAt":"2024-05-27T11:46:34Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We're using global variables to store the log configuration. Many of\nthese can be set both via the command line and via the config, and\ndepending on how they are being set, they may contain allocated strings.\nThis leads to hard-to-track memory ownership and memory leaks.\n\nRefactor the code to instead use a `struct log_config` that is being\nallocated on the stack. This allows us to more clearly scope the\nvariables, track memory ownership and ultimately release the memory.\n\nThis also prepares us for a change to `git_config_string()`, which will\nbe adapted to have a `char **` out parameter instead of `const char **`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/log.c | 259 ++++++++++++++++++++++++++++++--------------------\n 1 file changed, 156 insertions(+), 103 deletions(-)\n\ndiff --git a/builtin/log.c b/builtin/log.c\nindex a2f5845556..f5da29ee2a 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -48,22 +48,8 @@\n #define COVER_FROM_AUTO_MAX_SUBJECT_LEN 100\n #define FORMAT_PATCH_NAME_MAX_DEFAULT 64\n \n-/* Set a default date-time format for git log (\"log.date\" config variable) */\n-static const char *default_date_mode = NULL;\n-\n-static int default_abbrev_commit;\n-static int default_show_root = 1;\n-static int default_follow;\n-static int default_show_signature;\n-static int default_encode_email_headers = 1;\n-static int decoration_style;\n-static int decoration_given;\n-static int use_mailmap_config = 1;\n static unsigned int force_in_body_from;\n static int stdout_mboxrd;\n-static const char *fmt_patch_subject_prefix = \"PATCH\";\n-static int fmt_patch_name_max = FORMAT_PATCH_NAME_MAX_DEFAULT;\n-static const char *fmt_pretty;\n static int format_no_prefix;\n \n static const char * const builtin_log_usage[] = {\n@@ -111,6 +97,39 @@ static int parse_decoration_style(const char *value)\n \treturn -1;\n }\n \n+struct log_config {\n+\tint default_abbrev_commit;\n+\tint default_show_root;\n+\tint default_follow;\n+\tint default_show_signature;\n+\tint default_encode_email_headers;\n+\tint decoration_style;\n+\tint decoration_given;\n+\tint use_mailmap_config;\n+\tchar *fmt_patch_subject_prefix;\n+\tint fmt_patch_name_max;\n+\tchar *fmt_pretty;\n+\tchar *default_date_mode;\n+};\n+\n+static void log_config_init(struct log_config *cfg)\n+{\n+\tmemset(cfg, 0, sizeof(*cfg));\n+\tcfg->default_show_root = 1;\n+\tcfg->default_encode_email_headers = 1;\n+\tcfg->use_mailmap_config = 1;\n+\tcfg->fmt_patch_subject_prefix = xstrdup(\"PATCH\");\n+\tcfg->fmt_patch_name_max = FORMAT_PATCH_NAME_MAX_DEFAULT;\n+\tcfg->decoration_style = auto_decoration_style();\n+}\n+\n+static void log_config_release(struct log_config *cfg)\n+{\n+\tfree(cfg->default_date_mode);\n+\tfree(cfg->fmt_pretty);\n+\tfree(cfg->fmt_patch_subject_prefix);\n+}\n+\n static int use_default_decoration_filter = 1;\n static struct string_list decorate_refs_exclude = STRING_LIST_INIT_NODUP;\n static struct string_list decorate_refs_exclude_config = STRING_LIST_INIT_NODUP;\n@@ -127,20 +146,22 @@ static int clear_decorations_callback(const struct option *opt UNUSED,\n \treturn 0;\n }\n \n-static int decorate_callback(const struct option *opt UNUSED, const char *arg,\n+static int decorate_callback(const struct option *opt, const char *arg,\n \t\t\t     int unset)\n {\n+\tstruct log_config *cfg = opt->value;\n+\n \tif (unset)\n-\t\tdecoration_style = 0;\n+\t\tcfg->decoration_style = 0;\n \telse if (arg)\n-\t\tdecoration_style = parse_decoration_style(arg);\n+\t\tcfg->decoration_style = parse_decoration_style(arg);\n \telse\n-\t\tdecoration_style = DECORATE_SHORT_REFS;\n+\t\tcfg->decoration_style = DECORATE_SHORT_REFS;\n \n-\tif (decoration_style < 0)\n+\tif (cfg->decoration_style < 0)\n \t\tdie(_(\"invalid --decorate option: %s\"), arg);\n \n-\tdecoration_given = 1;\n+\tcfg->decoration_given = 1;\n \n \treturn 0;\n }\n@@ -160,32 +181,26 @@ static int log_line_range_callback(const struct option *option, const char *arg,\n \treturn 0;\n }\n \n-static void init_log_defaults(void)\n+static void cmd_log_init_defaults(struct rev_info *rev,\n+\t\t\t\t  struct log_config *cfg)\n {\n-\tinit_diff_ui_defaults();\n-\n-\tdecoration_style = auto_decoration_style();\n-}\n-\n-static void cmd_log_init_defaults(struct rev_info *rev)\n-{\n-\tif (fmt_pretty)\n-\t\tget_commit_format(fmt_pretty, rev);\n-\tif (default_follow)\n+\tif (cfg->fmt_pretty)\n+\t\tget_commit_format(cfg->fmt_pretty, rev);\n+\tif (cfg->default_follow)\n \t\trev->diffopt.flags.default_follow_renames = 1;\n \trev->verbose_header = 1;\n \tinit_diffstat_widths(&rev->diffopt);\n \trev->diffopt.flags.recursive = 1;\n \trev->diffopt.flags.allow_textconv = 1;\n-\trev->abbrev_commit = default_abbrev_commit;\n-\trev->show_root_diff = default_show_root;\n-\trev->subject_prefix = fmt_patch_subject_prefix;\n-\trev->patch_name_max = fmt_patch_name_max;\n-\trev->show_signature = default_show_signature;\n-\trev->encode_email_headers = default_encode_email_headers;\n+\trev->abbrev_commit = cfg->default_abbrev_commit;\n+\trev->show_root_diff = cfg->default_show_root;\n+\trev->subject_prefix = cfg->fmt_patch_subject_prefix;\n+\trev->patch_name_max = cfg->fmt_patch_name_max;\n+\trev->show_signature = cfg->default_show_signature;\n+\trev->encode_email_headers = cfg->default_encode_email_headers;\n \n-\tif (default_date_mode)\n-\t\tparse_date_format(default_date_mode, &rev->date_mode);\n+\tif (cfg->default_date_mode)\n+\t\tparse_date_format(cfg->default_date_mode, &rev->date_mode);\n }\n \n static void set_default_decoration_filter(struct decoration_filter *decoration_filter)\n@@ -233,7 +248,8 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f\n }\n \n static void cmd_log_init_finish(int argc, const char **argv, const char *prefix,\n-\t\t\t struct rev_info *rev, struct setup_revision_opt *opt)\n+\t\t\t struct rev_info *rev, struct setup_revision_opt *opt,\n+\t\t\t struct log_config *cfg)\n {\n \tstruct userformat_want w;\n \tint quiet = 0, source = 0, mailmap;\n@@ -258,7 +274,7 @@ static void cmd_log_init_finish(int argc, const char **argv, const char *prefix,\n \t\t\t\tN_(\"pattern\"), N_(\"only decorate refs that match <pattern>\")),\n \t\tOPT_STRING_LIST(0, \"decorate-refs-exclude\", &decorate_refs_exclude,\n \t\t\t\tN_(\"pattern\"), N_(\"do not decorate refs that match <pattern>\")),\n-\t\tOPT_CALLBACK_F(0, \"decorate\", NULL, NULL, N_(\"decorate options\"),\n+\t\tOPT_CALLBACK_F(0, \"decorate\", cfg, NULL, N_(\"decorate options\"),\n \t\t\t       PARSE_OPT_OPTARG, decorate_callback),\n \t\tOPT_CALLBACK('L', NULL, &line_cb, \"range:file\",\n \t\t\t     N_(\"trace the evolution of line range <start>,<end> or function :<funcname> in <file>\"),\n@@ -269,7 +285,7 @@ static void cmd_log_init_finish(int argc, const char **argv, const char *prefix,\n \tline_cb.rev = rev;\n \tline_cb.prefix = prefix;\n \n-\tmailmap = use_mailmap_config;\n+\tmailmap = cfg->use_mailmap_config;\n \targc = parse_options(argc, argv, prefix,\n \t\t\t     builtin_log_options, builtin_log_usage,\n \t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT |\n@@ -314,8 +330,8 @@ static void cmd_log_init_finish(int argc, const char **argv, const char *prefix,\n \t\t * \"log --pretty=raw\" is special; ignore UI oriented\n \t\t * configuration variables such as decoration.\n \t\t */\n-\t\tif (!decoration_given)\n-\t\t\tdecoration_style = 0;\n+\t\tif (!cfg->decoration_given)\n+\t\t\tcfg->decoration_style = 0;\n \t\tif (!rev->abbrev_commit_given)\n \t\t\trev->abbrev_commit = 0;\n \t}\n@@ -326,24 +342,24 @@ static void cmd_log_init_finish(int argc, const char **argv, const char *prefix,\n \t\t\t * Disable decoration loading if the format will not\n \t\t\t * show them anyway.\n \t\t\t */\n-\t\t\tdecoration_style = 0;\n-\t\t} else if (!decoration_style) {\n+\t\t\tcfg->decoration_style = 0;\n+\t\t} else if (!cfg->decoration_style) {\n \t\t\t/*\n \t\t\t * If we are going to show them, make sure we do load\n \t\t\t * them here, but taking care not to override a\n \t\t\t * specific style set by config or --decorate.\n \t\t\t */\n-\t\t\tdecoration_style = DECORATE_SHORT_REFS;\n+\t\t\tcfg->decoration_style = DECORATE_SHORT_REFS;\n \t\t}\n \t}\n \n-\tif (decoration_style || rev->simplify_by_decoration) {\n+\tif (cfg->decoration_style || rev->simplify_by_decoration) {\n \t\tset_default_decoration_filter(&decoration_filter);\n \n-\t\tif (decoration_style)\n+\t\tif (cfg->decoration_style)\n \t\t\trev->show_decorations = 1;\n \n-\t\tload_ref_decorations(&decoration_filter, decoration_style);\n+\t\tload_ref_decorations(&decoration_filter, cfg->decoration_style);\n \t}\n \n \tif (rev->line_level_traverse)\n@@ -353,16 +369,11 @@ static void cmd_log_init_finish(int argc, const char **argv, const char *prefix,\n }\n \n static void cmd_log_init(int argc, const char **argv, const char *prefix,\n-\t\t\t struct rev_info *rev, struct setup_revision_opt *opt)\n+\t\t\t struct rev_info *rev, struct setup_revision_opt *opt,\n+\t\t\t struct log_config *cfg)\n {\n-\tcmd_log_init_defaults(rev);\n-\tcmd_log_init_finish(argc, argv, prefix, rev, opt);\n-}\n-\n-static int cmd_log_deinit(int ret, struct rev_info *rev)\n-{\n-\trelease_revisions(rev);\n-\treturn ret;\n+\tcmd_log_init_defaults(rev, cfg);\n+\tcmd_log_init_finish(argc, argv, prefix, rev, opt, cfg);\n }\n \n /*\n@@ -566,30 +577,37 @@ static int cmd_log_walk(struct rev_info *rev)\n static int git_log_config(const char *var, const char *value,\n \t\t\t  const struct config_context *ctx, void *cb)\n {\n+\tstruct log_config *cfg = cb;\n \tconst char *slot_name;\n \n-\tif (!strcmp(var, \"format.pretty\"))\n-\t\treturn git_config_string(&fmt_pretty, var, value);\n-\tif (!strcmp(var, \"format.subjectprefix\"))\n-\t\treturn git_config_string(&fmt_patch_subject_prefix, var, value);\n+\tif (!strcmp(var, \"format.pretty\")) {\n+\t\tFREE_AND_NULL(cfg->fmt_pretty);\n+\t\treturn git_config_string((const char **) &cfg->fmt_pretty, var, value);\n+\t}\n+\tif (!strcmp(var, \"format.subjectprefix\")) {\n+\t\tFREE_AND_NULL(cfg->fmt_patch_subject_prefix);\n+\t\treturn git_config_string((const char **) &cfg->fmt_patch_subject_prefix, var, value);\n+\t}\n \tif (!strcmp(var, \"format.filenamemaxlength\")) {\n-\t\tfmt_patch_name_max = git_config_int(var, value, ctx->kvi);\n+\t\tcfg->fmt_patch_name_max = git_config_int(var, value, ctx->kvi);\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"format.encodeemailheaders\")) {\n-\t\tdefault_encode_email_headers = git_config_bool(var, value);\n+\t\tcfg->default_encode_email_headers = git_config_bool(var, value);\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"log.abbrevcommit\")) {\n-\t\tdefault_abbrev_commit = git_config_bool(var, value);\n+\t\tcfg->default_abbrev_commit = git_config_bool(var, value);\n \t\treturn 0;\n \t}\n-\tif (!strcmp(var, \"log.date\"))\n-\t\treturn git_config_string(&default_date_mode, var, value);\n+\tif (!strcmp(var, \"log.date\")) {\n+\t\tFREE_AND_NULL(cfg->default_date_mode);\n+\t\treturn git_config_string((const char **) &cfg->default_date_mode, var, value);\n+\t}\n \tif (!strcmp(var, \"log.decorate\")) {\n-\t\tdecoration_style = parse_decoration_style(value);\n-\t\tif (decoration_style < 0)\n-\t\t\tdecoration_style = 0; /* maybe warn? */\n+\t\tcfg->decoration_style = parse_decoration_style(value);\n+\t\tif (cfg->decoration_style < 0)\n+\t\t\tcfg->decoration_style = 0; /* maybe warn? */\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"log.diffmerges\")) {\n@@ -598,21 +616,21 @@ static int git_log_config(const char *var, const char *value,\n \t\treturn diff_merges_config(value);\n \t}\n \tif (!strcmp(var, \"log.showroot\")) {\n-\t\tdefault_show_root = git_config_bool(var, value);\n+\t\tcfg->default_show_root = git_config_bool(var, value);\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"log.follow\")) {\n-\t\tdefault_follow = git_config_bool(var, value);\n+\t\tcfg->default_follow = git_config_bool(var, value);\n \t\treturn 0;\n \t}\n \tif (skip_prefix(var, \"color.decorate.\", &slot_name))\n \t\treturn parse_decorate_color_config(var, slot_name, value);\n \tif (!strcmp(var, \"log.mailmap\")) {\n-\t\tuse_mailmap_config = git_config_bool(var, value);\n+\t\tcfg->use_mailmap_config = git_config_bool(var, value);\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"log.showsignature\")) {\n-\t\tdefault_show_signature = git_config_bool(var, value);\n+\t\tcfg->default_show_signature = git_config_bool(var, value);\n \t\treturn 0;\n \t}\n \n@@ -621,11 +639,14 @@ static int git_log_config(const char *var, const char *value,\n \n int cmd_whatchanged(int argc, const char **argv, const char *prefix)\n {\n+\tstruct log_config cfg;\n \tstruct rev_info rev;\n \tstruct setup_revision_opt opt;\n+\tint ret;\n \n-\tinit_log_defaults();\n-\tgit_config(git_log_config, NULL);\n+\tlog_config_init(&cfg);\n+\tinit_diff_ui_defaults();\n+\tgit_config(git_log_config, &cfg);\n \n \trepo_init_revisions(the_repository, &rev, prefix);\n \tgit_config(grep_config, &rev.grep_filter);\n@@ -635,10 +656,15 @@ int cmd_whatchanged(int argc, const char **argv, const char *prefix)\n \tmemset(&opt, 0, sizeof(opt));\n \topt.def = \"HEAD\";\n \topt.revarg_opt = REVARG_COMMITTISH;\n-\tcmd_log_init(argc, argv, prefix, &rev, &opt);\n+\tcmd_log_init(argc, argv, prefix, &rev, &opt, &cfg);\n \tif (!rev.diffopt.output_format)\n \t\trev.diffopt.output_format = DIFF_FORMAT_RAW;\n-\treturn cmd_log_deinit(cmd_log_walk(&rev), &rev);\n+\n+\tret = cmd_log_walk(&rev);\n+\n+\trelease_revisions(&rev);\n+\tlog_config_release(&cfg);\n+\treturn ret;\n }\n \n static void show_tagger(const char *buf, struct rev_info *rev)\n@@ -733,14 +759,16 @@ static void show_setup_revisions_tweak(struct rev_info *rev)\n \n int cmd_show(int argc, const char **argv, const char *prefix)\n {\n+\tstruct log_config cfg;\n \tstruct rev_info rev;\n \tunsigned int i;\n \tstruct setup_revision_opt opt;\n \tstruct pathspec match_all;\n \tint ret = 0;\n \n-\tinit_log_defaults();\n-\tgit_config(git_log_config, NULL);\n+\tlog_config_init(&cfg);\n+\tinit_diff_ui_defaults();\n+\tgit_config(git_log_config, &cfg);\n \n \tif (the_repository->gitdir) {\n \t\tprepare_repo_settings(the_repository);\n@@ -759,10 +787,14 @@ int cmd_show(int argc, const char **argv, const char *prefix)\n \tmemset(&opt, 0, sizeof(opt));\n \topt.def = \"HEAD\";\n \topt.tweak = show_setup_revisions_tweak;\n-\tcmd_log_init(argc, argv, prefix, &rev, &opt);\n+\tcmd_log_init(argc, argv, prefix, &rev, &opt, &cfg);\n \n-\tif (!rev.no_walk)\n-\t\treturn cmd_log_deinit(cmd_log_walk(&rev), &rev);\n+\tif (!rev.no_walk) {\n+\t\tret = cmd_log_walk(&rev);\n+\t\trelease_revisions(&rev);\n+\t\tlog_config_release(&cfg);\n+\t\treturn ret;\n+\t}\n \n \trev.diffopt.no_free = 1;\n \tfor (i = 0; i < rev.pending.nr && !ret; i++) {\n@@ -832,8 +864,10 @@ int cmd_show(int argc, const char **argv, const char *prefix)\n \n \trev.diffopt.no_free = 0;\n \tdiff_free(&rev.diffopt);\n+\trelease_revisions(&rev);\n+\tlog_config_release(&cfg);\n \n-\treturn cmd_log_deinit(ret, &rev);\n+\treturn ret;\n }\n \n /*\n@@ -841,11 +875,14 @@ int cmd_show(int argc, const char **argv, const char *prefix)\n  */\n int cmd_log_reflog(int argc, const char **argv, const char *prefix)\n {\n+\tstruct log_config cfg;\n \tstruct rev_info rev;\n \tstruct setup_revision_opt opt;\n+\tint ret;\n \n-\tinit_log_defaults();\n-\tgit_config(git_log_config, NULL);\n+\tlog_config_init(&cfg);\n+\tinit_diff_ui_defaults();\n+\tgit_config(git_log_config, &cfg);\n \n \trepo_init_revisions(the_repository, &rev, prefix);\n \tinit_reflog_walk(&rev.reflog_info);\n@@ -854,14 +891,18 @@ int cmd_log_reflog(int argc, const char **argv, const char *prefix)\n \trev.verbose_header = 1;\n \tmemset(&opt, 0, sizeof(opt));\n \topt.def = \"HEAD\";\n-\tcmd_log_init_defaults(&rev);\n+\tcmd_log_init_defaults(&rev, &cfg);\n \trev.abbrev_commit = 1;\n \trev.commit_format = CMIT_FMT_ONELINE;\n \trev.use_terminator = 1;\n \trev.always_show_header = 1;\n-\tcmd_log_init_finish(argc, argv, prefix, &rev, &opt);\n+\tcmd_log_init_finish(argc, argv, prefix, &rev, &opt, &cfg);\n+\n+\tret = cmd_log_walk(&rev);\n \n-\treturn cmd_log_deinit(cmd_log_walk(&rev), &rev);\n+\trelease_revisions(&rev);\n+\tlog_config_release(&cfg);\n+\treturn ret;\n }\n \n static void log_setup_revisions_tweak(struct rev_info *rev)\n@@ -876,11 +917,14 @@ static void log_setup_revisions_tweak(struct rev_info *rev)\n \n int cmd_log(int argc, const char **argv, const char *prefix)\n {\n+\tstruct log_config cfg;\n \tstruct rev_info rev;\n \tstruct setup_revision_opt opt;\n+\tint ret;\n \n-\tinit_log_defaults();\n-\tgit_config(git_log_config, NULL);\n+\tlog_config_init(&cfg);\n+\tinit_diff_ui_defaults();\n+\tgit_config(git_log_config, &cfg);\n \n \trepo_init_revisions(the_repository, &rev, prefix);\n \tgit_config(grep_config, &rev.grep_filter);\n@@ -890,8 +934,13 @@ int cmd_log(int argc, const char **argv, const char *prefix)\n \topt.def = \"HEAD\";\n \topt.revarg_opt = REVARG_COMMITTISH;\n \topt.tweak = log_setup_revisions_tweak;\n-\tcmd_log_init(argc, argv, prefix, &rev, &opt);\n-\treturn cmd_log_deinit(cmd_log_walk(&rev), &rev);\n+\tcmd_log_init(argc, argv, prefix, &rev, &opt, &cfg);\n+\n+\tret = cmd_log_walk(&rev);\n+\n+\trelease_revisions(&rev);\n+\tlog_config_release(&cfg);\n+\treturn ret;\n }\n \n /* format-patch */\n@@ -1884,6 +1933,7 @@ static void infer_range_diff_ranges(struct strbuf *r1,\n \n int cmd_format_patch(int argc, const char **argv, const char *prefix)\n {\n+\tstruct log_config cfg;\n \tstruct commit *commit;\n \tstruct commit **list = NULL;\n \tstruct rev_info rev;\n@@ -1943,7 +1993,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\t\t    N_(\"start numbering patches at <n> instead of 1\")),\n \t\tOPT_STRING('v', \"reroll-count\", &reroll_count, N_(\"reroll-count\"),\n \t\t\t    N_(\"mark the series as Nth re-roll\")),\n-\t\tOPT_INTEGER(0, \"filename-max-length\", &fmt_patch_name_max,\n+\t\tOPT_INTEGER(0, \"filename-max-length\", &cfg.fmt_patch_name_max,\n \t\t\t    N_(\"max length of output filename\")),\n \t\tOPT_CALLBACK_F(0, \"rfc\", &rfc, N_(\"rfc\"),\n \t\t\t       N_(\"add <rfc> (default 'RFC') before 'PATCH'\"),\n@@ -2017,16 +2067,17 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \textra_to.strdup_strings = 1;\n \textra_cc.strdup_strings = 1;\n \n-\tinit_log_defaults();\n+\tlog_config_init(&cfg);\n+\tinit_diff_ui_defaults();\n \tinit_display_notes(&notes_opt);\n-\tgit_config(git_format_config, NULL);\n+\tgit_config(git_format_config, &cfg);\n \trepo_init_revisions(the_repository, &rev, prefix);\n \tgit_config(grep_config, &rev.grep_filter);\n \n \trev.show_notes = show_notes;\n \tmemcpy(&rev.notes_opt, &notes_opt, sizeof(notes_opt));\n \trev.commit_format = CMIT_FMT_EMAIL;\n-\trev.encode_email_headers = default_encode_email_headers;\n+\trev.encode_email_headers = cfg.default_encode_email_headers;\n \trev.expand_tabs_in_log_default = 0;\n \trev.verbose_header = 1;\n \trev.diff = 1;\n@@ -2037,7 +2088,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \ts_r_opt.def = \"HEAD\";\n \ts_r_opt.revarg_opt = REVARG_COMMITTISH;\n \n-\tstrbuf_addstr(&sprefix, fmt_patch_subject_prefix);\n+\tstrbuf_addstr(&sprefix, cfg.fmt_patch_subject_prefix);\n \tif (format_no_prefix)\n \t\tdiff_set_noprefix(&rev.diffopt);\n \n@@ -2059,8 +2110,8 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \trev.force_in_body_from = force_in_body_from;\n \n \t/* Make sure \"0000-$sub.patch\" gives non-negative length for $sub */\n-\tif (fmt_patch_name_max <= strlen(\"0000-\") + strlen(fmt_patch_suffix))\n-\t\tfmt_patch_name_max = strlen(\"0000-\") + strlen(fmt_patch_suffix);\n+\tif (cfg.fmt_patch_name_max <= strlen(\"0000-\") + strlen(fmt_patch_suffix))\n+\t\tcfg.fmt_patch_name_max = strlen(\"0000-\") + strlen(fmt_patch_suffix);\n \n \tif (cover_from_description_arg)\n \t\tcover_from_description_mode = parse_cover_from_description(cover_from_description_arg);\n@@ -2156,7 +2207,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \trev.always_show_header = 1;\n \n \trev.zero_commit = zero_commit;\n-\trev.patch_name_max = fmt_patch_name_max;\n+\trev.patch_name_max = cfg.fmt_patch_name_max;\n \n \tif (!rev.diffopt.flags.text && !no_binary_diff)\n \t\trev.diffopt.flags.binary = 1;\n@@ -2450,7 +2501,9 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \tif (rev.ref_message_ids)\n \t\tstring_list_clear(rev.ref_message_ids, 0);\n \tfree(rev.ref_message_ids);\n-\treturn cmd_log_deinit(0, &rev);\n+\trelease_revisions(&rev);\n+\tlog_config_release(&cfg);\n+\treturn 0;\n }\n \n static int add_pending_commit(const char *arg, struct rev_info *revs, int flags)\n-- \n2.45.1.246.gb9cfe4845c.dirty\n\n"},{"id":"495681","messageId":"eeba79678a80dfb2dcd6e9e9800c862a83d2aed0.1716810168.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716810168.git.ps@pks.im","subject":"[PATCH v3 11/21] builtin/log: stop using globals for format config","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-27T11:46:34Z","receivedAt":"2024-05-27T11:46:40Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"This commit does the exact same as the preceding commit, only for the\nformat configuration instead of the log configuration.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/log.c | 467 ++++++++++++++++++++++++++++----------------------\n 1 file changed, 265 insertions(+), 202 deletions(-)\n\ndiff --git a/builtin/log.c b/builtin/log.c\nindex f5da29ee2a..890bf0c425 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -945,36 +945,6 @@ int cmd_log(int argc, const char **argv, const char *prefix)\n \n /* format-patch */\n \n-static const char *fmt_patch_suffix = \".patch\";\n-static int numbered = 0;\n-static int auto_number = 1;\n-\n-static char *default_attach = NULL;\n-\n-static struct string_list extra_hdr = STRING_LIST_INIT_NODUP;\n-static struct string_list extra_to = STRING_LIST_INIT_NODUP;\n-static struct string_list extra_cc = STRING_LIST_INIT_NODUP;\n-\n-static void add_header(const char *value)\n-{\n-\tstruct string_list_item *item;\n-\tint len = strlen(value);\n-\twhile (len && value[len - 1] == '\\n')\n-\t\tlen--;\n-\n-\tif (!strncasecmp(value, \"to: \", 4)) {\n-\t\titem = string_list_append(&extra_to, value + 4);\n-\t\tlen -= 4;\n-\t} else if (!strncasecmp(value, \"cc: \", 4)) {\n-\t\titem = string_list_append(&extra_cc, value + 4);\n-\t\tlen -= 4;\n-\t} else {\n-\t\titem = string_list_append(&extra_hdr, value);\n-\t}\n-\n-\titem->string[len] = '\\0';\n-}\n-\n enum cover_setting {\n \tCOVER_UNSET,\n \tCOVER_OFF,\n@@ -1001,17 +971,61 @@ enum auto_base_setting {\n \tAUTO_BASE_WHEN_ABLE\n };\n \n-static enum thread_level thread;\n-static int do_signoff;\n-static enum auto_base_setting auto_base;\n-static char *from;\n-static const char *signature = git_version_string;\n-static char *signature_file;\n-static enum cover_setting config_cover_letter;\n-static const char *config_output_directory;\n-static enum cover_from_description cover_from_description_mode = COVER_FROM_MESSAGE;\n-static int show_notes;\n-static struct display_notes_opt notes_opt;\n+struct format_config {\n+\tstruct log_config log;\n+\tenum thread_level thread;\n+\tint do_signoff;\n+\tenum auto_base_setting auto_base;\n+\tchar *base_commit;\n+\tchar *from;\n+\tchar *signature;\n+\tchar *signature_file;\n+\tenum cover_setting config_cover_letter;\n+\tchar *config_output_directory;\n+\tenum cover_from_description cover_from_description_mode;\n+\tint show_notes;\n+\tstruct display_notes_opt notes_opt;\n+\tint numbered_cmdline_opt;\n+\tint numbered;\n+\tint auto_number;\n+\tchar *default_attach;\n+\tstruct string_list extra_hdr;\n+\tstruct string_list extra_to;\n+\tstruct string_list extra_cc;\n+\tint keep_subject;\n+\tint subject_prefix;\n+\tstruct strbuf sprefix;\n+\tchar *fmt_patch_suffix;\n+};\n+\n+static void format_config_init(struct format_config *cfg)\n+{\n+\tmemset(cfg, 0, sizeof(*cfg));\n+\tlog_config_init(&cfg->log);\n+\tcfg->cover_from_description_mode = COVER_FROM_MESSAGE;\n+\tcfg->auto_number = 1;\n+\tstring_list_init_dup(&cfg->extra_hdr);\n+\tstring_list_init_dup(&cfg->extra_to);\n+\tstring_list_init_dup(&cfg->extra_cc);\n+\tstrbuf_init(&cfg->sprefix, 0);\n+\tcfg->fmt_patch_suffix = xstrdup(\".patch\");\n+}\n+\n+static void format_config_release(struct format_config *cfg)\n+{\n+\tlog_config_release(&cfg->log);\n+\tfree(cfg->base_commit);\n+\tfree(cfg->from);\n+\tfree(cfg->signature);\n+\tfree(cfg->signature_file);\n+\tfree(cfg->config_output_directory);\n+\tfree(cfg->default_attach);\n+\tstring_list_clear(&cfg->extra_hdr, 0);\n+\tstring_list_clear(&cfg->extra_to, 0);\n+\tstring_list_clear(&cfg->extra_cc, 0);\n+\tstrbuf_release(&cfg->sprefix);\n+\tfree(cfg->fmt_patch_suffix);\n+}\n \n static enum cover_from_description parse_cover_from_description(const char *arg)\n {\n@@ -1029,27 +1043,51 @@ static enum cover_from_description parse_cover_from_description(const char *arg)\n \t\tdie(_(\"%s: invalid cover from description mode\"), arg);\n }\n \n+static void add_header(struct format_config *cfg, const char *value)\n+{\n+\tstruct string_list_item *item;\n+\tint len = strlen(value);\n+\twhile (len && value[len - 1] == '\\n')\n+\t\tlen--;\n+\n+\tif (!strncasecmp(value, \"to: \", 4)) {\n+\t\titem = string_list_append(&cfg->extra_to, value + 4);\n+\t\tlen -= 4;\n+\t} else if (!strncasecmp(value, \"cc: \", 4)) {\n+\t\titem = string_list_append(&cfg->extra_cc, value + 4);\n+\t\tlen -= 4;\n+\t} else {\n+\t\titem = string_list_append(&cfg->extra_hdr, value);\n+\t}\n+\n+\titem->string[len] = '\\0';\n+}\n+\n static int git_format_config(const char *var, const char *value,\n \t\t\t     const struct config_context *ctx, void *cb)\n {\n+\tstruct format_config *cfg = cb;\n+\n \tif (!strcmp(var, \"format.headers\")) {\n \t\tif (!value)\n \t\t\tdie(_(\"format.headers without value\"));\n-\t\tadd_header(value);\n+\t\tadd_header(cfg, value);\n \t\treturn 0;\n \t}\n-\tif (!strcmp(var, \"format.suffix\"))\n-\t\treturn git_config_string(&fmt_patch_suffix, var, value);\n+\tif (!strcmp(var, \"format.suffix\")) {\n+\t\tFREE_AND_NULL(cfg->fmt_patch_suffix);\n+\t\treturn git_config_string((const char **) &cfg->fmt_patch_suffix, var, value);\n+\t}\n \tif (!strcmp(var, \"format.to\")) {\n \t\tif (!value)\n \t\t\treturn config_error_nonbool(var);\n-\t\tstring_list_append(&extra_to, value);\n+\t\tstring_list_append(&cfg->extra_to, value);\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"format.cc\")) {\n \t\tif (!value)\n \t\t\treturn config_error_nonbool(var);\n-\t\tstring_list_append(&extra_cc, value);\n+\t\tstring_list_append(&cfg->extra_cc, value);\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"diff.color\") || !strcmp(var, \"color.diff\") ||\n@@ -1058,69 +1096,76 @@ static int git_format_config(const char *var, const char *value,\n \t}\n \tif (!strcmp(var, \"format.numbered\")) {\n \t\tif (value && !strcasecmp(value, \"auto\")) {\n-\t\t\tauto_number = 1;\n+\t\t\tcfg->auto_number = 1;\n \t\t\treturn 0;\n \t\t}\n-\t\tnumbered = git_config_bool(var, value);\n-\t\tauto_number = auto_number && numbered;\n+\t\tcfg->numbered = git_config_bool(var, value);\n+\t\tcfg->auto_number = cfg->auto_number && cfg->numbered;\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"format.attach\")) {\n-\t\tif (value && *value)\n-\t\t\tdefault_attach = xstrdup(value);\n-\t\telse if (value && !*value)\n-\t\t\tFREE_AND_NULL(default_attach);\n-\t\telse\n-\t\t\tdefault_attach = xstrdup(git_version_string);\n+\t\tif (value && *value) {\n+\t\t\tFREE_AND_NULL(cfg->default_attach);\n+\t\t\tcfg->default_attach = xstrdup(value);\n+\t\t} else if (value && !*value) {\n+\t\t\tFREE_AND_NULL(cfg->default_attach);\n+\t\t} else {\n+\t\t\tFREE_AND_NULL(cfg->default_attach);\n+\t\t\tcfg->default_attach = xstrdup(git_version_string);\n+\t\t}\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"format.thread\")) {\n \t\tif (value && !strcasecmp(value, \"deep\")) {\n-\t\t\tthread = THREAD_DEEP;\n+\t\t\tcfg->thread = THREAD_DEEP;\n \t\t\treturn 0;\n \t\t}\n \t\tif (value && !strcasecmp(value, \"shallow\")) {\n-\t\t\tthread = THREAD_SHALLOW;\n+\t\t\tcfg->thread = THREAD_SHALLOW;\n \t\t\treturn 0;\n \t\t}\n-\t\tthread = git_config_bool(var, value) ? THREAD_SHALLOW : THREAD_UNSET;\n+\t\tcfg->thread = git_config_bool(var, value) ? THREAD_SHALLOW : THREAD_UNSET;\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"format.signoff\")) {\n-\t\tdo_signoff = git_config_bool(var, value);\n+\t\tcfg->do_signoff = git_config_bool(var, value);\n \t\treturn 0;\n \t}\n-\tif (!strcmp(var, \"format.signature\"))\n-\t\treturn git_config_string(&signature, var, value);\n-\tif (!strcmp(var, \"format.signaturefile\"))\n-\t\treturn git_config_pathname(&signature_file, var, value);\n+\tif (!strcmp(var, \"format.signature\")) {\n+\t\tFREE_AND_NULL(cfg->signature);\n+\t\treturn git_config_string((const char **) &cfg->signature, var, value);\n+\t}\n+\tif (!strcmp(var, \"format.signaturefile\")) {\n+\t\tFREE_AND_NULL(cfg->signature_file);\n+\t\treturn git_config_pathname(&cfg->signature_file, var, value);\n+\t}\n \tif (!strcmp(var, \"format.coverletter\")) {\n \t\tif (value && !strcasecmp(value, \"auto\")) {\n-\t\t\tconfig_cover_letter = COVER_AUTO;\n+\t\t\tcfg->config_cover_letter = COVER_AUTO;\n \t\t\treturn 0;\n \t\t}\n-\t\tconfig_cover_letter = git_config_bool(var, value) ? COVER_ON : COVER_OFF;\n+\t\tcfg->config_cover_letter = git_config_bool(var, value) ? COVER_ON : COVER_OFF;\n \t\treturn 0;\n \t}\n-\tif (!strcmp(var, \"format.outputdirectory\"))\n-\t\treturn git_config_string(&config_output_directory, var, value);\n+\tif (!strcmp(var, \"format.outputdirectory\")) {\n+\t\tFREE_AND_NULL(cfg->config_output_directory);\n+\t\treturn git_config_string((const char **) &cfg->config_output_directory, var, value);\n+\t}\n \tif (!strcmp(var, \"format.useautobase\")) {\n \t\tif (value && !strcasecmp(value, \"whenAble\")) {\n-\t\t\tauto_base = AUTO_BASE_WHEN_ABLE;\n+\t\t\tcfg->auto_base = AUTO_BASE_WHEN_ABLE;\n \t\t\treturn 0;\n \t\t}\n-\t\tauto_base = git_config_bool(var, value) ? AUTO_BASE_ALWAYS : AUTO_BASE_NEVER;\n+\t\tcfg->auto_base = git_config_bool(var, value) ? AUTO_BASE_ALWAYS : AUTO_BASE_NEVER;\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"format.from\")) {\n \t\tint b = git_parse_maybe_bool(value);\n-\t\tfree(from);\n+\t\tFREE_AND_NULL(cfg->from);\n \t\tif (b < 0)\n-\t\t\tfrom = xstrdup(value);\n+\t\t\tcfg->from = xstrdup(value);\n \t\telse if (b)\n-\t\t\tfrom = xstrdup(git_committer_info(IDENT_NO_DATE));\n-\t\telse\n-\t\t\tfrom = NULL;\n+\t\t\tcfg->from = xstrdup(git_committer_info(IDENT_NO_DATE));\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"format.forceinbodyfrom\")) {\n@@ -1130,15 +1175,15 @@ static int git_format_config(const char *var, const char *value,\n \tif (!strcmp(var, \"format.notes\")) {\n \t\tint b = git_parse_maybe_bool(value);\n \t\tif (b < 0)\n-\t\t\tenable_ref_display_notes(&notes_opt, &show_notes, value);\n+\t\t\tenable_ref_display_notes(&cfg->notes_opt, &cfg->show_notes, value);\n \t\telse if (b)\n-\t\t\tenable_default_display_notes(&notes_opt, &show_notes);\n+\t\t\tenable_default_display_notes(&cfg->notes_opt, &cfg->show_notes);\n \t\telse\n-\t\t\tdisable_display_notes(&notes_opt, &show_notes);\n+\t\t\tdisable_display_notes(&cfg->notes_opt, &cfg->show_notes);\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"format.coverfromdescription\")) {\n-\t\tcover_from_description_mode = parse_cover_from_description(value);\n+\t\tcfg->cover_from_description_mode = parse_cover_from_description(value);\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"format.mboxrd\")) {\n@@ -1159,7 +1204,7 @@ static int git_format_config(const char *var, const char *value,\n \tif (!strcmp(var, \"diff.noprefix\"))\n \t\treturn 0;\n \n-\treturn git_log_config(var, value, ctx, cb);\n+\treturn git_log_config(var, value, ctx, &cfg->log);\n }\n \n static const char *output_directory = NULL;\n@@ -1247,7 +1292,7 @@ static void gen_message_id(struct rev_info *info, char *base)\n \tinfo->message_id = strbuf_detach(&buf, NULL);\n }\n \n-static void print_signature(FILE *file)\n+static void print_signature(const char *signature, FILE *file)\n {\n \tif (!signature || !*signature)\n \t\treturn;\n@@ -1317,14 +1362,15 @@ static void prepare_cover_text(struct pretty_print_context *pp,\n \t\t\t       const char *branch_name,\n \t\t\t       struct strbuf *sb,\n \t\t\t       const char *encoding,\n-\t\t\t       int need_8bit_cte)\n+\t\t\t       int need_8bit_cte,\n+\t\t\t       const struct format_config *cfg)\n {\n \tconst char *subject = \"*** SUBJECT HERE ***\";\n \tconst char *body = \"*** BLURB HERE ***\";\n \tstruct strbuf description_sb = STRBUF_INIT;\n \tstruct strbuf subject_sb = STRBUF_INIT;\n \n-\tif (cover_from_description_mode == COVER_FROM_NONE)\n+\tif (cfg->cover_from_description_mode == COVER_FROM_NONE)\n \t\tgoto do_pp;\n \n \tif (description_file && *description_file)\n@@ -1334,13 +1380,13 @@ static void prepare_cover_text(struct pretty_print_context *pp,\n \tif (!description_sb.len)\n \t\tgoto do_pp;\n \n-\tif (cover_from_description_mode == COVER_FROM_SUBJECT ||\n-\t\t\tcover_from_description_mode == COVER_FROM_AUTO)\n+\tif (cfg->cover_from_description_mode == COVER_FROM_SUBJECT ||\n+\t    cfg->cover_from_description_mode == COVER_FROM_AUTO)\n \t\tbody = format_subject(&subject_sb, description_sb.buf, \" \");\n \n-\tif (cover_from_description_mode == COVER_FROM_MESSAGE ||\n-\t\t\t(cover_from_description_mode == COVER_FROM_AUTO &&\n-\t\t\t subject_sb.len > COVER_FROM_AUTO_MAX_SUBJECT_LEN))\n+\tif (cfg->cover_from_description_mode == COVER_FROM_MESSAGE ||\n+\t    (cfg->cover_from_description_mode == COVER_FROM_AUTO &&\n+\t     subject_sb.len > COVER_FROM_AUTO_MAX_SUBJECT_LEN))\n \t\tbody = description_sb.buf;\n \telse\n \t\tsubject = subject_sb.buf;\n@@ -1377,7 +1423,8 @@ static void make_cover_letter(struct rev_info *rev, int use_separate_file,\n \t\t\t      int nr, struct commit **list,\n \t\t\t      const char *description_file,\n \t\t\t      const char *branch_name,\n-\t\t\t      int quiet)\n+\t\t\t      int quiet,\n+\t\t\t      const struct format_config *cfg)\n {\n \tconst char *committer;\n \tstruct shortlog log;\n@@ -1416,7 +1463,7 @@ static void make_cover_letter(struct rev_info *rev, int use_separate_file,\n \tpp.encode_email_headers = rev->encode_email_headers;\n \tpp_user_info(&pp, NULL, &sb, committer, encoding);\n \tprepare_cover_text(&pp, description_file, branch_name, &sb,\n-\t\t\t   encoding, need_8bit_cte);\n+\t\t\t   encoding, need_8bit_cte, cfg);\n \tfprintf(rev->diffopt.file, \"%s\\n\", sb.buf);\n \n \tfree(pp.after_subject);\n@@ -1517,29 +1564,30 @@ static const char * const builtin_format_patch_usage[] = {\n \tNULL\n };\n \n-static int keep_subject = 0;\n+struct keep_callback_data {\n+\tstruct format_config *cfg;\n+\tstruct rev_info *revs;\n+};\n \n static int keep_callback(const struct option *opt, const char *arg, int unset)\n {\n+\tstruct keep_callback_data *data = opt->value;\n \tBUG_ON_OPT_NEG(unset);\n \tBUG_ON_OPT_ARG(arg);\n-\t((struct rev_info *)opt->value)->total = -1;\n-\tkeep_subject = 1;\n+\tdata->revs->total = -1;\n+\tdata->cfg->keep_subject = 1;\n \treturn 0;\n }\n \n-static int subject_prefix = 0;\n-\n static int subject_prefix_callback(const struct option *opt, const char *arg,\n \t\t\t    int unset)\n {\n-\tstruct strbuf *sprefix;\n+\tstruct format_config *cfg = opt->value;\n \n \tBUG_ON_OPT_NEG(unset);\n-\tsprefix = opt->value;\n-\tsubject_prefix = 1;\n-\tstrbuf_reset(sprefix);\n-\tstrbuf_addstr(sprefix, arg);\n+\tcfg->subject_prefix = 1;\n+\tstrbuf_reset(&cfg->sprefix);\n+\tstrbuf_addstr(&cfg->sprefix, arg);\n \treturn 0;\n }\n \n@@ -1556,15 +1604,14 @@ static int rfc_callback(const struct option *opt, const char *arg,\n \treturn 0;\n }\n \n-static int numbered_cmdline_opt = 0;\n-\n static int numbered_callback(const struct option *opt, const char *arg,\n \t\t\t     int unset)\n {\n+\tstruct format_config *cfg = opt->value;\n \tBUG_ON_OPT_ARG(arg);\n-\t*(int *)opt->value = numbered_cmdline_opt = unset ? 0 : 1;\n+\tcfg->numbered = cfg->numbered_cmdline_opt = unset ? 0 : 1;\n \tif (unset)\n-\t\tauto_number =  0;\n+\t\tcfg->auto_number =  0;\n \treturn 0;\n }\n \n@@ -1588,13 +1635,14 @@ static int output_directory_callback(const struct option *opt, const char *arg,\n \n static int thread_callback(const struct option *opt, const char *arg, int unset)\n {\n-\tenum thread_level *thread = (enum thread_level *)opt->value;\n+\tstruct format_config *cfg = opt->value;\n+\n \tif (unset)\n-\t\t*thread = THREAD_UNSET;\n+\t\tcfg->thread = THREAD_UNSET;\n \telse if (!arg || !strcmp(arg, \"shallow\"))\n-\t\t*thread = THREAD_SHALLOW;\n+\t\tcfg->thread = THREAD_SHALLOW;\n \telse if (!strcmp(arg, \"deep\"))\n-\t\t*thread = THREAD_DEEP;\n+\t\tcfg->thread = THREAD_DEEP;\n \t/*\n \t * Please update _git_formatpatch() in git-completion.bash\n \t * when you add new options.\n@@ -1630,15 +1678,17 @@ static int inline_callback(const struct option *opt, const char *arg, int unset)\n \treturn 0;\n }\n \n-static int header_callback(const struct option *opt UNUSED, const char *arg,\n+static int header_callback(const struct option *opt, const char *arg,\n \t\t\t   int unset)\n {\n+\tstruct format_config *cfg = opt->value;\n+\n \tif (unset) {\n-\t\tstring_list_clear(&extra_hdr, 0);\n-\t\tstring_list_clear(&extra_to, 0);\n-\t\tstring_list_clear(&extra_cc, 0);\n+\t\tstring_list_clear(&cfg->extra_hdr, 0);\n+\t\tstring_list_clear(&cfg->extra_to, 0);\n+\t\tstring_list_clear(&cfg->extra_cc, 0);\n \t} else {\n-\t\tadd_header(arg);\n+\t\tadd_header(cfg, arg);\n \t}\n \treturn 0;\n }\n@@ -1660,17 +1710,17 @@ static int from_callback(const struct option *opt, const char *arg, int unset)\n \n static int base_callback(const struct option *opt, const char *arg, int unset)\n {\n-\tconst char **base_commit = opt->value;\n+\tstruct format_config *cfg = opt->value;\n \n \tif (unset) {\n-\t\tauto_base = AUTO_BASE_NEVER;\n-\t\t*base_commit = NULL;\n+\t\tcfg->auto_base = AUTO_BASE_NEVER;\n+\t\tFREE_AND_NULL(cfg->base_commit);\n \t} else if (!strcmp(arg, \"auto\")) {\n-\t\tauto_base = AUTO_BASE_ALWAYS;\n-\t\t*base_commit = NULL;\n+\t\tcfg->auto_base = AUTO_BASE_ALWAYS;\n+\t\tFREE_AND_NULL(cfg->base_commit);\n \t} else {\n-\t\tauto_base = AUTO_BASE_NEVER;\n-\t\t*base_commit = arg;\n+\t\tcfg->auto_base = AUTO_BASE_NEVER;\n+\t\tcfg->base_commit = xstrdup(arg);\n \t}\n \treturn 0;\n }\n@@ -1681,7 +1731,7 @@ struct base_tree_info {\n \tstruct object_id *patch_id;\n };\n \n-static struct commit *get_base_commit(const char *base_commit,\n+static struct commit *get_base_commit(const struct format_config *cfg,\n \t\t\t\t      struct commit **list,\n \t\t\t\t      int total)\n {\n@@ -1689,9 +1739,9 @@ static struct commit *get_base_commit(const char *base_commit,\n \tstruct commit **rev;\n \tint i = 0, rev_nr = 0, auto_select, die_on_failure, ret;\n \n-\tswitch (auto_base) {\n+\tswitch (cfg->auto_base) {\n \tcase AUTO_BASE_NEVER:\n-\t\tif (base_commit) {\n+\t\tif (cfg->base_commit) {\n \t\t\tauto_select = 0;\n \t\t\tdie_on_failure = 1;\n \t\t} else {\n@@ -1701,11 +1751,11 @@ static struct commit *get_base_commit(const char *base_commit,\n \t\tbreak;\n \tcase AUTO_BASE_ALWAYS:\n \tcase AUTO_BASE_WHEN_ABLE:\n-\t\tif (base_commit) {\n+\t\tif (cfg->base_commit) {\n \t\t\tBUG(\"requested automatic base selection but a commit was provided\");\n \t\t} else {\n \t\t\tauto_select = 1;\n-\t\t\tdie_on_failure = auto_base == AUTO_BASE_ALWAYS;\n+\t\t\tdie_on_failure = cfg->auto_base == AUTO_BASE_ALWAYS;\n \t\t}\n \t\tbreak;\n \tdefault:\n@@ -1713,9 +1763,9 @@ static struct commit *get_base_commit(const char *base_commit,\n \t}\n \n \tif (!auto_select) {\n-\t\tbase = lookup_commit_reference_by_name(base_commit);\n+\t\tbase = lookup_commit_reference_by_name(cfg->base_commit);\n \t\tif (!base)\n-\t\t\tdie(_(\"unknown commit %s\"), base_commit);\n+\t\t\tdie(_(\"unknown commit %s\"), cfg->base_commit);\n \t} else {\n \t\tstruct branch *curr_branch = branch_get(NULL);\n \t\tconst char *upstream = branch_get_upstream(curr_branch, NULL);\n@@ -1933,7 +1983,7 @@ static void infer_range_diff_ranges(struct strbuf *r1,\n \n int cmd_format_patch(int argc, const char **argv, const char *prefix)\n {\n-\tstruct log_config cfg;\n+\tstruct format_config cfg;\n \tstruct commit *commit;\n \tstruct commit **list = NULL;\n \tstruct rev_info rev;\n@@ -1958,7 +2008,6 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \tchar *cover_from_description_arg = NULL;\n \tchar *description_file = NULL;\n \tchar *branch_name = NULL;\n-\tchar *base_commit = NULL;\n \tstruct base_tree_info bases;\n \tstruct commit *base;\n \tint show_progress = 0;\n@@ -1969,18 +2018,24 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \tstruct strbuf rdiff1 = STRBUF_INIT;\n \tstruct strbuf rdiff2 = STRBUF_INIT;\n \tstruct strbuf rdiff_title = STRBUF_INIT;\n-\tstruct strbuf sprefix = STRBUF_INIT;\n \tconst char *rfc = NULL;\n \tint creation_factor = -1;\n+\tconst char *signature = git_version_string;\n+\tconst char *signature_file_arg = NULL;\n+\tstruct keep_callback_data keep_callback_data = {\n+\t\t.cfg = &cfg,\n+\t\t.revs = &rev,\n+\t};\n+\tconst char *fmt_patch_suffix = NULL;\n \n \tconst struct option builtin_format_patch_options[] = {\n-\t\tOPT_CALLBACK_F('n', \"numbered\", &numbered, NULL,\n+\t\tOPT_CALLBACK_F('n', \"numbered\", &cfg, NULL,\n \t\t\t    N_(\"use [PATCH n/m] even with a single patch\"),\n \t\t\t    PARSE_OPT_NOARG, numbered_callback),\n-\t\tOPT_CALLBACK_F('N', \"no-numbered\", &numbered, NULL,\n+\t\tOPT_CALLBACK_F('N', \"no-numbered\", &cfg, NULL,\n \t\t\t    N_(\"use [PATCH] even with multiple patches\"),\n \t\t\t    PARSE_OPT_NOARG | PARSE_OPT_NONEG, no_numbered_callback),\n-\t\tOPT_BOOL('s', \"signoff\", &do_signoff, N_(\"add a Signed-off-by trailer\")),\n+\t\tOPT_BOOL('s', \"signoff\", &cfg.do_signoff, N_(\"add a Signed-off-by trailer\")),\n \t\tOPT_BOOL(0, \"stdout\", &use_stdout,\n \t\t\t    N_(\"print patches to standard out\")),\n \t\tOPT_BOOL(0, \"cover-letter\", &cover_letter,\n@@ -1993,7 +2048,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\t\t    N_(\"start numbering patches at <n> instead of 1\")),\n \t\tOPT_STRING('v', \"reroll-count\", &reroll_count, N_(\"reroll-count\"),\n \t\t\t    N_(\"mark the series as Nth re-roll\")),\n-\t\tOPT_INTEGER(0, \"filename-max-length\", &cfg.fmt_patch_name_max,\n+\t\tOPT_INTEGER(0, \"filename-max-length\", &cfg.log.fmt_patch_name_max,\n \t\t\t    N_(\"max length of output filename\")),\n \t\tOPT_CALLBACK_F(0, \"rfc\", &rfc, N_(\"rfc\"),\n \t\t\t       N_(\"add <rfc> (default 'RFC') before 'PATCH'\"),\n@@ -2003,13 +2058,13 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\t\t    N_(\"generate parts of a cover letter based on a branch's description\")),\n \t\tOPT_FILENAME(0, \"description-file\", &description_file,\n \t\t\t     N_(\"use branch description from file\")),\n-\t\tOPT_CALLBACK_F(0, \"subject-prefix\", &sprefix, N_(\"prefix\"),\n+\t\tOPT_CALLBACK_F(0, \"subject-prefix\", &cfg, N_(\"prefix\"),\n \t\t\t    N_(\"use [<prefix>] instead of [PATCH]\"),\n \t\t\t    PARSE_OPT_NONEG, subject_prefix_callback),\n \t\tOPT_CALLBACK_F('o', \"output-directory\", &output_directory,\n \t\t\t    N_(\"dir\"), N_(\"store resulting files in <dir>\"),\n \t\t\t    PARSE_OPT_NONEG, output_directory_callback),\n-\t\tOPT_CALLBACK_F('k', \"keep-subject\", &rev, NULL,\n+\t\tOPT_CALLBACK_F('k', \"keep-subject\", &keep_callback_data, NULL,\n \t\t\t    N_(\"don't strip/add [PATCH]\"),\n \t\t\t    PARSE_OPT_NOARG | PARSE_OPT_NONEG, keep_callback),\n \t\tOPT_BOOL(0, \"no-binary\", &no_binary_diff,\n@@ -2022,11 +2077,11 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\t\t      N_(\"show patch format instead of default (patch + stat)\"),\n \t\t\t      1, PARSE_OPT_NONEG),\n \t\tOPT_GROUP(N_(\"Messaging\")),\n-\t\tOPT_CALLBACK(0, \"add-header\", NULL, N_(\"header\"),\n+\t\tOPT_CALLBACK(0, \"add-header\", &cfg, N_(\"header\"),\n \t\t\t    N_(\"add email header\"), header_callback),\n-\t\tOPT_STRING_LIST(0, \"to\", &extra_to, N_(\"email\"), N_(\"add To: header\")),\n-\t\tOPT_STRING_LIST(0, \"cc\", &extra_cc, N_(\"email\"), N_(\"add Cc: header\")),\n-\t\tOPT_CALLBACK_F(0, \"from\", &from, N_(\"ident\"),\n+\t\tOPT_STRING_LIST(0, \"to\", &cfg.extra_to, N_(\"email\"), N_(\"add To: header\")),\n+\t\tOPT_STRING_LIST(0, \"cc\", &cfg.extra_cc, N_(\"email\"), N_(\"add Cc: header\")),\n+\t\tOPT_CALLBACK_F(0, \"from\", &cfg.from, N_(\"ident\"),\n \t\t\t    N_(\"set From address to <ident> (or committer ident if absent)\"),\n \t\t\t    PARSE_OPT_OPTARG, from_callback),\n \t\tOPT_STRING(0, \"in-reply-to\", &in_reply_to, N_(\"message-id\"),\n@@ -2038,15 +2093,15 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\t\t    N_(\"inline the patch\"),\n \t\t\t    PARSE_OPT_OPTARG | PARSE_OPT_NONEG,\n \t\t\t    inline_callback),\n-\t\tOPT_CALLBACK_F(0, \"thread\", &thread, N_(\"style\"),\n+\t\tOPT_CALLBACK_F(0, \"thread\", &cfg, N_(\"style\"),\n \t\t\t    N_(\"enable message threading, styles: shallow, deep\"),\n \t\t\t    PARSE_OPT_OPTARG, thread_callback),\n \t\tOPT_STRING(0, \"signature\", &signature, N_(\"signature\"),\n \t\t\t    N_(\"add a signature\")),\n-\t\tOPT_CALLBACK_F(0, \"base\", &base_commit, N_(\"base-commit\"),\n+\t\tOPT_CALLBACK_F(0, \"base\", &cfg, N_(\"base-commit\"),\n \t\t\t       N_(\"add prerequisite tree info to the patch series\"),\n \t\t\t       0, base_callback),\n-\t\tOPT_FILENAME(0, \"signature-file\", &signature_file,\n+\t\tOPT_FILENAME(0, \"signature-file\", &signature_file_arg,\n \t\t\t\tN_(\"add a signature from a file\")),\n \t\tOPT__QUIET(&quiet, N_(\"don't print the patch filenames\")),\n \t\tOPT_BOOL(0, \"progress\", &show_progress,\n@@ -2063,21 +2118,17 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\tOPT_END()\n \t};\n \n-\textra_hdr.strdup_strings = 1;\n-\textra_to.strdup_strings = 1;\n-\textra_cc.strdup_strings = 1;\n-\n-\tlog_config_init(&cfg);\n+\tformat_config_init(&cfg);\n \tinit_diff_ui_defaults();\n-\tinit_display_notes(&notes_opt);\n+\tinit_display_notes(&cfg.notes_opt);\n \tgit_config(git_format_config, &cfg);\n \trepo_init_revisions(the_repository, &rev, prefix);\n \tgit_config(grep_config, &rev.grep_filter);\n \n-\trev.show_notes = show_notes;\n-\tmemcpy(&rev.notes_opt, &notes_opt, sizeof(notes_opt));\n+\trev.show_notes = cfg.show_notes;\n+\tmemcpy(&rev.notes_opt, &cfg.notes_opt, sizeof(cfg.notes_opt));\n \trev.commit_format = CMIT_FMT_EMAIL;\n-\trev.encode_email_headers = cfg.default_encode_email_headers;\n+\trev.encode_email_headers = cfg.log.default_encode_email_headers;\n \trev.expand_tabs_in_log_default = 0;\n \trev.verbose_header = 1;\n \trev.diff = 1;\n@@ -2088,12 +2139,12 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \ts_r_opt.def = \"HEAD\";\n \ts_r_opt.revarg_opt = REVARG_COMMITTISH;\n \n-\tstrbuf_addstr(&sprefix, cfg.fmt_patch_subject_prefix);\n+\tstrbuf_addstr(&cfg.sprefix, cfg.log.fmt_patch_subject_prefix);\n \tif (format_no_prefix)\n \t\tdiff_set_noprefix(&rev.diffopt);\n \n-\tif (default_attach) {\n-\t\trev.mime_boundary = default_attach;\n+\tif (cfg.default_attach) {\n+\t\trev.mime_boundary = cfg.default_attach;\n \t\trev.no_inline = 1;\n \t}\n \n@@ -2109,60 +2160,63 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \n \trev.force_in_body_from = force_in_body_from;\n \n+\tif (!fmt_patch_suffix)\n+\t\tfmt_patch_suffix = cfg.fmt_patch_suffix;\n+\n \t/* Make sure \"0000-$sub.patch\" gives non-negative length for $sub */\n-\tif (cfg.fmt_patch_name_max <= strlen(\"0000-\") + strlen(fmt_patch_suffix))\n-\t\tcfg.fmt_patch_name_max = strlen(\"0000-\") + strlen(fmt_patch_suffix);\n+\tif (cfg.log.fmt_patch_name_max <= strlen(\"0000-\") + strlen(fmt_patch_suffix))\n+\t\tcfg.log.fmt_patch_name_max = strlen(\"0000-\") + strlen(fmt_patch_suffix);\n \n \tif (cover_from_description_arg)\n-\t\tcover_from_description_mode = parse_cover_from_description(cover_from_description_arg);\n+\t\tcfg.cover_from_description_mode = parse_cover_from_description(cover_from_description_arg);\n \n \tif (rfc && rfc[0]) {\n-\t\tsubject_prefix = 1;\n+\t\tcfg.subject_prefix = 1;\n \t\tif (rfc[0] == '-')\n-\t\t\tstrbuf_addf(&sprefix, \" %s\", rfc + 1);\n+\t\t\tstrbuf_addf(&cfg.sprefix, \" %s\", rfc + 1);\n \t\telse\n-\t\t\tstrbuf_insertf(&sprefix, 0, \"%s \", rfc);\n+\t\t\tstrbuf_insertf(&cfg.sprefix, 0, \"%s \", rfc);\n \t}\n \n \tif (reroll_count) {\n-\t\tstrbuf_addf(&sprefix, \" v%s\", reroll_count);\n+\t\tstrbuf_addf(&cfg.sprefix, \" v%s\", reroll_count);\n \t\trev.reroll_count = reroll_count;\n \t}\n \n-\trev.subject_prefix = sprefix.buf;\n+\trev.subject_prefix = cfg.sprefix.buf;\n \n-\tfor (i = 0; i < extra_hdr.nr; i++) {\n-\t\tstrbuf_addstr(&buf, extra_hdr.items[i].string);\n+\tfor (i = 0; i < cfg.extra_hdr.nr; i++) {\n+\t\tstrbuf_addstr(&buf, cfg.extra_hdr.items[i].string);\n \t\tstrbuf_addch(&buf, '\\n');\n \t}\n \n-\tif (extra_to.nr)\n+\tif (cfg.extra_to.nr)\n \t\tstrbuf_addstr(&buf, \"To: \");\n-\tfor (i = 0; i < extra_to.nr; i++) {\n+\tfor (i = 0; i < cfg.extra_to.nr; i++) {\n \t\tif (i)\n \t\t\tstrbuf_addstr(&buf, \"    \");\n-\t\tstrbuf_addstr(&buf, extra_to.items[i].string);\n-\t\tif (i + 1 < extra_to.nr)\n+\t\tstrbuf_addstr(&buf, cfg.extra_to.items[i].string);\n+\t\tif (i + 1 < cfg.extra_to.nr)\n \t\t\tstrbuf_addch(&buf, ',');\n \t\tstrbuf_addch(&buf, '\\n');\n \t}\n \n-\tif (extra_cc.nr)\n+\tif (cfg.extra_cc.nr)\n \t\tstrbuf_addstr(&buf, \"Cc: \");\n-\tfor (i = 0; i < extra_cc.nr; i++) {\n+\tfor (i = 0; i < cfg.extra_cc.nr; i++) {\n \t\tif (i)\n \t\t\tstrbuf_addstr(&buf, \"    \");\n-\t\tstrbuf_addstr(&buf, extra_cc.items[i].string);\n-\t\tif (i + 1 < extra_cc.nr)\n+\t\tstrbuf_addstr(&buf, cfg.extra_cc.items[i].string);\n+\t\tif (i + 1 < cfg.extra_cc.nr)\n \t\t\tstrbuf_addch(&buf, ',');\n \t\tstrbuf_addch(&buf, '\\n');\n \t}\n \n \trev.extra_headers = to_free = strbuf_detach(&buf, NULL);\n \n-\tif (from) {\n-\t\tif (split_ident_line(&rev.from_ident, from, strlen(from)))\n-\t\t\tdie(_(\"invalid ident line: %s\"), from);\n+\tif (cfg.from) {\n+\t\tif (split_ident_line(&rev.from_ident, cfg.from, strlen(cfg.from)))\n+\t\t\tdie(_(\"invalid ident line: %s\"), cfg.from);\n \t}\n \n \tif (start_number < 0)\n@@ -2173,14 +2227,14 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t * and it would conflict with --keep-subject (-k) from the\n \t * command line, reset \"numbered\".\n \t */\n-\tif (numbered && keep_subject && !numbered_cmdline_opt)\n-\t\tnumbered = 0;\n+\tif (cfg.numbered && cfg.keep_subject && !cfg.numbered_cmdline_opt)\n+\t\tcfg.numbered = 0;\n \n-\tif (numbered && keep_subject)\n+\tif (cfg.numbered && cfg.keep_subject)\n \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"-n\", \"-k\");\n-\tif (keep_subject && subject_prefix)\n+\tif (cfg.keep_subject && cfg.subject_prefix)\n \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--subject-prefix/--rfc\", \"-k\");\n-\trev.preserve_subject = keep_subject;\n+\trev.preserve_subject = cfg.keep_subject;\n \n \targc = setup_revisions(argc, argv, &rev, &s_r_opt);\n \tif (argc > 1)\n@@ -2207,7 +2261,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \trev.always_show_header = 1;\n \n \trev.zero_commit = zero_commit;\n-\trev.patch_name_max = cfg.fmt_patch_name_max;\n+\trev.patch_name_max = cfg.log.fmt_patch_name_max;\n \n \tif (!rev.diffopt.flags.text && !no_binary_diff)\n \t\trev.diffopt.flags.binary = 1;\n@@ -2228,7 +2282,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\tint saved;\n \n \t\tif (!output_directory)\n-\t\t\toutput_directory = config_output_directory;\n+\t\t\toutput_directory = cfg.config_output_directory;\n \t\toutput_directory = set_outdir(prefix, output_directory);\n \n \t\tif (rev.diffopt.use_color != GIT_COLOR_ALWAYS)\n@@ -2326,14 +2380,14 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\tgoto done;\n \ttotal = nr;\n \tif (cover_letter == -1) {\n-\t\tif (config_cover_letter == COVER_AUTO)\n+\t\tif (cfg.config_cover_letter == COVER_AUTO)\n \t\t\tcover_letter = (total > 1);\n \t\telse\n-\t\t\tcover_letter = (config_cover_letter == COVER_ON);\n+\t\t\tcover_letter = (cfg.config_cover_letter == COVER_ON);\n \t}\n-\tif (!keep_subject && auto_number && (total > 1 || cover_letter))\n-\t\tnumbered = 1;\n-\tif (numbered)\n+\tif (!cfg.keep_subject && cfg.auto_number && (total > 1 || cover_letter))\n+\t\tcfg.numbered = 1;\n+\tif (cfg.numbered)\n \t\trev.total = total + start_number - 1;\n \n \tif (idiff_prev.nr) {\n@@ -2365,27 +2419,40 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\t\t\t\t     _(\"Range-diff against v%d:\"));\n \t}\n \n+\t/*\n+\t * The order of precedence is:\n+\t *\n+\t *   1. The `--signature` and `--no-signature` options.\n+\t *   2. The `--signature-file` option.\n+\t *   3. The `format.signature` config.\n+\t *   4. The `format.signatureFile` config.\n+\t *   5. Default `git_version_string`.\n+\t */\n \tif (!signature) {\n \t\t; /* --no-signature inhibits all signatures */\n \t} else if (signature && signature != git_version_string) {\n \t\t; /* non-default signature already set */\n-\t} else if (signature_file) {\n+\t} else if (signature_file_arg || (cfg.signature_file && !cfg.signature)) {\n \t\tstruct strbuf buf = STRBUF_INIT;\n+\t\tconst char *signature_file = signature_file_arg ?\n+\t\t\tsignature_file_arg : cfg.signature_file;\n \n \t\tif (strbuf_read_file(&buf, signature_file, 128) < 0)\n \t\t\tdie_errno(_(\"unable to read signature file '%s'\"), signature_file);\n \t\tsignature = strbuf_detach(&buf, NULL);\n+\t} else if (cfg.signature) {\n+\t\tsignature = cfg.signature;\n \t}\n \n \tmemset(&bases, 0, sizeof(bases));\n-\tbase = get_base_commit(base_commit, list, nr);\n+\tbase = get_base_commit(&cfg, list, nr);\n \tif (base) {\n \t\treset_revision_walk();\n \t\tclear_object_flags(UNINTERESTING);\n \t\tprepare_bases(&bases, base, list, nr);\n \t}\n \n-\tif (in_reply_to || thread || cover_letter) {\n+\tif (in_reply_to || cfg.thread || cover_letter) {\n \t\trev.ref_message_ids = xmalloc(sizeof(*rev.ref_message_ids));\n \t\tstring_list_init_dup(rev.ref_message_ids);\n \t}\n@@ -2396,19 +2463,19 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \trev.numbered_files = just_numbers;\n \trev.patch_suffix = fmt_patch_suffix;\n \tif (cover_letter) {\n-\t\tif (thread)\n+\t\tif (cfg.thread)\n \t\t\tgen_message_id(&rev, \"cover\");\n \t\tmake_cover_letter(&rev, !!output_directory,\n-\t\t\t\t  origin, nr, list, description_file, branch_name, quiet);\n+\t\t\t\t  origin, nr, list, description_file, branch_name, quiet, &cfg);\n \t\tprint_bases(&bases, rev.diffopt.file);\n-\t\tprint_signature(rev.diffopt.file);\n+\t\tprint_signature(signature, rev.diffopt.file);\n \t\ttotal++;\n \t\tstart_number--;\n \t\t/* interdiff/range-diff in cover-letter; omit from patches */\n \t\trev.idiff_oid1 = NULL;\n \t\trev.rdiff1 = NULL;\n \t}\n-\trev.add_signoff = do_signoff;\n+\trev.add_signoff = cfg.do_signoff;\n \n \tif (show_progress)\n \t\tprogress = start_delayed_progress(_(\"Generating patches\"), total);\n@@ -2418,7 +2485,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\tcommit = list[nr];\n \t\trev.nr = total - nr + (start_number - 1);\n \t\t/* Make the second and subsequent mails replies to the first */\n-\t\tif (thread) {\n+\t\tif (cfg.thread) {\n \t\t\t/* Have we already had a message ID? */\n \t\t\tif (rev.message_id) {\n \t\t\t\t/*\n@@ -2442,7 +2509,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\t\t\t * letter is a reply to the\n \t\t\t\t * --in-reply-to, if specified.\n \t\t\t\t */\n-\t\t\t\tif (thread == THREAD_SHALLOW\n+\t\t\t\tif (cfg.thread == THREAD_SHALLOW\n \t\t\t\t    && rev.ref_message_ids->nr > 0\n \t\t\t\t    && (!cover_letter || rev.nr > 1))\n \t\t\t\t\tfree(rev.message_id);\n@@ -2475,7 +2542,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\t\t\t       mime_boundary_leader,\n \t\t\t\t       rev.mime_boundary);\n \t\t\telse\n-\t\t\t\tprint_signature(rev.diffopt.file);\n+\t\t\t\tprint_signature(signature, rev.diffopt.file);\n \t\t}\n \t\tif (output_directory)\n \t\t\tfclose(rev.diffopt.file);\n@@ -2483,9 +2550,6 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \tstop_progress(&progress);\n \tfree(list);\n \tfree(branch_name);\n-\tstring_list_clear(&extra_to, 0);\n-\tstring_list_clear(&extra_cc, 0);\n-\tstring_list_clear(&extra_hdr, 0);\n \tif (ignore_if_in_upstream)\n \t\tfree_patch_ids(&ids);\n \n@@ -2495,14 +2559,13 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \tstrbuf_release(&rdiff1);\n \tstrbuf_release(&rdiff2);\n \tstrbuf_release(&rdiff_title);\n-\tstrbuf_release(&sprefix);\n \tfree(to_free);\n \tfree(rev.message_id);\n \tif (rev.ref_message_ids)\n \t\tstring_list_clear(rev.ref_message_ids, 0);\n \tfree(rev.ref_message_ids);\n \trelease_revisions(&rev);\n-\tlog_config_release(&cfg);\n+\tformat_config_release(&cfg);\n \treturn 0;\n }\n \n-- \n2.45.1.246.gb9cfe4845c.dirty\n\n"},{"id":"495682","messageId":"a4fafcd079b8a5f45f0db1dce4a51d1195818f3b.1716810168.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716810168.git.ps@pks.im","subject":"[PATCH v3 12/21] config: clarify memory ownership in `git_config_string()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-27T11:46:39Z","receivedAt":"2024-05-27T11:46:45Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The out parameter of `git_config_string()` is a `const char **` even\nthough we transfer ownership of memory to the caller. This is quite\nmisleading and has led to many memory leaks all over the place. Adapt\nthe parameter to instead be `char **`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n alias.c                |  2 +-\n attr.c                 |  2 +-\n attr.h                 |  2 +-\n builtin/commit.c       |  2 +-\n builtin/log.c          | 12 ++++++------\n builtin/merge.c        |  4 ++--\n builtin/rebase.c       |  2 +-\n builtin/receive-pack.c |  2 +-\n builtin/repack.c       |  8 ++++----\n config.c               |  6 +++---\n config.h               |  2 +-\n convert.c              |  6 +++---\n delta-islands.c        |  2 +-\n diff.c                 |  8 ++++----\n environment.c          |  8 ++++----\n environment.h          |  8 ++++----\n gpg-interface.c        |  4 ++--\n http.c                 | 24 ++++++++++++------------\n imap-send.c            | 12 ++++++------\n mailmap.c              |  2 +-\n mailmap.h              |  2 +-\n merge-ll.c             |  6 +++---\n pager.c                |  2 +-\n pretty.c               | 14 +++++++++-----\n promisor-remote.h      |  2 +-\n remote.c               | 20 ++++++++++----------\n remote.h               |  8 ++++----\n sequencer.c            |  2 +-\n upload-pack.c          |  2 +-\n userdiff.h             | 12 ++++++------\n 30 files changed, 96 insertions(+), 92 deletions(-)\n\ndiff --git a/alias.c b/alias.c\nindex 5a238f2e30..269892c356 100644\n--- a/alias.c\n+++ b/alias.c\n@@ -22,7 +22,7 @@ static int config_alias_cb(const char *key, const char *value,\n \n \tif (data->alias) {\n \t\tif (!strcasecmp(p, data->alias))\n-\t\t\treturn git_config_string((const char **)&data->v,\n+\t\t\treturn git_config_string(&data->v,\n \t\t\t\t\t\t key, value);\n \t} else if (data->list) {\n \t\tstring_list_append(data->list, p);\ndiff --git a/attr.c b/attr.c\nindex 33473bdce0..5607db2bbe 100644\n--- a/attr.c\n+++ b/attr.c\n@@ -25,7 +25,7 @@\n #include \"tree-walk.h\"\n #include \"object-name.h\"\n \n-const char *git_attr_tree;\n+char *git_attr_tree;\n \n const char git_attr__true[] = \"(builtin)true\";\n const char git_attr__false[] = \"\\0(builtin)false\";\ndiff --git a/attr.h b/attr.h\nindex 127998ae01..e7cc318b0c 100644\n--- a/attr.h\n+++ b/attr.h\n@@ -236,6 +236,6 @@ const char *git_attr_global_file(void);\n /* Return whether the system gitattributes file is enabled and should be used. */\n int git_attr_system_is_enabled(void);\n \n-extern const char *git_attr_tree;\n+extern char *git_attr_tree;\n \n #endif /* ATTR_H */\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 1cc88e92bf..f53e7e86ff 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -133,7 +133,7 @@ static struct strvec trailer_args = STRVEC_INIT;\n  * is specified explicitly.\n  */\n static enum commit_msg_cleanup_mode cleanup_mode;\n-static const char *cleanup_arg;\n+static char *cleanup_arg;\n \n static enum commit_whence whence;\n static int use_editor = 1, include_status = 1;\ndiff --git a/builtin/log.c b/builtin/log.c\nindex 890bf0c425..517d7982f1 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -582,11 +582,11 @@ static int git_log_config(const char *var, const char *value,\n \n \tif (!strcmp(var, \"format.pretty\")) {\n \t\tFREE_AND_NULL(cfg->fmt_pretty);\n-\t\treturn git_config_string((const char **) &cfg->fmt_pretty, var, value);\n+\t\treturn git_config_string(&cfg->fmt_pretty, var, value);\n \t}\n \tif (!strcmp(var, \"format.subjectprefix\")) {\n \t\tFREE_AND_NULL(cfg->fmt_patch_subject_prefix);\n-\t\treturn git_config_string((const char **) &cfg->fmt_patch_subject_prefix, var, value);\n+\t\treturn git_config_string(&cfg->fmt_patch_subject_prefix, var, value);\n \t}\n \tif (!strcmp(var, \"format.filenamemaxlength\")) {\n \t\tcfg->fmt_patch_name_max = git_config_int(var, value, ctx->kvi);\n@@ -602,7 +602,7 @@ static int git_log_config(const char *var, const char *value,\n \t}\n \tif (!strcmp(var, \"log.date\")) {\n \t\tFREE_AND_NULL(cfg->default_date_mode);\n-\t\treturn git_config_string((const char **) &cfg->default_date_mode, var, value);\n+\t\treturn git_config_string(&cfg->default_date_mode, var, value);\n \t}\n \tif (!strcmp(var, \"log.decorate\")) {\n \t\tcfg->decoration_style = parse_decoration_style(value);\n@@ -1076,7 +1076,7 @@ static int git_format_config(const char *var, const char *value,\n \t}\n \tif (!strcmp(var, \"format.suffix\")) {\n \t\tFREE_AND_NULL(cfg->fmt_patch_suffix);\n-\t\treturn git_config_string((const char **) &cfg->fmt_patch_suffix, var, value);\n+\t\treturn git_config_string(&cfg->fmt_patch_suffix, var, value);\n \t}\n \tif (!strcmp(var, \"format.to\")) {\n \t\tif (!value)\n@@ -1133,7 +1133,7 @@ static int git_format_config(const char *var, const char *value,\n \t}\n \tif (!strcmp(var, \"format.signature\")) {\n \t\tFREE_AND_NULL(cfg->signature);\n-\t\treturn git_config_string((const char **) &cfg->signature, var, value);\n+\t\treturn git_config_string(&cfg->signature, var, value);\n \t}\n \tif (!strcmp(var, \"format.signaturefile\")) {\n \t\tFREE_AND_NULL(cfg->signature_file);\n@@ -1149,7 +1149,7 @@ static int git_format_config(const char *var, const char *value,\n \t}\n \tif (!strcmp(var, \"format.outputdirectory\")) {\n \t\tFREE_AND_NULL(cfg->config_output_directory);\n-\t\treturn git_config_string((const char **) &cfg->config_output_directory, var, value);\n+\t\treturn git_config_string(&cfg->config_output_directory, var, value);\n \t}\n \tif (!strcmp(var, \"format.useautobase\")) {\n \t\tif (value && !strcasecmp(value, \"whenAble\")) {\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex e4bd65eeba..daed2d4e1e 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -100,7 +100,7 @@ static struct strategy all_strategy[] = {\n \t{ \"subtree\",    NO_FAST_FORWARD | NO_TRIVIAL },\n };\n \n-static const char *pull_twohead, *pull_octopus;\n+static char *pull_twohead, *pull_octopus;\n \n enum ff_type {\n \tFF_NO,\n@@ -110,7 +110,7 @@ enum ff_type {\n \n static enum ff_type fast_forward = FF_ALLOW;\n \n-static const char *cleanup_arg;\n+static char *cleanup_arg;\n static enum commit_msg_cleanup_mode cleanup_mode;\n \n static int option_parse_message(const struct option *opt,\ndiff --git a/builtin/rebase.c b/builtin/rebase.c\nindex 0466d9414a..14d4f0a5e6 100644\n--- a/builtin/rebase.c\n+++ b/builtin/rebase.c\n@@ -83,7 +83,7 @@ static const char *action_names[] = {\n struct rebase_options {\n \tenum rebase_type type;\n \tenum empty_type empty;\n-\tconst char *default_backend;\n+\tchar *default_backend;\n \tconst char *state_dir;\n \tstruct commit *upstream;\n \tconst char *upstream_name;\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 56228ad314..01c1f04ece 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -88,7 +88,7 @@ static struct strbuf push_cert = STRBUF_INIT;\n static struct object_id push_cert_oid;\n static struct signature_check sigcheck;\n static const char *push_cert_nonce;\n-static const char *cert_nonce_seed;\n+static char *cert_nonce_seed;\n static struct strvec hidden_refs = STRVEC_INIT;\n \n static const char *NONCE_UNSOLICITED = \"UNSOLICITED\";\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex 43491a4cbf..e40dceaada 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -48,10 +48,10 @@ static const char incremental_bitmap_conflict_error[] = N_(\n );\n \n struct pack_objects_args {\n-\tconst char *window;\n-\tconst char *window_memory;\n-\tconst char *depth;\n-\tconst char *threads;\n+\tchar *window;\n+\tchar *window_memory;\n+\tchar *depth;\n+\tchar *threads;\n \tunsigned long max_pack_size;\n \tint no_reuse_delta;\n \tint no_reuse_object;\ndiff --git a/config.c b/config.c\nindex f9101045ee..da52a7f8a1 100644\n--- a/config.c\n+++ b/config.c\n@@ -1338,7 +1338,7 @@ int git_config_bool(const char *name, const char *value)\n \treturn v;\n }\n \n-int git_config_string(const char **dest, const char *var, const char *value)\n+int git_config_string(char **dest, const char *var, const char *value)\n {\n \tif (!value)\n \t\treturn config_error_nonbool(var);\n@@ -1566,7 +1566,7 @@ static int git_default_core_config(const char *var, const char *value,\n \n \tif (!strcmp(var, \"core.checkroundtripencoding\")) {\n \t\tFREE_AND_NULL(check_roundtrip_encoding);\n-\t\treturn git_config_string((const char **) &check_roundtrip_encoding, var, value);\n+\t\treturn git_config_string(&check_roundtrip_encoding, var, value);\n \t}\n \n \tif (!strcmp(var, \"core.notesref\")) {\n@@ -2418,7 +2418,7 @@ int git_configset_get_string(struct config_set *set, const char *key, char **des\n {\n \tconst char *value;\n \tif (!git_configset_get_value(set, key, &value, NULL))\n-\t\treturn git_config_string((const char **)dest, key, value);\n+\t\treturn git_config_string(dest, key, value);\n \telse\n \t\treturn 1;\n }\ndiff --git a/config.h b/config.h\nindex b3103bba94..2d2e22ed64 100644\n--- a/config.h\n+++ b/config.h\n@@ -280,7 +280,7 @@ int git_config_bool(const char *, const char *);\n  * Allocates and copies the value string into the `dest` parameter; if no\n  * string is given, prints an error message and returns -1.\n  */\n-int git_config_string(const char **, const char *, const char *);\n+int git_config_string(char **, const char *, const char *);\n \n /**\n  * Similar to `git_config_string`, but expands `~` or `~user` into the\ndiff --git a/convert.c b/convert.c\nindex 03c3c528f9..f2b9f01354 100644\n--- a/convert.c\n+++ b/convert.c\n@@ -981,9 +981,9 @@ int async_query_available_blobs(const char *cmd, struct string_list *available_p\n static struct convert_driver {\n \tconst char *name;\n \tstruct convert_driver *next;\n-\tconst char *smudge;\n-\tconst char *clean;\n-\tconst char *process;\n+\tchar *smudge;\n+\tchar *clean;\n+\tchar *process;\n \tint required;\n } *user_convert, **user_convert_tail;\n \ndiff --git a/delta-islands.c b/delta-islands.c\nindex 4ac3c10551..89d51b72e3 100644\n--- a/delta-islands.c\n+++ b/delta-islands.c\n@@ -313,7 +313,7 @@ struct island_load_data {\n \tsize_t nr;\n \tsize_t alloc;\n };\n-static const char *core_island_name;\n+static char *core_island_name;\n \n static void free_config_regexes(struct island_load_data *ild)\n {\ndiff --git a/diff.c b/diff.c\nindex 679ef472f4..e70301df76 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -56,8 +56,8 @@ static int diff_color_moved_default;\n static int diff_color_moved_ws_default;\n static int diff_context_default = 3;\n static int diff_interhunk_context_default;\n-static const char *diff_word_regex_cfg;\n-static const char *external_diff_cmd_cfg;\n+static char *diff_word_regex_cfg;\n+static char *external_diff_cmd_cfg;\n static char *diff_order_file_cfg;\n int diff_auto_refresh_index = 1;\n static int diff_mnemonic_prefix;\n@@ -412,11 +412,11 @@ int git_diff_ui_config(const char *var, const char *value,\n \t}\n \tif (!strcmp(var, \"diff.srcprefix\")) {\n \t\tFREE_AND_NULL(diff_src_prefix);\n-\t\treturn git_config_string((const char **) &diff_src_prefix, var, value);\n+\t\treturn git_config_string(&diff_src_prefix, var, value);\n \t}\n \tif (!strcmp(var, \"diff.dstprefix\")) {\n \t\tFREE_AND_NULL(diff_dst_prefix);\n-\t\treturn git_config_string((const char **) &diff_dst_prefix, var, value);\n+\t\treturn git_config_string(&diff_dst_prefix, var, value);\n \t}\n \tif (!strcmp(var, \"diff.relative\")) {\n \t\tdiff_relative = git_config_bool(var, value);\ndiff --git a/environment.c b/environment.c\nindex ab6956559e..701d515135 100644\n--- a/environment.c\n+++ b/environment.c\n@@ -42,8 +42,8 @@ int is_bare_repository_cfg = -1; /* unspecified */\n int warn_ambiguous_refs = 1;\n int warn_on_object_refname_ambiguity = 1;\n int repository_format_precious_objects;\n-const char *git_commit_encoding;\n-const char *git_log_output_encoding;\n+char *git_commit_encoding;\n+char *git_log_output_encoding;\n char *apply_default_whitespace;\n char *apply_default_ignorewhitespace;\n char *git_attributes_file;\n@@ -58,8 +58,8 @@ size_t packed_git_window_size = DEFAULT_PACKED_GIT_WINDOW_SIZE;\n size_t packed_git_limit = DEFAULT_PACKED_GIT_LIMIT;\n size_t delta_base_cache_limit = 96 * 1024 * 1024;\n unsigned long big_file_threshold = 512 * 1024 * 1024;\n-const char *editor_program;\n-const char *askpass_program;\n+char *editor_program;\n+char *askpass_program;\n char *excludes_file;\n enum auto_crlf auto_crlf = AUTO_CRLF_FALSE;\n enum eol core_eol = EOL_UNSET;\ndiff --git a/environment.h b/environment.h\nindex be1b88ad6f..e9f01d4d11 100644\n--- a/environment.h\n+++ b/environment.h\n@@ -224,11 +224,11 @@ int odb_pack_keep(const char *name);\n const char *get_log_output_encoding(void);\n const char *get_commit_output_encoding(void);\n \n-extern const char *git_commit_encoding;\n-extern const char *git_log_output_encoding;\n+extern char *git_commit_encoding;\n+extern char *git_log_output_encoding;\n \n-extern const char *editor_program;\n-extern const char *askpass_program;\n+extern char *editor_program;\n+extern char *askpass_program;\n extern char *excludes_file;\n \n /*\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 2b50ed0fa0..5193223714 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -27,14 +27,14 @@ static void gpg_interface_lazy_init(void)\n }\n \n static char *configured_signing_key;\n-static const char *ssh_default_key_command;\n+static char *ssh_default_key_command;\n static char *ssh_allowed_signers;\n static char *ssh_revocation_file;\n static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;\n \n struct gpg_format {\n \tconst char *name;\n-\tconst char *program;\n+\tchar *program;\n \tconst char **verify_args;\n \tconst char **sigs;\n \tint (*verify_signed_buffer)(struct signature_check *sigc,\ndiff --git a/http.c b/http.c\nindex fa3ea87451..67cc47d28f 100644\n--- a/http.c\n+++ b/http.c\n@@ -38,11 +38,11 @@ char curl_errorstr[CURL_ERROR_SIZE];\n \n static int curl_ssl_verify = -1;\n static int curl_ssl_try;\n-static const char *curl_http_version = NULL;\n+static char *curl_http_version;\n static char *ssl_cert;\n static char *ssl_cert_type;\n-static const char *ssl_cipherlist;\n-static const char *ssl_version;\n+static char *ssl_cipherlist;\n+static char *ssl_version;\n static struct {\n \tconst char *name;\n \tlong ssl_version;\n@@ -95,7 +95,7 @@ static struct {\n \t */\n };\n #ifdef CURLGSSAPI_DELEGATION_FLAG\n-static const char *curl_deleg;\n+static char *curl_deleg;\n static struct {\n \tconst char *name;\n \tlong curl_deleg_param;\n@@ -383,11 +383,11 @@ static int http_options(const char *var, const char *value,\n \tif (!strcmp(\"http.sslcert\", var))\n \t\treturn git_config_pathname(&ssl_cert, var, value);\n \tif (!strcmp(\"http.sslcerttype\", var))\n-\t\treturn git_config_string((const char **)&ssl_cert_type, var, value);\n+\t\treturn git_config_string(&ssl_cert_type, var, value);\n \tif (!strcmp(\"http.sslkey\", var))\n \t\treturn git_config_pathname(&ssl_key, var, value);\n \tif (!strcmp(\"http.sslkeytype\", var))\n-\t\treturn git_config_string((const char **)&ssl_key_type, var, value);\n+\t\treturn git_config_string(&ssl_key_type, var, value);\n \tif (!strcmp(\"http.sslcapath\", var))\n \t\treturn git_config_pathname(&ssl_capath, var, value);\n \tif (!strcmp(\"http.sslcainfo\", var))\n@@ -440,19 +440,19 @@ static int http_options(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \tif (!strcmp(\"http.proxy\", var))\n-\t\treturn git_config_string((const char **)&curl_http_proxy, var, value);\n+\t\treturn git_config_string(&curl_http_proxy, var, value);\n \n \tif (!strcmp(\"http.proxyauthmethod\", var))\n-\t\treturn git_config_string((const char **)&http_proxy_authmethod, var, value);\n+\t\treturn git_config_string(&http_proxy_authmethod, var, value);\n \n \tif (!strcmp(\"http.proxysslcert\", var))\n-\t\treturn git_config_string((const char **)&http_proxy_ssl_cert, var, value);\n+\t\treturn git_config_string(&http_proxy_ssl_cert, var, value);\n \n \tif (!strcmp(\"http.proxysslkey\", var))\n-\t\treturn git_config_string((const char **)&http_proxy_ssl_key, var, value);\n+\t\treturn git_config_string(&http_proxy_ssl_key, var, value);\n \n \tif (!strcmp(\"http.proxysslcainfo\", var))\n-\t\treturn git_config_string((const char **)&http_proxy_ssl_ca_info, var, value);\n+\t\treturn git_config_string(&http_proxy_ssl_ca_info, var, value);\n \n \tif (!strcmp(\"http.proxysslcertpasswordprotected\", var)) {\n \t\tproxy_ssl_cert_password_required = git_config_bool(var, value);\n@@ -476,7 +476,7 @@ static int http_options(const char *var, const char *value,\n \t}\n \n \tif (!strcmp(\"http.useragent\", var))\n-\t\treturn git_config_string((const char **)&user_agent, var, value);\n+\t\treturn git_config_string(&user_agent, var, value);\n \n \tif (!strcmp(\"http.emptyauth\", var)) {\n \t\tif (value && !strcmp(\"auto\", value))\ndiff --git a/imap-send.c b/imap-send.c\nindex c0130d0e02..a5d1510180 100644\n--- a/imap-send.c\n+++ b/imap-send.c\n@@ -70,16 +70,16 @@ static char *next_arg(char **);\n \n struct imap_server_conf {\n \tconst char *name;\n-\tconst char *tunnel;\n-\tconst char *host;\n+\tchar *tunnel;\n+\tchar *host;\n \tint port;\n-\tconst char *folder;\n-\tconst char *user;\n-\tconst char *pass;\n+\tchar *folder;\n+\tchar *user;\n+\tchar *pass;\n \tint use_ssl;\n \tint ssl_verify;\n \tint use_html;\n-\tconst char *auth_method;\n+\tchar *auth_method;\n };\n \n static struct imap_server_conf server = {\ndiff --git a/mailmap.c b/mailmap.c\nindex 044466b043..b2efe29b3d 100644\n--- a/mailmap.c\n+++ b/mailmap.c\n@@ -7,7 +7,7 @@\n #include \"setup.h\"\n \n char *git_mailmap_file;\n-const char *git_mailmap_blob;\n+char *git_mailmap_blob;\n \n struct mailmap_info {\n \tchar *name;\ndiff --git a/mailmap.h b/mailmap.h\nindex 429a760945..cbda9bc5e0 100644\n--- a/mailmap.h\n+++ b/mailmap.h\n@@ -4,7 +4,7 @@\n struct string_list;\n \n extern char *git_mailmap_file;\n-extern const char *git_mailmap_blob;\n+extern char *git_mailmap_blob;\n \n int read_mailmap(struct string_list *map);\n void clear_mailmap(struct string_list *map);\ndiff --git a/merge-ll.c b/merge-ll.c\nindex bf1077ae09..e29b15fa4a 100644\n--- a/merge-ll.c\n+++ b/merge-ll.c\n@@ -27,9 +27,9 @@ typedef enum ll_merge_result (*ll_merge_fn)(const struct ll_merge_driver *,\n \n struct ll_merge_driver {\n \tconst char *name;\n-\tconst char *description;\n+\tchar *description;\n \tll_merge_fn fn;\n-\tconst char *recursive;\n+\tchar *recursive;\n \tstruct ll_merge_driver *next;\n \tchar *cmdline;\n };\n@@ -268,7 +268,7 @@ static enum ll_merge_result ll_ext_merge(const struct ll_merge_driver *fn,\n  * merge.default and merge.driver configuration items\n  */\n static struct ll_merge_driver *ll_user_merge, **ll_user_merge_tail;\n-static const char *default_ll_merge;\n+static char *default_ll_merge;\n \n static int read_merge_config(const char *var, const char *value,\n \t\t\t     const struct config_context *ctx UNUSED,\ndiff --git a/pager.c b/pager.c\nindex b8822a9381..e9e121db69 100644\n--- a/pager.c\n+++ b/pager.c\n@@ -13,7 +13,7 @@ int pager_use_color = 1;\n #endif\n \n static struct child_process pager_process;\n-static const char *pager_program;\n+static char *pager_program;\n \n /* Is the value coming back from term_columns() just a guess? */\n static int term_columns_guessed;\ndiff --git a/pretty.c b/pretty.c\nindex 7ead078998..22a81506b7 100644\n--- a/pretty.c\n+++ b/pretty.c\n@@ -62,7 +62,7 @@ static int git_pretty_formats_config(const char *var, const char *value,\n {\n \tstruct cmt_fmt_map *commit_format = NULL;\n \tconst char *name;\n-\tconst char *fmt;\n+\tchar *fmt;\n \tint i;\n \n \tif (!skip_prefix(var, \"pretty.\", &name))\n@@ -93,13 +93,17 @@ static int git_pretty_formats_config(const char *var, const char *value,\n \tif (git_config_string(&fmt, var, value))\n \t\treturn -1;\n \n-\tif (skip_prefix(fmt, \"format:\", &fmt))\n+\tif (skip_prefix(fmt, \"format:\", &commit_format->user_format)) {\n \t\tcommit_format->is_tformat = 0;\n-\telse if (skip_prefix(fmt, \"tformat:\", &fmt) || strchr(fmt, '%'))\n+\t} else if (skip_prefix(fmt, \"tformat:\", &commit_format->user_format)) {\n \t\tcommit_format->is_tformat = 1;\n-\telse\n+\t} else if (strchr(fmt, '%')) {\n+\t\tcommit_format->is_tformat = 1;\n+\t\tcommit_format->user_format = fmt;\n+\t} else {\n \t\tcommit_format->is_alias = 1;\n-\tcommit_format->user_format = fmt;\n+\t\tcommit_format->user_format = fmt;\n+\t}\n \n \treturn 0;\n }\ndiff --git a/promisor-remote.h b/promisor-remote.h\nindex 2cb9eda9ea..88cb599c39 100644\n--- a/promisor-remote.h\n+++ b/promisor-remote.h\n@@ -13,7 +13,7 @@ struct object_id;\n  */\n struct promisor_remote {\n \tstruct promisor_remote *next;\n-\tconst char *partial_clone_filter;\n+\tchar *partial_clone_filter;\n \tconst char name[FLEX_ARRAY];\n };\n \ndiff --git a/remote.c b/remote.c\nindex ec8c158e60..d319f28757 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -428,29 +428,29 @@ static int handle_config(const char *key, const char *value,\n \telse if (!strcmp(subkey, \"prunetags\"))\n \t\tremote->prune_tags = git_config_bool(key, value);\n \telse if (!strcmp(subkey, \"url\")) {\n-\t\tconst char *v;\n+\t\tchar *v;\n \t\tif (git_config_string(&v, key, value))\n \t\t\treturn -1;\n \t\tadd_url(remote, v);\n \t} else if (!strcmp(subkey, \"pushurl\")) {\n-\t\tconst char *v;\n+\t\tchar *v;\n \t\tif (git_config_string(&v, key, value))\n \t\t\treturn -1;\n \t\tadd_pushurl(remote, v);\n \t} else if (!strcmp(subkey, \"push\")) {\n-\t\tconst char *v;\n+\t\tchar *v;\n \t\tif (git_config_string(&v, key, value))\n \t\t\treturn -1;\n \t\trefspec_append(&remote->push, v);\n-\t\tfree((char *)v);\n+\t\tfree(v);\n \t} else if (!strcmp(subkey, \"fetch\")) {\n-\t\tconst char *v;\n+\t\tchar *v;\n \t\tif (git_config_string(&v, key, value))\n \t\t\treturn -1;\n \t\trefspec_append(&remote->fetch, v);\n-\t\tfree((char *)v);\n+\t\tfree(v);\n \t} else if (!strcmp(subkey, \"receivepack\")) {\n-\t\tconst char *v;\n+\t\tchar *v;\n \t\tif (git_config_string(&v, key, value))\n \t\t\treturn -1;\n \t\tif (!remote->receivepack)\n@@ -458,7 +458,7 @@ static int handle_config(const char *key, const char *value,\n \t\telse\n \t\t\terror(_(\"more than one receivepack given, using the first\"));\n \t} else if (!strcmp(subkey, \"uploadpack\")) {\n-\t\tconst char *v;\n+\t\tchar *v;\n \t\tif (git_config_string(&v, key, value))\n \t\t\treturn -1;\n \t\tif (!remote->uploadpack)\n@@ -471,10 +471,10 @@ static int handle_config(const char *key, const char *value,\n \t\telse if (!strcmp(value, \"--tags\"))\n \t\t\tremote->fetch_tags = 2;\n \t} else if (!strcmp(subkey, \"proxy\")) {\n-\t\treturn git_config_string((const char **)&remote->http_proxy,\n+\t\treturn git_config_string(&remote->http_proxy,\n \t\t\t\t\t key, value);\n \t} else if (!strcmp(subkey, \"proxyauthmethod\")) {\n-\t\treturn git_config_string((const char **)&remote->http_proxy_authmethod,\n+\t\treturn git_config_string(&remote->http_proxy_authmethod,\n \t\t\t\t\t key, value);\n \t} else if (!strcmp(subkey, \"vcs\")) {\n \t\treturn git_config_string(&remote->foreign_vcs, key, value);\ndiff --git a/remote.h b/remote.h\nindex dfd4837e60..e8c6655e42 100644\n--- a/remote.h\n+++ b/remote.h\n@@ -46,7 +46,7 @@ struct remote_state {\n \tstruct hashmap branches_hash;\n \n \tstruct branch *current_branch;\n-\tconst char *pushremote_name;\n+\tchar *pushremote_name;\n \n \tstruct rewrites rewrites;\n \tstruct rewrites rewrites_push;\n@@ -65,7 +65,7 @@ struct remote {\n \n \tint origin, configured_in_repo;\n \n-\tconst char *foreign_vcs;\n+\tchar *foreign_vcs;\n \n \t/* An array of all of the url_nr URLs configured for the remote */\n \tconst char **url;\n@@ -309,9 +309,9 @@ struct branch {\n \tconst char *refname;\n \n \t/* The name of the remote listed in the configuration. */\n-\tconst char *remote_name;\n+\tchar *remote_name;\n \n-\tconst char *pushremote_name;\n+\tchar *pushremote_name;\n \n \t/* An array of the \"merge\" lines in the configuration. */\n \tconst char **merge_name;\ndiff --git a/sequencer.c b/sequencer.c\nindex dbd60d79b9..3c81ef9ca5 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -306,7 +306,7 @@ static int git_sequencer_config(const char *k, const char *v,\n \t}\n \n \tif (!opts->default_strategy && !strcmp(k, \"pull.twohead\")) {\n-\t\tint ret = git_config_string((const char**)&opts->default_strategy, k, v);\n+\t\tint ret = git_config_string(&opts->default_strategy, k, v);\n \t\tif (ret == 0) {\n \t\t\t/*\n \t\t\t * pull.twohead is allowed to be multi-valued; we only\ndiff --git a/upload-pack.c b/upload-pack.c\nindex 8fbd138515..5801eb2639 100644\n--- a/upload-pack.c\n+++ b/upload-pack.c\n@@ -94,7 +94,7 @@ struct upload_pack_data {\n \n \tstruct packet_writer writer;\n \n-\tconst char *pack_objects_hook;\n+\tchar *pack_objects_hook;\n \n \tunsigned stateless_rpc : 1;\t\t\t\t/* v0 only */\n \tunsigned no_done : 1;\t\t\t\t\t/* v0 only */\ndiff --git a/userdiff.h b/userdiff.h\nindex d726804c3e..cc8e5abfef 100644\n--- a/userdiff.h\n+++ b/userdiff.h\n@@ -7,19 +7,19 @@ struct index_state;\n struct repository;\n \n struct userdiff_funcname {\n-\tconst char *pattern;\n+\tchar *pattern;\n \tint cflags;\n };\n \n struct userdiff_driver {\n \tconst char *name;\n-\tconst char *external;\n-\tconst char *algorithm;\n+\tchar *external;\n+\tchar *algorithm;\n \tint binary;\n \tstruct userdiff_funcname funcname;\n-\tconst char *word_regex;\n-\tconst char *word_regex_multi_byte;\n-\tconst char *textconv;\n+\tchar *word_regex;\n+\tchar *word_regex_multi_byte;\n+\tchar *textconv;\n \tstruct notes_cache *textconv_cache;\n \tint textconv_want_cache;\n };\n-- \n2.45.1.246.gb9cfe4845c.dirty\n\n"},{"id":"495683","messageId":"8b74dff67874a6c1c8d1cd9c8207db046bca985a.1716810168.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716810168.git.ps@pks.im","subject":"[PATCH v3 13/21] config: plug various memory leaks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-27T11:46:44Z","receivedAt":"2024-05-27T11:46:49Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Now that memory ownership rules around `git_config_string()` and\n`git_config_pathname()` are clearer, it also got easier to spot that\nthe returned memory needs to be free'd. Plug a subset of those cases and\nmark now-passing tests as leak free.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n alias.c                                      |  4 ++-\n config.c                                     | 36 +++++++++++++++-----\n t/t1306-xdg-files.sh                         |  1 +\n t/t1350-config-hooks-path.sh                 |  1 +\n t/t3415-rebase-autosquash.sh                 |  1 +\n t/t4041-diff-submodule-option.sh             |  1 +\n t/t4060-diff-submodule-option-diff-format.sh |  1 +\n t/t4210-log-i18n.sh                          |  2 ++\n t/t6006-rev-list-format.sh                   |  1 +\n t/t7005-editor.sh                            |  1 +\n t/t7102-reset.sh                             |  1 +\n t/t9129-git-svn-i18n-commitencoding.sh       |  1 -\n t/t9139-git-svn-non-utf8-commitencoding.sh   |  1 -\n 13 files changed, 40 insertions(+), 12 deletions(-)\n\ndiff --git a/alias.c b/alias.c\nindex 269892c356..4daafd9bda 100644\n--- a/alias.c\n+++ b/alias.c\n@@ -21,9 +21,11 @@ static int config_alias_cb(const char *key, const char *value,\n \t\treturn 0;\n \n \tif (data->alias) {\n-\t\tif (!strcasecmp(p, data->alias))\n+\t\tif (!strcasecmp(p, data->alias)) {\n+\t\t\tFREE_AND_NULL(data->v);\n \t\t\treturn git_config_string(&data->v,\n \t\t\t\t\t\t key, value);\n+\t\t}\n \t} else if (data->list) {\n \t\tstring_list_append(data->list, p);\n \t}\ndiff --git a/config.c b/config.c\nindex da52a7f8a1..496cd1a61e 100644\n--- a/config.c\n+++ b/config.c\n@@ -1414,8 +1414,10 @@ static int git_default_core_config(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \n-\tif (!strcmp(var, \"core.attributesfile\"))\n+\tif (!strcmp(var, \"core.attributesfile\")) {\n+\t\tFREE_AND_NULL(git_attributes_file);\n \t\treturn git_config_pathname(&git_attributes_file, var, value);\n+\t}\n \n \tif (!strcmp(var, \"core.hookspath\")) {\n \t\tif (ctx->kvi && ctx->kvi->scope == CONFIG_SCOPE_LOCAL &&\n@@ -1428,6 +1430,7 @@ static int git_default_core_config(const char *var, const char *value,\n \t\t\t      \"again with \"\n \t\t\t      \"`GIT_CLONE_PROTECTION_ACTIVE=false`\"),\n \t\t\t    value);\n+\t\tFREE_AND_NULL(git_hooks_path);\n \t\treturn git_config_pathname(&git_hooks_path, var, value);\n \t}\n \n@@ -1576,8 +1579,10 @@ static int git_default_core_config(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \n-\tif (!strcmp(var, \"core.editor\"))\n+\tif (!strcmp(var, \"core.editor\")) {\n+\t\tFREE_AND_NULL(editor_program);\n \t\treturn git_config_string(&editor_program, var, value);\n+\t}\n \n \tif (!strcmp(var, \"core.commentchar\") ||\n \t    !strcmp(var, \"core.commentstring\")) {\n@@ -1595,11 +1600,13 @@ static int git_default_core_config(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \n-\tif (!strcmp(var, \"core.askpass\"))\n+\tif (!strcmp(var, \"core.askpass\")) {\n+\t\tFREE_AND_NULL(askpass_program);\n \t\treturn git_config_string(&askpass_program, var, value);\n+\t}\n \n \tif (!strcmp(var, \"core.excludesfile\")) {\n-\t\tfree(excludes_file);\n+\t\tFREE_AND_NULL(excludes_file);\n \t\treturn git_config_pathname(&excludes_file, var, value);\n \t}\n \n@@ -1702,11 +1709,15 @@ static int git_default_sparse_config(const char *var, const char *value)\n \n static int git_default_i18n_config(const char *var, const char *value)\n {\n-\tif (!strcmp(var, \"i18n.commitencoding\"))\n+\tif (!strcmp(var, \"i18n.commitencoding\")) {\n+\t\tFREE_AND_NULL(git_commit_encoding);\n \t\treturn git_config_string(&git_commit_encoding, var, value);\n+\t}\n \n-\tif (!strcmp(var, \"i18n.logoutputencoding\"))\n+\tif (!strcmp(var, \"i18n.logoutputencoding\")) {\n+\t\tFREE_AND_NULL(git_log_output_encoding);\n \t\treturn git_config_string(&git_log_output_encoding, var, value);\n+\t}\n \n \t/* Add other config variables here and to Documentation/config.txt. */\n \treturn 0;\n@@ -1779,10 +1790,15 @@ static int git_default_push_config(const char *var, const char *value)\n \n static int git_default_mailmap_config(const char *var, const char *value)\n {\n-\tif (!strcmp(var, \"mailmap.file\"))\n+\tif (!strcmp(var, \"mailmap.file\")) {\n+\t\tFREE_AND_NULL(git_mailmap_file);\n \t\treturn git_config_pathname(&git_mailmap_file, var, value);\n-\tif (!strcmp(var, \"mailmap.blob\"))\n+\t}\n+\n+\tif (!strcmp(var, \"mailmap.blob\")) {\n+\t\tFREE_AND_NULL(git_mailmap_blob);\n \t\treturn git_config_string(&git_mailmap_blob, var, value);\n+\t}\n \n \t/* Add other config variables here and to Documentation/config.txt. */\n \treturn 0;\n@@ -1790,8 +1806,10 @@ static int git_default_mailmap_config(const char *var, const char *value)\n \n static int git_default_attr_config(const char *var, const char *value)\n {\n-\tif (!strcmp(var, \"attr.tree\"))\n+\tif (!strcmp(var, \"attr.tree\")) {\n+\t\tFREE_AND_NULL(git_attr_tree);\n \t\treturn git_config_string(&git_attr_tree, var, value);\n+\t}\n \n \t/*\n \t * Add other attribute related config variables here and to\ndiff --git a/t/t1306-xdg-files.sh b/t/t1306-xdg-files.sh\nindex 40d3c42618..53e5b290b9 100755\n--- a/t/t1306-xdg-files.sh\n+++ b/t/t1306-xdg-files.sh\n@@ -7,6 +7,7 @@\n \n test_description='Compatibility with $XDG_CONFIG_HOME/git/ files'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n test_expect_success 'read config: xdg file exists and ~/.gitconfig doesn'\\''t' '\ndiff --git a/t/t1350-config-hooks-path.sh b/t/t1350-config-hooks-path.sh\nindex f6dc83e2aa..5c47f9ecc3 100755\n--- a/t/t1350-config-hooks-path.sh\n+++ b/t/t1350-config-hooks-path.sh\n@@ -2,6 +2,7 @@\n \n test_description='Test the core.hooksPath configuration variable'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n test_expect_success 'set up a pre-commit hook in core.hooksPath' '\ndiff --git a/t/t3415-rebase-autosquash.sh b/t/t3415-rebase-autosquash.sh\nindex fcc40d6fe1..22452ff84c 100755\n--- a/t/t3415-rebase-autosquash.sh\n+++ b/t/t3415-rebase-autosquash.sh\n@@ -5,6 +5,7 @@ test_description='auto squash'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n . \"$TEST_DIRECTORY\"/lib-rebase.sh\ndiff --git a/t/t4041-diff-submodule-option.sh b/t/t4041-diff-submodule-option.sh\nindex 0c1502d4b0..8fc40e75eb 100755\n--- a/t/t4041-diff-submodule-option.sh\n+++ b/t/t4041-diff-submodule-option.sh\n@@ -12,6 +12,7 @@ This test tries to verify the sanity of the --submodule option of git diff.\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n # Tested non-UTF-8 encoding\ndiff --git a/t/t4060-diff-submodule-option-diff-format.sh b/t/t4060-diff-submodule-option-diff-format.sh\nindex 97c6424cd5..8ce67442d9 100755\n--- a/t/t4060-diff-submodule-option-diff-format.sh\n+++ b/t/t4060-diff-submodule-option-diff-format.sh\n@@ -10,6 +10,7 @@ test_description='Support for diff format verbose submodule difference in git di\n This test tries to verify the sanity of --submodule=diff option of git diff.\n '\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n # Tested non-UTF-8 encoding\ndiff --git a/t/t4210-log-i18n.sh b/t/t4210-log-i18n.sh\nindex 75216f19ce..7120030b5c 100755\n--- a/t/t4210-log-i18n.sh\n+++ b/t/t4210-log-i18n.sh\n@@ -1,6 +1,8 @@\n #!/bin/sh\n \n test_description='test log with i18n features'\n+\n+TEST_PASSES_SANITIZE_LEAK=true\n . ./lib-gettext.sh\n \n # two forms of é\ndiff --git a/t/t6006-rev-list-format.sh b/t/t6006-rev-list-format.sh\nindex 573eb97a0f..f1623b1c06 100755\n--- a/t/t6006-rev-list-format.sh\n+++ b/t/t6006-rev-list-format.sh\n@@ -8,6 +8,7 @@ test_description='git rev-list --pretty=format test'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-terminal.sh\n \ndiff --git a/t/t7005-editor.sh b/t/t7005-editor.sh\nindex 5fcf281dfb..b9822294fe 100755\n--- a/t/t7005-editor.sh\n+++ b/t/t7005-editor.sh\n@@ -2,6 +2,7 @@\n \n test_description='GIT_EDITOR, core.editor, and stuff'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n unset EDITOR VISUAL GIT_EDITOR\ndiff --git a/t/t7102-reset.sh b/t/t7102-reset.sh\nindex 62d9f846ce..2add26d768 100755\n--- a/t/t7102-reset.sh\n+++ b/t/t7102-reset.sh\n@@ -10,6 +10,7 @@ Documented tests for git reset'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n commit_msg () {\ndiff --git a/t/t9129-git-svn-i18n-commitencoding.sh b/t/t9129-git-svn-i18n-commitencoding.sh\nindex 185248a4cd..01e1e8a8f7 100755\n--- a/t/t9129-git-svn-i18n-commitencoding.sh\n+++ b/t/t9129-git-svn-i18n-commitencoding.sh\n@@ -4,7 +4,6 @@\n \n test_description='git svn honors i18n.commitEncoding in config'\n \n-TEST_FAILS_SANITIZE_LEAK=true\n . ./lib-git-svn.sh\n \n compare_git_head_with () {\ndiff --git a/t/t9139-git-svn-non-utf8-commitencoding.sh b/t/t9139-git-svn-non-utf8-commitencoding.sh\nindex b7f756b2b7..22d80b0be2 100755\n--- a/t/t9139-git-svn-non-utf8-commitencoding.sh\n+++ b/t/t9139-git-svn-non-utf8-commitencoding.sh\n@@ -4,7 +4,6 @@\n \n test_description='git svn refuses to dcommit non-UTF8 messages'\n \n-TEST_FAILS_SANITIZE_LEAK=true\n . ./lib-git-svn.sh\n \n # ISO-2022-JP can pass for valid UTF-8, so skipping that in this test\n-- \n2.45.1.246.gb9cfe4845c.dirty\n\n"},{"id":"495684","messageId":"265665fe6cb7dc56bb637624910369f2bd0525e8.1716810168.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716810168.git.ps@pks.im","subject":"[PATCH v3 14/21] builtin/credential: clear credential before exit","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-27T11:46:49Z","receivedAt":"2024-05-27T11:46:54Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We never release memory associated with `struct credential`. Fix this\nand mark the corresponding test as leak free.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/credential.c   | 2 ++\n t/t0300-credentials.sh | 2 ++\n 2 files changed, 4 insertions(+)\n\ndiff --git a/builtin/credential.c b/builtin/credential.c\nindex 5100d441f2..b72e76dd9a 100644\n--- a/builtin/credential.c\n+++ b/builtin/credential.c\n@@ -39,5 +39,7 @@ int cmd_credential(int argc, const char **argv, const char *prefix UNUSED)\n \t} else {\n \t\tusage(usage_msg);\n \t}\n+\n+\tcredential_clear(&c);\n \treturn 0;\n }\ndiff --git a/t/t0300-credentials.sh b/t/t0300-credentials.sh\nindex 432f029d48..6a76b7fdbd 100755\n--- a/t/t0300-credentials.sh\n+++ b/t/t0300-credentials.sh\n@@ -1,6 +1,8 @@\n #!/bin/sh\n \n test_description='basic credential helper tests'\n+\n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-credential.sh\n \n-- \n2.45.1.246.gb9cfe4845c.dirty\n\n"},{"id":"495685","messageId":"b315a5bb5cbf03180965e79f129f44ec7deb0eb5.1716810168.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716810168.git.ps@pks.im","subject":"[PATCH v3 15/21] commit-reach: fix memory leak in `ahead_behind()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-27T11:46:54Z","receivedAt":"2024-05-27T11:46:59Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We use a priority queue in `ahead_behind()` to compute the ahead/behind\ncount for commits. We may not iterate through all commits part of that\nqueue though in case all of its entries are stale. Consequently, as we\nnever make the effort to release the remaining commits, we end up\nleaking bit arrays that we have allocated for each of the contained\ncommits.\n\nPlug this leak and mark the corresponding test as leak free.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n commit-reach.c           | 4 ++++\n t/t3203-branch-output.sh | 2 ++\n 2 files changed, 6 insertions(+)\n\ndiff --git a/commit-reach.c b/commit-reach.c\nindex 8f9b008f87..384aee1ab3 100644\n--- a/commit-reach.c\n+++ b/commit-reach.c\n@@ -1106,6 +1106,10 @@ void ahead_behind(struct repository *r,\n \n \t/* STALE is used here, PARENT2 is used by insert_no_dup(). */\n \trepo_clear_commit_marks(r, PARENT2 | STALE);\n+\twhile (prio_queue_peek(&queue)) {\n+\t\tstruct commit *c = prio_queue_get(&queue);\n+\t\tfree_bit_array(c);\n+\t}\n \tclear_bit_arrays(&bit_arrays);\n \tclear_prio_queue(&queue);\n }\ndiff --git a/t/t3203-branch-output.sh b/t/t3203-branch-output.sh\nindex 758963b189..e627f08a17 100755\n--- a/t/t3203-branch-output.sh\n+++ b/t/t3203-branch-output.sh\n@@ -1,6 +1,8 @@\n #!/bin/sh\n \n test_description='git branch display tests'\n+\n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-terminal.sh\n \n-- \n2.45.1.246.gb9cfe4845c.dirty\n\n"},{"id":"495686","messageId":"7c75a94756c676a92a8dc719f270c692dc5a5c18.1716810168.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716810168.git.ps@pks.im","subject":"[PATCH v3 16/21] submodule: fix leaking memory for submodule entries","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-27T11:46:59Z","receivedAt":"2024-05-27T11:47:04Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"In `free_one_config()` we never end up freeing the `url` and `ignore`\nfields and thus leak memory. Fix those leaks and mark now-passing tests\nas leak free.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n submodule-config.c                     | 2 ++\n t/t1013-read-tree-submodule.sh         | 1 +\n t/t2013-checkout-submodule.sh          | 1 +\n t/t3007-ls-files-recurse-submodules.sh | 1 +\n t/t7112-reset-submodule.sh             | 1 +\n 5 files changed, 6 insertions(+)\n\ndiff --git a/submodule-config.c b/submodule-config.c\nindex 11428b4ada..ec45ea67b9 100644\n--- a/submodule-config.c\n+++ b/submodule-config.c\n@@ -91,6 +91,8 @@ static void free_one_config(struct submodule_entry *entry)\n \tfree((void *) entry->config->path);\n \tfree((void *) entry->config->name);\n \tfree((void *) entry->config->branch);\n+\tfree((void *) entry->config->url);\n+\tfree((void *) entry->config->ignore);\n \tfree((void *) entry->config->update_strategy.command);\n \tfree(entry->config);\n }\ndiff --git a/t/t1013-read-tree-submodule.sh b/t/t1013-read-tree-submodule.sh\nindex bfc90d4cf2..cf8b94ebed 100755\n--- a/t/t1013-read-tree-submodule.sh\n+++ b/t/t1013-read-tree-submodule.sh\n@@ -2,6 +2,7 @@\n \n test_description='read-tree can handle submodules'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-submodule-update.sh\n \ndiff --git a/t/t2013-checkout-submodule.sh b/t/t2013-checkout-submodule.sh\nindex b2bdd1fcb4..3c1d663d94 100755\n--- a/t/t2013-checkout-submodule.sh\n+++ b/t/t2013-checkout-submodule.sh\n@@ -2,6 +2,7 @@\n \n test_description='checkout can handle submodules'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-submodule-update.sh\n \ndiff --git a/t/t3007-ls-files-recurse-submodules.sh b/t/t3007-ls-files-recurse-submodules.sh\nindex 61771eec83..f04bdc8c78 100755\n--- a/t/t3007-ls-files-recurse-submodules.sh\n+++ b/t/t3007-ls-files-recurse-submodules.sh\n@@ -6,6 +6,7 @@ This test verifies the recurse-submodules feature correctly lists files from\n submodules.\n '\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n test_expect_success 'setup directory structure and submodules' '\ndiff --git a/t/t7112-reset-submodule.sh b/t/t7112-reset-submodule.sh\nindex a3e2413bc3..b0d3d93b0b 100755\n--- a/t/t7112-reset-submodule.sh\n+++ b/t/t7112-reset-submodule.sh\n@@ -2,6 +2,7 @@\n \n test_description='reset can handle submodules'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-submodule-update.sh\n \n-- \n2.45.1.246.gb9cfe4845c.dirty\n\n"},{"id":"495687","messageId":"0f61ee9929ab70d5231af28f118c3b395546d570.1716810168.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716810168.git.ps@pks.im","subject":"[PATCH v3 17/21] strvec: add functions to replace and remove strings","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-27T11:47:04Z","receivedAt":"2024-05-27T11:47:08Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Add two functions that allow to replace and remove strings contained in\nthe strvec. This will be used by a subsequent commit that refactors\ngit-mv(1).\n\nWhile at it, add a bunch of unit tests that cover both old and new\nfunctionality.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile                |   1 +\n strvec.c                |  20 +++\n strvec.h                |  13 ++\n t/unit-tests/t-strvec.c | 269 ++++++++++++++++++++++++++++++++++++++++\n t/unit-tests/test-lib.c |  13 ++\n t/unit-tests/test-lib.h |  13 ++\n 6 files changed, 329 insertions(+)\n create mode 100644 t/unit-tests/t-strvec.c\n\ndiff --git a/Makefile b/Makefile\nindex cf504963c2..d4000fb1d6 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1336,6 +1336,7 @@ THIRD_PARTY_SOURCES += sha1dc/%\n \n UNIT_TEST_PROGRAMS += t-mem-pool\n UNIT_TEST_PROGRAMS += t-strbuf\n+UNIT_TEST_PROGRAMS += t-strvec\n UNIT_TEST_PROGRAMS += t-ctype\n UNIT_TEST_PROGRAMS += t-prio-queue\n UNIT_TEST_PROGS = $(patsubst %,$(UNIT_TEST_BIN)/%$X,$(UNIT_TEST_PROGRAMS))\ndiff --git a/strvec.c b/strvec.c\nindex 178f4f3748..d4073ec9fa 100644\n--- a/strvec.c\n+++ b/strvec.c\n@@ -56,6 +56,26 @@ void strvec_pushv(struct strvec *array, const char **items)\n \t\tstrvec_push(array, *items);\n }\n \n+const char *strvec_replace(struct strvec *array, size_t idx, const char *replacement)\n+{\n+\tchar *to_free;\n+\tif (idx >= array->nr)\n+\t\tBUG(\"index outside of array boundary\");\n+\tto_free = (char *) array->v[idx];\n+\tarray->v[idx] = xstrdup(replacement);\n+\tfree(to_free);\n+\treturn array->v[idx];\n+}\n+\n+void strvec_remove(struct strvec *array, size_t idx)\n+{\n+\tif (idx >= array->nr)\n+\t\tBUG(\"index outside of array boundary\");\n+\tfree((char *)array->v[idx]);\n+\tmemmove(array->v + idx, array->v + idx + 1, (array->nr - idx) * sizeof(char *));\n+\tarray->nr--;\n+}\n+\n void strvec_pop(struct strvec *array)\n {\n \tif (!array->nr)\ndiff --git a/strvec.h b/strvec.h\nindex 4715d3e51f..6c7e8b7d50 100644\n--- a/strvec.h\n+++ b/strvec.h\n@@ -64,6 +64,19 @@ void strvec_pushl(struct strvec *, ...);\n /* Push a null-terminated array of strings onto the end of the array. */\n void strvec_pushv(struct strvec *, const char **);\n \n+/**\n+ * Replace the value at the given index with a new value. The index must be\n+ * valid. Returns a pointer to the inserted value.\n+ */\n+const char *strvec_replace(struct strvec *array, size_t idx, const char *replacement);\n+\n+/*\n+ * Remove the value at the given index. The remainder of the array will be\n+ * moved to fill the resulting gap. The provided index must point into the\n+ * array.\n+ */\n+void strvec_remove(struct strvec *array, size_t idx);\n+\n /**\n  * Remove the final element from the array. If there are no\n  * elements in the array, do nothing.\ndiff --git a/t/unit-tests/t-strvec.c b/t/unit-tests/t-strvec.c\nnew file mode 100644\nindex 0000000000..f17fb10d9e\n--- /dev/null\n+++ b/t/unit-tests/t-strvec.c\n@@ -0,0 +1,269 @@\n+#include \"test-lib.h\"\n+#include \"strbuf.h\"\n+#include \"strvec.h\"\n+\n+#define check_strvec(vec, ...) \\\n+\tcheck_strvec_loc(TEST_LOCATION(), vec, __VA_ARGS__)\n+static void check_strvec_loc(const char *loc, struct strvec *vec, ...)\n+{\n+\tva_list ap;\n+\tsize_t nr = 0;\n+\n+\tva_start(ap, vec);\n+\twhile (1) {\n+\t\tconst char *str = va_arg(ap, const char *);\n+\t\tif (!str)\n+\t\t\tbreak;\n+\n+\t\tif (!check_uint(vec->nr, >, nr) ||\n+\t\t    !check_uint(vec->alloc, >, nr) ||\n+\t\t    !check_str(vec->v[nr], str)) {\n+\t\t\tstruct strbuf msg = STRBUF_INIT;\n+\t\t\tstrbuf_addf(&msg, \"strvec index %\"PRIuMAX, (uintmax_t) nr);\n+\t\t\ttest_assert(loc, msg.buf, 0);\n+\t\t\tstrbuf_release(&msg);\n+\t\t\treturn;\n+\t\t}\n+\n+\t\tnr++;\n+\t}\n+\n+\tcheck_uint(vec->nr, ==, nr);\n+\tcheck_uint(vec->alloc, >=, nr);\n+\tcheck_pointer_eq(vec->v[nr], NULL);\n+}\n+\n+static void t_static_init(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tcheck_pointer_eq(vec.v, empty_strvec);\n+\tcheck_uint(vec.nr, ==, 0);\n+\tcheck_uint(vec.alloc, ==, 0);\n+}\n+\n+static void t_dynamic_init(void)\n+{\n+\tstruct strvec vec;\n+\tstrvec_init(&vec);\n+\tcheck_pointer_eq(vec.v, empty_strvec);\n+\tcheck_uint(vec.nr, ==, 0);\n+\tcheck_uint(vec.alloc, ==, 0);\n+}\n+\n+static void t_clear(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_push(&vec, \"foo\");\n+\tstrvec_clear(&vec);\n+\tcheck_pointer_eq(vec.v, empty_strvec);\n+\tcheck_uint(vec.nr, ==, 0);\n+\tcheck_uint(vec.alloc, ==, 0);\n+}\n+\n+static void t_push(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_push(&vec, \"foo\");\n+\tcheck_strvec(&vec, \"foo\", NULL);\n+\n+\tstrvec_push(&vec, \"bar\");\n+\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n+\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_pushf(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushf(&vec, \"foo: %d\", 1);\n+\tcheck_strvec(&vec, \"foo: 1\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_pushl(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_pushv(void)\n+{\n+\tconst char *strings[] = {\n+\t\t\"foo\", \"bar\", \"baz\", NULL,\n+\t};\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_pushv(&vec, strings);\n+\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_replace_at_head(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_replace(&vec, 0, \"replaced\");\n+\tcheck_strvec(&vec, \"replaced\", \"bar\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_replace_at_tail(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_replace(&vec, 2, \"replaced\");\n+\tcheck_strvec(&vec, \"foo\", \"bar\", \"replaced\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_replace_in_between(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_replace(&vec, 1, \"replaced\");\n+\tcheck_strvec(&vec, \"foo\", \"replaced\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_replace_with_substring(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", NULL);\n+\tstrvec_replace(&vec, 0, vec.v[0] + 1);\n+\tcheck_strvec(&vec, \"oo\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_remove_at_head(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_remove(&vec, 0);\n+\tcheck_strvec(&vec, \"bar\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_remove_at_tail(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_remove(&vec, 2);\n+\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_remove_in_between(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_remove(&vec, 1);\n+\tcheck_strvec(&vec, \"foo\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_pop_empty_array(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pop(&vec);\n+\tcheck_strvec(&vec, NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_pop_non_empty_array(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_pop(&vec);\n+\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_split_empty_string(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_split(&vec, \"\");\n+\tcheck_strvec(&vec, NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_split_single_item(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_split(&vec, \"foo\");\n+\tcheck_strvec(&vec, \"foo\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_split_multiple_items(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_split(&vec, \"foo bar baz\");\n+\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_split_whitespace_only(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_split(&vec, \" \\t\\n\");\n+\tcheck_strvec(&vec, NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_split_multiple_consecutive_whitespaces(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_split(&vec, \"foo\\n\\t bar\");\n+\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+static void t_detach(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tconst char **detached;\n+\n+\tstrvec_push(&vec, \"foo\");\n+\n+\tdetached = strvec_detach(&vec);\n+\tcheck_str(detached[0], \"foo\");\n+\tcheck_pointer_eq(detached[1], NULL);\n+\n+\tcheck_pointer_eq(vec.v, empty_strvec);\n+\tcheck_uint(vec.nr, ==, 0);\n+\tcheck_uint(vec.alloc, ==, 0);\n+\n+\tfree((char *) detached[0]);\n+\tfree(detached);\n+}\n+\n+int cmd_main(int argc, const char **argv)\n+{\n+\tTEST(t_static_init(), \"static initialization\");\n+\tTEST(t_dynamic_init(), \"dynamic initialization\");\n+\tTEST(t_clear(), \"clear\");\n+\tTEST(t_push(), \"push\");\n+\tTEST(t_pushf(), \"pushf\");\n+\tTEST(t_pushl(), \"pushl\");\n+\tTEST(t_pushv(), \"pushv\");\n+\tTEST(t_replace_at_head(), \"replace at head\");\n+\tTEST(t_replace_in_between(), \"replace in between\");\n+\tTEST(t_replace_at_tail(), \"replace at tail\");\n+\tTEST(t_replace_with_substring(), \"replace with substring\");\n+\tTEST(t_remove_at_head(), \"remove at head\");\n+\tTEST(t_remove_in_between(), \"remove in between\");\n+\tTEST(t_remove_at_tail(), \"remove at tail\");\n+\tTEST(t_pop_empty_array(), \"pop with empty array\");\n+\tTEST(t_pop_non_empty_array(), \"pop with non-empty array\");\n+\tTEST(t_split_empty_string(), \"split empty string\");\n+\tTEST(t_split_single_item(), \"split single item\");\n+\tTEST(t_split_multiple_items(), \"split multiple items\");\n+\tTEST(t_split_whitespace_only(), \"split whitespace only\");\n+\tTEST(t_split_multiple_consecutive_whitespaces(), \"split multiple consecutive whitespaces\");\n+\tTEST(t_detach(), \"detach\");\n+\treturn test_done();\n+}\ndiff --git a/t/unit-tests/test-lib.c b/t/unit-tests/test-lib.c\nindex 66d6980ffb..3c513ce59a 100644\n--- a/t/unit-tests/test-lib.c\n+++ b/t/unit-tests/test-lib.c\n@@ -318,6 +318,19 @@ int check_bool_loc(const char *loc, const char *check, int ok)\n \n union test__tmp test__tmp[2];\n \n+int check_pointer_eq_loc(const char *loc, const char *check, int ok,\n+\t\t\t const void *a, const void *b)\n+{\n+\tint ret = test_assert(loc, check, ok);\n+\n+\tif (!ret) {\n+\t\ttest_msg(\"   left: %p\", a);\n+\t\ttest_msg(\"  right: %p\", b);\n+\t}\n+\n+\treturn ret;\n+}\n+\n int check_int_loc(const char *loc, const char *check, int ok,\n \t\t  intmax_t a, intmax_t b)\n {\ndiff --git a/t/unit-tests/test-lib.h b/t/unit-tests/test-lib.h\nindex a8f07ae0b7..2de6d715d5 100644\n--- a/t/unit-tests/test-lib.h\n+++ b/t/unit-tests/test-lib.h\n@@ -75,6 +75,18 @@ int test_assert(const char *location, const char *check, int ok);\n \tcheck_bool_loc(TEST_LOCATION(), #x, x)\n int check_bool_loc(const char *loc, const char *check, int ok);\n \n+/*\n+ * Compare two integers. Prints a message with the two values if the\n+ * comparison fails. NB this is not thread safe.\n+ */\n+#define check_pointer_eq(a, b)\t\t\t\t\t\t\\\n+\t(test__tmp[0].p = (a), test__tmp[1].p = (b),\t\t\t\\\n+\t check_pointer_eq_loc(TEST_LOCATION(), #a\" == \"#b,\t\t\\\n+\t\t\t      test__tmp[0].p == test__tmp[1].p,\t\t\\\n+\t\t\t      test__tmp[0].p, test__tmp[1].p))\n+int check_pointer_eq_loc(const char *loc, const char *check, int ok,\n+\t\t\t const void *a, const void *b);\n+\n /*\n  * Compare two integers. Prints a message with the two values if the\n  * comparison fails. NB this is not thread safe.\n@@ -136,6 +148,7 @@ union test__tmp {\n \tintmax_t i;\n \tuintmax_t u;\n \tchar c;\n+\tconst void *p;\n };\n \n extern union test__tmp test__tmp[2];\n-- \n2.45.1.246.gb9cfe4845c.dirty\n\n"},{"id":"495688","messageId":"1830e2a568d4a97c914b8fc6eafdfbb62527296d.1716810168.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716810168.git.ps@pks.im","subject":"[PATCH v3 18/21] builtin/mv: refactor `add_slash()` to always return allocated strings","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-27T11:47:09Z","receivedAt":"2024-05-27T11:47:14Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The `add_slash()` function will only conditionally return an allocated\nstring when the passed-in string did not yet have a trailing slash. This\nmakes the memory ownership harder to track than really necessary.\n\nIt's dubious whether this optimization really buys us all that much. The\nnumber of times we execute this function is bounded by the number of\narguments to git-mv(1), so in the typical case we may end up saving an\nallocation or two.\n\nSimplify the code to unconditionally return allocated strings.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/mv.c | 38 ++++++++++++++++++++------------------\n 1 file changed, 20 insertions(+), 18 deletions(-)\n\ndiff --git a/builtin/mv.c b/builtin/mv.c\nindex 74aa9746aa..9f4c75df04 100644\n--- a/builtin/mv.c\n+++ b/builtin/mv.c\n@@ -76,7 +76,7 @@ static const char **internal_prefix_pathspec(const char *prefix,\n \treturn result;\n }\n \n-static const char *add_slash(const char *path)\n+static char *add_slash(const char *path)\n {\n \tsize_t len = strlen(path);\n \tif (len && path[len - 1] != '/') {\n@@ -86,7 +86,7 @@ static const char *add_slash(const char *path)\n \t\twith_slash[len] = 0;\n \t\treturn with_slash;\n \t}\n-\treturn path;\n+\treturn xstrdup(path);\n }\n \n #define SUBMODULE_WITH_GITDIR ((const char *)1)\n@@ -111,7 +111,7 @@ static void prepare_move_submodule(const char *src, int first,\n static int index_range_of_same_dir(const char *src, int length,\n \t\t\t\t   int *first_p, int *last_p)\n {\n-\tconst char *src_w_slash = add_slash(src);\n+\tchar *src_w_slash = add_slash(src);\n \tint first, last, len_w_slash = length + 1;\n \n \tfirst = index_name_pos(the_repository->index, src_w_slash, len_w_slash);\n@@ -124,8 +124,8 @@ static int index_range_of_same_dir(const char *src, int length,\n \t\tif (strncmp(path, src_w_slash, len_w_slash))\n \t\t\tbreak;\n \t}\n-\tif (src_w_slash != src)\n-\t\tfree((char *)src_w_slash);\n+\n+\tfree(src_w_slash);\n \t*first_p = first;\n \t*last_p = last;\n \treturn last - first;\n@@ -141,7 +141,7 @@ static int index_range_of_same_dir(const char *src, int length,\n static int empty_dir_has_sparse_contents(const char *name)\n {\n \tint ret = 0;\n-\tconst char *with_slash = add_slash(name);\n+\tchar *with_slash = add_slash(name);\n \tint length = strlen(with_slash);\n \n \tint pos = index_name_pos(the_repository->index, with_slash, length);\n@@ -159,8 +159,7 @@ static int empty_dir_has_sparse_contents(const char *name)\n \t}\n \n free_return:\n-\tif (with_slash != name)\n-\t\tfree((char *)with_slash);\n+\tfree(with_slash);\n \treturn ret;\n }\n \n@@ -178,7 +177,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\tOPT_END(),\n \t};\n \tconst char **source, **destination, **dest_path, **submodule_gitfile;\n-\tconst char *dst_w_slash;\n+\tchar *dst_w_slash = NULL;\n \tconst char **src_dir = NULL;\n \tint src_dir_nr = 0, src_dir_alloc = 0;\n \tstruct strbuf a_src_dir = STRBUF_INIT;\n@@ -243,10 +242,6 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\t\t\tdst_mode = SPARSE;\n \t\t}\n \t}\n-\tif (dst_w_slash != dest_path[0]) {\n-\t\tfree((char *)dst_w_slash);\n-\t\tdst_w_slash = NULL;\n-\t}\n \n \t/* Checking */\n \tfor (i = 0; i < argc; i++) {\n@@ -265,12 +260,14 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \n \t\t\tpos = index_name_pos(the_repository->index, src, length);\n \t\t\tif (pos < 0) {\n-\t\t\t\tconst char *src_w_slash = add_slash(src);\n+\t\t\t\tchar *src_w_slash = add_slash(src);\n \t\t\t\tif (!path_in_sparse_checkout(src_w_slash, the_repository->index) &&\n \t\t\t\t    empty_dir_has_sparse_contents(src)) {\n+\t\t\t\t\tfree(src_w_slash);\n \t\t\t\t\tmodes[i] |= SKIP_WORKTREE_DIR;\n \t\t\t\t\tgoto dir_check;\n \t\t\t\t}\n+\t\t\t\tfree(src_w_slash);\n \t\t\t\t/* only error if existence is expected. */\n \t\t\t\tif (!(modes[i] & SPARSE))\n \t\t\t\t\tbad = _(\"bad source\");\n@@ -310,7 +307,9 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \n dir_check:\n \t\tif (S_ISDIR(st.st_mode)) {\n-\t\t\tint j, dst_len, n;\n+\t\t\tchar *dst_with_slash;\n+\t\t\tsize_t dst_with_slash_len;\n+\t\t\tint j, n;\n \t\t\tint first = index_name_pos(the_repository->index, src, length), last;\n \n \t\t\tif (first >= 0) {\n@@ -335,19 +334,21 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\t\tREALLOC_ARRAY(modes, n);\n \t\t\tREALLOC_ARRAY(submodule_gitfile, n);\n \n-\t\t\tdst = add_slash(dst);\n-\t\t\tdst_len = strlen(dst);\n+\t\t\tdst_with_slash = add_slash(dst);\n+\t\t\tdst_with_slash_len = strlen(dst_with_slash);\n \n \t\t\tfor (j = 0; j < last - first; j++) {\n \t\t\t\tconst struct cache_entry *ce = the_repository->index->cache[first + j];\n \t\t\t\tconst char *path = ce->name;\n \t\t\t\tsource[argc + j] = path;\n \t\t\t\tdestination[argc + j] =\n-\t\t\t\t\tprefix_path(dst, dst_len, path + length + 1);\n+\t\t\t\t\tprefix_path(dst_with_slash, dst_with_slash_len, path + length + 1);\n \t\t\t\tmemset(modes + argc + j, 0, sizeof(enum update_mode));\n \t\t\t\tmodes[argc + j] |= ce_skip_worktree(ce) ? SPARSE : INDEX;\n \t\t\t\tsubmodule_gitfile[argc + j] = NULL;\n \t\t\t}\n+\n+\t\t\tfree(dst_with_slash);\n \t\t\targc += last - first;\n \t\t\tgoto act_on_entry;\n \t\t}\n@@ -565,6 +566,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\t\t       COMMIT_LOCK | SKIP_IF_UNCHANGED))\n \t\tdie(_(\"Unable to write new index file\"));\n \n+\tfree(dst_w_slash);\n \tstring_list_clear(&src_for_dst, 0);\n \tstring_list_clear(&dirty_paths, 0);\n \tUNLEAK(source);\n-- \n2.45.1.246.gb9cfe4845c.dirty\n\n"},{"id":"495689","messageId":"9eeafac3650d6573c4ef09ccb499b75c5de2f3d6.1716810168.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716810168.git.ps@pks.im","subject":"[PATCH v3 19/21] builtin/mv duplicate string list memory","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-27T11:47:13Z","receivedAt":"2024-05-27T11:47:18Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"makes the next patch easier, where we will migrate to the paths being\nowned by a strvec. given that we are talking about command line\nparameters here it's also not like we have tons of allocations that this\nwould save\n\nwhile at it, fix a memory leak\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/mv.c | 19 +++++++++++++------\n 1 file changed, 13 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/mv.c b/builtin/mv.c\nindex 9f4c75df04..12dcc0b13c 100644\n--- a/builtin/mv.c\n+++ b/builtin/mv.c\n@@ -183,11 +183,12 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \tstruct strbuf a_src_dir = STRBUF_INIT;\n \tenum update_mode *modes, dst_mode = 0;\n \tstruct stat st, dest_st;\n-\tstruct string_list src_for_dst = STRING_LIST_INIT_NODUP;\n+\tstruct string_list src_for_dst = STRING_LIST_INIT_DUP;\n \tstruct lock_file lock_file = LOCK_INIT;\n \tstruct cache_entry *ce;\n-\tstruct string_list only_match_skip_worktree = STRING_LIST_INIT_NODUP;\n-\tstruct string_list dirty_paths = STRING_LIST_INIT_NODUP;\n+\tstruct string_list only_match_skip_worktree = STRING_LIST_INIT_DUP;\n+\tstruct string_list dirty_paths = STRING_LIST_INIT_DUP;\n+\tint ret;\n \n \tgit_config(git_default_config, NULL);\n \n@@ -440,8 +441,10 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \n \tif (only_match_skip_worktree.nr) {\n \t\tadvise_on_updating_sparse_paths(&only_match_skip_worktree);\n-\t\tif (!ignore_errors)\n-\t\t\treturn 1;\n+\t\tif (!ignore_errors) {\n+\t\t\tret = 1;\n+\t\t\tgoto out;\n+\t\t}\n \t}\n \n \tfor (i = 0; i < argc; i++) {\n@@ -566,12 +569,16 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\t\t       COMMIT_LOCK | SKIP_IF_UNCHANGED))\n \t\tdie(_(\"Unable to write new index file\"));\n \n+\tret = 0;\n+\n+out:\n \tfree(dst_w_slash);\n \tstring_list_clear(&src_for_dst, 0);\n \tstring_list_clear(&dirty_paths, 0);\n+\tstring_list_clear(&only_match_skip_worktree, 0);\n \tUNLEAK(source);\n \tUNLEAK(dest_path);\n \tfree(submodule_gitfile);\n \tfree(modes);\n-\treturn 0;\n+\treturn ret;\n }\n-- \n2.45.1.246.gb9cfe4845c.dirty\n\n"},{"id":"495690","messageId":"48b9d3e3436aff539e5c76c945f95067110254ff.1716810168.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716810168.git.ps@pks.im","subject":"[PATCH v3 20/21] builtin/mv: refactor to use `struct strvec`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-27T11:47:18Z","receivedAt":"2024-05-27T11:47:23Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Memory allocation patterns in git-mv(1) are extremely hard to follow:\nWe copy around string pointers into manually-managed arrays, some of\nwhich alias each other, but only sometimes, while we also drop some of\nthose strings at other times without ever daring to free them.\n\nWhile this may be my own subjective feeling, it seems like others have\ngiven up as the code has multiple calls to `UNLEAK()`. These are not\nsufficient though, and git-mv(1) is still leaking all over the place\neven with them.\n\nRefactor the code to instead track strings in `struct strvec`. While\nthis has the effect of effectively duplicating some of the strings\nwithout an actual need, it is way easier to reason about and fixes all\nof the aliasing of memory that has been going on. It allows us to get\nrid of the `UNLEAK()` calls and also fixes leaks that those calls did\nnot paper over.\n\nMark tests which are now leak-free accordingly.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/mv.c                             | 125 +++++++++++------------\n t/t4001-diff-rename.sh                   |   4 +-\n t/t4043-diff-rename-binary.sh            |   1 +\n t/t4120-apply-popt.sh                    |   1 +\n t/t6400-merge-df.sh                      |   1 +\n t/t6412-merge-large-rename.sh            |   1 +\n t/t6426-merge-skip-unneeded-updates.sh   |   1 +\n t/t6429-merge-sequence-rename-caching.sh |   1 +\n 8 files changed, 68 insertions(+), 67 deletions(-)\n\ndiff --git a/builtin/mv.c b/builtin/mv.c\nindex 12dcc0b13c..e461d29ca1 100644\n--- a/builtin/mv.c\n+++ b/builtin/mv.c\n@@ -20,6 +20,7 @@\n #include \"read-cache-ll.h\"\n #include \"repository.h\"\n #include \"setup.h\"\n+#include \"strvec.h\"\n #include \"submodule.h\"\n #include \"entry.h\"\n \n@@ -38,42 +39,32 @@ enum update_mode {\n #define DUP_BASENAME 1\n #define KEEP_TRAILING_SLASH 2\n \n-static const char **internal_prefix_pathspec(const char *prefix,\n-\t\t\t\t\t     const char **pathspec,\n-\t\t\t\t\t     int count, unsigned flags)\n+static void internal_prefix_pathspec(struct strvec *out,\n+\t\t\t\t     const char *prefix,\n+\t\t\t\t     const char **pathspec,\n+\t\t\t\t     int count, unsigned flags)\n {\n-\tint i;\n-\tconst char **result;\n \tint prefixlen = prefix ? strlen(prefix) : 0;\n-\tALLOC_ARRAY(result, count + 1);\n \n \t/* Create an intermediate copy of the pathspec based on the flags */\n-\tfor (i = 0; i < count; i++) {\n-\t\tint length = strlen(pathspec[i]);\n-\t\tint to_copy = length;\n-\t\tchar *it;\n+\tfor (int i = 0; i < count; i++) {\n+\t\tsize_t length = strlen(pathspec[i]);\n+\t\tsize_t to_copy = length;\n+\t\tconst char *maybe_basename;\n+\t\tchar *trimmed, *prefixed_path;\n+\n \t\twhile (!(flags & KEEP_TRAILING_SLASH) &&\n \t\t       to_copy > 0 && is_dir_sep(pathspec[i][to_copy - 1]))\n \t\t\tto_copy--;\n \n-\t\tit = xmemdupz(pathspec[i], to_copy);\n-\t\tif (flags & DUP_BASENAME) {\n-\t\t\tresult[i] = xstrdup(basename(it));\n-\t\t\tfree(it);\n-\t\t} else {\n-\t\t\tresult[i] = it;\n-\t\t}\n-\t}\n-\tresult[count] = NULL;\n+\t\ttrimmed = xmemdupz(pathspec[i], to_copy);\n+\t\tmaybe_basename = (flags & DUP_BASENAME) ? basename(trimmed) : trimmed;\n+\t\tprefixed_path = prefix_path(prefix, prefixlen, maybe_basename);\n+\t\tstrvec_push(out, prefixed_path);\n \n-\t/* Prefix the pathspec and free the old intermediate strings */\n-\tfor (i = 0; i < count; i++) {\n-\t\tconst char *match = prefix_path(prefix, prefixlen, result[i]);\n-\t\tfree((char *) result[i]);\n-\t\tresult[i] = match;\n+\t\tfree(prefixed_path);\n+\t\tfree(trimmed);\n \t}\n-\n-\treturn result;\n }\n \n static char *add_slash(const char *path)\n@@ -176,7 +167,10 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\tOPT_BOOL(0, \"sparse\", &ignore_sparse, N_(\"allow updating entries outside of the sparse-checkout cone\")),\n \t\tOPT_END(),\n \t};\n-\tconst char **source, **destination, **dest_path, **submodule_gitfile;\n+\tstruct strvec sources = STRVEC_INIT;\n+\tstruct strvec dest_paths = STRVEC_INIT;\n+\tstruct strvec destinations = STRVEC_INIT;\n+\tconst char **submodule_gitfile;\n \tchar *dst_w_slash = NULL;\n \tconst char **src_dir = NULL;\n \tint src_dir_nr = 0, src_dir_alloc = 0;\n@@ -201,7 +195,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \tif (repo_read_index(the_repository) < 0)\n \t\tdie(_(\"index file corrupt\"));\n \n-\tsource = internal_prefix_pathspec(prefix, argv, argc, 0);\n+\tinternal_prefix_pathspec(&sources, prefix, argv, argc, 0);\n \tCALLOC_ARRAY(modes, argc);\n \n \t/*\n@@ -212,41 +206,39 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \tflags = KEEP_TRAILING_SLASH;\n \tif (argc == 1 && is_directory(argv[0]) && !is_directory(argv[1]))\n \t\tflags = 0;\n-\tdest_path = internal_prefix_pathspec(prefix, argv + argc, 1, flags);\n-\tdst_w_slash = add_slash(dest_path[0]);\n+\tinternal_prefix_pathspec(&dest_paths, prefix, argv + argc, 1, flags);\n+\tdst_w_slash = add_slash(dest_paths.v[0]);\n \tsubmodule_gitfile = xcalloc(argc, sizeof(char *));\n \n-\tif (dest_path[0][0] == '\\0')\n+\tif (dest_paths.v[0][0] == '\\0')\n \t\t/* special case: \".\" was normalized to \"\" */\n-\t\tdestination = internal_prefix_pathspec(dest_path[0], argv, argc, DUP_BASENAME);\n-\telse if (!lstat(dest_path[0], &st) &&\n-\t\t\tS_ISDIR(st.st_mode)) {\n-\t\tdestination = internal_prefix_pathspec(dst_w_slash, argv, argc, DUP_BASENAME);\n+\t\tinternal_prefix_pathspec(&destinations, dest_paths.v[0], argv, argc, DUP_BASENAME);\n+\telse if (!lstat(dest_paths.v[0], &st) && S_ISDIR(st.st_mode)) {\n+\t\tinternal_prefix_pathspec(&destinations, dst_w_slash, argv, argc, DUP_BASENAME);\n+\t} else if (!path_in_sparse_checkout(dst_w_slash, the_repository->index) &&\n+\t\t   empty_dir_has_sparse_contents(dst_w_slash)) {\n+\t\tinternal_prefix_pathspec(&destinations, dst_w_slash, argv, argc, DUP_BASENAME);\n+\t\tdst_mode = SKIP_WORKTREE_DIR;\n+\t} else if (argc != 1) {\n+\t\tdie(_(\"destination '%s' is not a directory\"), dest_paths.v[0]);\n \t} else {\n-\t\tif (!path_in_sparse_checkout(dst_w_slash, the_repository->index) &&\n-\t\t    empty_dir_has_sparse_contents(dst_w_slash)) {\n-\t\t\tdestination = internal_prefix_pathspec(dst_w_slash, argv, argc, DUP_BASENAME);\n-\t\t\tdst_mode = SKIP_WORKTREE_DIR;\n-\t\t} else if (argc != 1) {\n-\t\t\tdie(_(\"destination '%s' is not a directory\"), dest_path[0]);\n-\t\t} else {\n-\t\t\tdestination = dest_path;\n-\t\t\t/*\n-\t\t\t * <destination> is a file outside of sparse-checkout\n-\t\t\t * cone. Insist on cone mode here for backward\n-\t\t\t * compatibility. We don't want dst_mode to be assigned\n-\t\t\t * for a file when the repo is using no-cone mode (which\n-\t\t\t * is deprecated at this point) sparse-checkout. As\n-\t\t\t * SPARSE here is only considering cone-mode situation.\n-\t\t\t */\n-\t\t\tif (!path_in_cone_mode_sparse_checkout(destination[0], the_repository->index))\n-\t\t\t\tdst_mode = SPARSE;\n-\t\t}\n+\t\tstrvec_pushv(&destinations, dest_paths.v);\n+\n+\t\t/*\n+\t\t * <destination> is a file outside of sparse-checkout\n+\t\t * cone. Insist on cone mode here for backward\n+\t\t * compatibility. We don't want dst_mode to be assigned\n+\t\t * for a file when the repo is using no-cone mode (which\n+\t\t * is deprecated at this point) sparse-checkout. As\n+\t\t * SPARSE here is only considering cone-mode situation.\n+\t\t */\n+\t\tif (!path_in_cone_mode_sparse_checkout(destinations.v[0], the_repository->index))\n+\t\t\tdst_mode = SPARSE;\n \t}\n \n \t/* Checking */\n \tfor (i = 0; i < argc; i++) {\n-\t\tconst char *src = source[i], *dst = destination[i];\n+\t\tconst char *src = sources.v[i], *dst = destinations.v[i];\n \t\tint length;\n \t\tconst char *bad = NULL;\n \t\tint skip_sparse = 0;\n@@ -330,8 +322,6 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\t\tsrc_dir[src_dir_nr++] = src;\n \n \t\t\tn = argc + last - first;\n-\t\t\tREALLOC_ARRAY(source, n);\n-\t\t\tREALLOC_ARRAY(destination, n);\n \t\t\tREALLOC_ARRAY(modes, n);\n \t\t\tREALLOC_ARRAY(submodule_gitfile, n);\n \n@@ -341,12 +331,16 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\t\tfor (j = 0; j < last - first; j++) {\n \t\t\t\tconst struct cache_entry *ce = the_repository->index->cache[first + j];\n \t\t\t\tconst char *path = ce->name;\n-\t\t\t\tsource[argc + j] = path;\n-\t\t\t\tdestination[argc + j] =\n-\t\t\t\t\tprefix_path(dst_with_slash, dst_with_slash_len, path + length + 1);\n+\t\t\t\tchar *prefixed_path = prefix_path(dst_with_slash, dst_with_slash_len, path + length + 1);\n+\n+\t\t\t\tstrvec_push(&sources, path);\n+\t\t\t\tstrvec_push(&destinations, prefixed_path);\n+\n \t\t\t\tmemset(modes + argc + j, 0, sizeof(enum update_mode));\n \t\t\t\tmodes[argc + j] |= ce_skip_worktree(ce) ? SPARSE : INDEX;\n \t\t\t\tsubmodule_gitfile[argc + j] = NULL;\n+\n+\t\t\t\tfree(prefixed_path);\n \t\t\t}\n \n \t\t\tfree(dst_with_slash);\n@@ -430,8 +424,8 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n remove_entry:\n \t\tif (--argc > 0) {\n \t\t\tint n = argc - i;\n-\t\t\tMOVE_ARRAY(source + i, source + i + 1, n);\n-\t\t\tMOVE_ARRAY(destination + i, destination + i + 1, n);\n+\t\t\tstrvec_remove(&sources, i);\n+\t\t\tstrvec_remove(&destinations, i);\n \t\t\tMOVE_ARRAY(modes + i, modes + i + 1, n);\n \t\t\tMOVE_ARRAY(submodule_gitfile + i,\n \t\t\t\t   submodule_gitfile + i + 1, n);\n@@ -448,7 +442,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t}\n \n \tfor (i = 0; i < argc; i++) {\n-\t\tconst char *src = source[i], *dst = destination[i];\n+\t\tconst char *src = sources.v[i], *dst = destinations.v[i];\n \t\tenum update_mode mode = modes[i];\n \t\tint pos;\n \t\tint sparse_and_dirty = 0;\n@@ -576,8 +570,9 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \tstring_list_clear(&src_for_dst, 0);\n \tstring_list_clear(&dirty_paths, 0);\n \tstring_list_clear(&only_match_skip_worktree, 0);\n-\tUNLEAK(source);\n-\tUNLEAK(dest_path);\n+\tstrvec_clear(&sources);\n+\tstrvec_clear(&dest_paths);\n+\tstrvec_clear(&destinations);\n \tfree(submodule_gitfile);\n \tfree(modes);\n \treturn ret;\ndiff --git a/t/t4001-diff-rename.sh b/t/t4001-diff-rename.sh\nindex 49c042a38a..cd1931dd55 100755\n--- a/t/t4001-diff-rename.sh\n+++ b/t/t4001-diff-rename.sh\n@@ -3,9 +3,9 @@\n # Copyright (c) 2005 Junio C Hamano\n #\n \n-test_description='Test rename detection in diff engine.\n+test_description='Test rename detection in diff engine.'\n \n-'\n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-diff.sh\n \ndiff --git a/t/t4043-diff-rename-binary.sh b/t/t4043-diff-rename-binary.sh\nindex 2a2cf91352..e486493908 100755\n--- a/t/t4043-diff-rename-binary.sh\n+++ b/t/t4043-diff-rename-binary.sh\n@@ -5,6 +5,7 @@\n \n test_description='Move a binary file'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n \ndiff --git a/t/t4120-apply-popt.sh b/t/t4120-apply-popt.sh\nindex 697e86c0ff..f788428540 100755\n--- a/t/t4120-apply-popt.sh\n+++ b/t/t4120-apply-popt.sh\n@@ -5,6 +5,7 @@\n \n test_description='git apply -p handling.'\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n test_expect_success setup '\ndiff --git a/t/t6400-merge-df.sh b/t/t6400-merge-df.sh\nindex 3de4ef6bd9..27d6efdc9a 100755\n--- a/t/t6400-merge-df.sh\n+++ b/t/t6400-merge-df.sh\n@@ -7,6 +7,7 @@ test_description='Test merge with directory/file conflicts'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n test_expect_success 'prepare repository' '\ndiff --git a/t/t6412-merge-large-rename.sh b/t/t6412-merge-large-rename.sh\nindex ca018d11f5..d0863a8fb5 100755\n--- a/t/t6412-merge-large-rename.sh\n+++ b/t/t6412-merge-large-rename.sh\n@@ -4,6 +4,7 @@ test_description='merging with large rename matrix'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n count() {\ndiff --git a/t/t6426-merge-skip-unneeded-updates.sh b/t/t6426-merge-skip-unneeded-updates.sh\nindex b059475ed0..62f0180325 100755\n--- a/t/t6426-merge-skip-unneeded-updates.sh\n+++ b/t/t6426-merge-skip-unneeded-updates.sh\n@@ -22,6 +22,7 @@ test_description=\"merge cases\"\n #                     underscore notation is to differentiate different\n #                     files that might be renamed into each other's paths.)\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-merge.sh\n \ndiff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh\nindex 0f39ed0d08..cb1c4ceef7 100755\n--- a/t/t6429-merge-sequence-rename-caching.sh\n+++ b/t/t6429-merge-sequence-rename-caching.sh\n@@ -2,6 +2,7 @@\n \n test_description=\"remember regular & dir renames in sequence of merges\"\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n #\n-- \n2.45.1.246.gb9cfe4845c.dirty\n\n"},{"id":"495691","messageId":"add7946446daab57b28454dda625c4c77ab6c745.1716810168.git.ps@pks.im","threadId":"61535","inReplyTo":"cover.1716810168.git.ps@pks.im","subject":"[PATCH v3 21/21] builtin/mv: fix leaks for submodule gitfile paths","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-27T11:47:23Z","receivedAt":"2024-05-27T11:47:28Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Similar to the preceding commit, we have effectively given tracking\nmemory ownership of submodule gitfile paths. Refactor the code to start\ntracking allocated strings in a separate `struct strvec` such that we\ncan easily plug those leaks. Mark now-passing tests as leak free.\n\nNote that ideally, we wouldn't require two separate data structures to\ntrack those paths. But we do need to store `NULL` pointers for the\ngitfile paths such that we can indicate that its corresponding entries\nin the other arrays do not have such a path at all. And given that\n`struct strvec`s cannot store `NULL` pointers we cannot use them to\nstore this information.\n\nThere is another small gotcha that is easy to miss: you may be wondering\nwhy we don't want to store `SUBMODULE_WITH_GITDIR` in the strvec. This\nis because this is a mere sentinel value and not actually a string at\nall.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/mv.c                              | 44 +++++++++++++----------\n t/t4059-diff-submodule-not-initialized.sh |  1 +\n t/t7001-mv.sh                             |  2 ++\n t/t7417-submodule-path-url.sh             |  1 +\n t/t7421-submodule-summary-add.sh          |  1 +\n 5 files changed, 30 insertions(+), 19 deletions(-)\n\ndiff --git a/builtin/mv.c b/builtin/mv.c\nindex e461d29ca1..81ca910de6 100644\n--- a/builtin/mv.c\n+++ b/builtin/mv.c\n@@ -82,21 +82,23 @@ static char *add_slash(const char *path)\n \n #define SUBMODULE_WITH_GITDIR ((const char *)1)\n \n-static void prepare_move_submodule(const char *src, int first,\n-\t\t\t\t   const char **submodule_gitfile)\n+static const char *submodule_gitfile_path(const char *src, int first)\n {\n \tstruct strbuf submodule_dotgit = STRBUF_INIT;\n+\tconst char *path;\n+\n \tif (!S_ISGITLINK(the_repository->index->cache[first]->ce_mode))\n \t\tdie(_(\"Directory %s is in index and no submodule?\"), src);\n \tif (!is_staging_gitmodules_ok(the_repository->index))\n \t\tdie(_(\"Please stage your changes to .gitmodules or stash them to proceed\"));\n+\n \tstrbuf_addf(&submodule_dotgit, \"%s/.git\", src);\n-\t*submodule_gitfile = read_gitfile(submodule_dotgit.buf);\n-\tif (*submodule_gitfile)\n-\t\t*submodule_gitfile = xstrdup(*submodule_gitfile);\n-\telse\n-\t\t*submodule_gitfile = SUBMODULE_WITH_GITDIR;\n+\n+\tpath = read_gitfile(submodule_dotgit.buf);\n \tstrbuf_release(&submodule_dotgit);\n+\tif (path)\n+\t\treturn path;\n+\treturn SUBMODULE_WITH_GITDIR;\n }\n \n static int index_range_of_same_dir(const char *src, int length,\n@@ -170,7 +172,8 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \tstruct strvec sources = STRVEC_INIT;\n \tstruct strvec dest_paths = STRVEC_INIT;\n \tstruct strvec destinations = STRVEC_INIT;\n-\tconst char **submodule_gitfile;\n+\tstruct strvec submodule_gitfiles_to_free = STRVEC_INIT;\n+\tconst char **submodule_gitfiles;\n \tchar *dst_w_slash = NULL;\n \tconst char **src_dir = NULL;\n \tint src_dir_nr = 0, src_dir_alloc = 0;\n@@ -208,7 +211,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\tflags = 0;\n \tinternal_prefix_pathspec(&dest_paths, prefix, argv + argc, 1, flags);\n \tdst_w_slash = add_slash(dest_paths.v[0]);\n-\tsubmodule_gitfile = xcalloc(argc, sizeof(char *));\n+\tsubmodule_gitfiles = xcalloc(argc, sizeof(char *));\n \n \tif (dest_paths.v[0][0] == '\\0')\n \t\t/* special case: \".\" was normalized to \"\" */\n@@ -306,8 +309,10 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\t\tint first = index_name_pos(the_repository->index, src, length), last;\n \n \t\t\tif (first >= 0) {\n-\t\t\t\tprepare_move_submodule(src, first,\n-\t\t\t\t\t\t       submodule_gitfile + i);\n+\t\t\t\tconst char *path = submodule_gitfile_path(src, first);\n+\t\t\t\tif (path != SUBMODULE_WITH_GITDIR)\n+\t\t\t\t\tpath = strvec_push(&submodule_gitfiles_to_free, path);\n+\t\t\t\tsubmodule_gitfiles[i] = path;\n \t\t\t\tgoto act_on_entry;\n \t\t\t} else if (index_range_of_same_dir(src, length,\n \t\t\t\t\t\t\t   &first, &last) < 1) {\n@@ -323,7 +328,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \n \t\t\tn = argc + last - first;\n \t\t\tREALLOC_ARRAY(modes, n);\n-\t\t\tREALLOC_ARRAY(submodule_gitfile, n);\n+\t\t\tREALLOC_ARRAY(submodule_gitfiles, n);\n \n \t\t\tdst_with_slash = add_slash(dst);\n \t\t\tdst_with_slash_len = strlen(dst_with_slash);\n@@ -338,7 +343,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \n \t\t\t\tmemset(modes + argc + j, 0, sizeof(enum update_mode));\n \t\t\t\tmodes[argc + j] |= ce_skip_worktree(ce) ? SPARSE : INDEX;\n-\t\t\t\tsubmodule_gitfile[argc + j] = NULL;\n+\t\t\t\tsubmodule_gitfiles[argc + j] = NULL;\n \n \t\t\t\tfree(prefixed_path);\n \t\t\t}\n@@ -427,8 +432,8 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\t\tstrvec_remove(&sources, i);\n \t\t\tstrvec_remove(&destinations, i);\n \t\t\tMOVE_ARRAY(modes + i, modes + i + 1, n);\n-\t\t\tMOVE_ARRAY(submodule_gitfile + i,\n-\t\t\t\t   submodule_gitfile + i + 1, n);\n+\t\t\tMOVE_ARRAY(submodule_gitfiles + i,\n+\t\t\t\t   submodule_gitfiles + i + 1, n);\n \t\t\ti--;\n \t\t}\n \t}\n@@ -462,12 +467,12 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\t\t\tcontinue;\n \t\t\tdie_errno(_(\"renaming '%s' failed\"), src);\n \t\t}\n-\t\tif (submodule_gitfile[i]) {\n+\t\tif (submodule_gitfiles[i]) {\n \t\t\tif (!update_path_in_gitmodules(src, dst))\n \t\t\t\tgitmodules_modified = 1;\n-\t\t\tif (submodule_gitfile[i] != SUBMODULE_WITH_GITDIR)\n+\t\t\tif (submodule_gitfiles[i] != SUBMODULE_WITH_GITDIR)\n \t\t\t\tconnect_work_tree_and_git_dir(dst,\n-\t\t\t\t\t\t\t      submodule_gitfile[i],\n+\t\t\t\t\t\t\t      submodule_gitfiles[i],\n \t\t\t\t\t\t\t      1);\n \t\t}\n \n@@ -573,7 +578,8 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \tstrvec_clear(&sources);\n \tstrvec_clear(&dest_paths);\n \tstrvec_clear(&destinations);\n-\tfree(submodule_gitfile);\n+\tstrvec_clear(&submodule_gitfiles_to_free);\n+\tfree(submodule_gitfiles);\n \tfree(modes);\n \treturn ret;\n }\ndiff --git a/t/t4059-diff-submodule-not-initialized.sh b/t/t4059-diff-submodule-not-initialized.sh\nindex d489230df8..668f526303 100755\n--- a/t/t4059-diff-submodule-not-initialized.sh\n+++ b/t/t4059-diff-submodule-not-initialized.sh\n@@ -9,6 +9,7 @@ This test tries to verify that add_submodule_odb works when the submodule was\n initialized previously but the checkout has since been removed.\n '\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n # Tested non-UTF-8 encoding\ndiff --git a/t/t7001-mv.sh b/t/t7001-mv.sh\nindex 879a6dce60..86258f9f43 100755\n--- a/t/t7001-mv.sh\n+++ b/t/t7001-mv.sh\n@@ -1,6 +1,8 @@\n #!/bin/sh\n \n test_description='git mv in subdirs'\n+\n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-diff-data.sh\n \ndiff --git a/t/t7417-submodule-path-url.sh b/t/t7417-submodule-path-url.sh\nindex 5e3051da8b..dbbb3853dc 100755\n--- a/t/t7417-submodule-path-url.sh\n+++ b/t/t7417-submodule-path-url.sh\n@@ -4,6 +4,7 @@ test_description='check handling of .gitmodule path with dash'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n test_expect_success 'setup' '\ndiff --git a/t/t7421-submodule-summary-add.sh b/t/t7421-submodule-summary-add.sh\nindex ce64d8b137..479c8fdde1 100755\n--- a/t/t7421-submodule-summary-add.sh\n+++ b/t/t7421-submodule-summary-add.sh\n@@ -10,6 +10,7 @@ while making sure to add submodules using `git submodule add` instead of\n `git add` as done in t7401.\n '\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n test_expect_success 'setup' '\n-- \n2.45.1.246.gb9cfe4845c.dirty\n\n"},{"id":"495699","messageId":"xmqqwmnf9mto.fsf@gitster.g","threadId":"61535","inReplyTo":"ZlQr11P_C-KyO2DF@tanuki","subject":"Re: [PATCH v2 00/21] Various memory leak fixes","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-27T17:38:59Z","receivedAt":"2024-05-27T17:39:09Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Indeed. The following diff fixes the leak:\n>\n>     diff --git a/builtin/update-ref.c b/builtin/update-ref.c\n>     index 7d2a419230..e54be9c429 100644\n>     --- a/builtin/update-ref.c\n>     +++ b/builtin/update-ref.c\n>     @@ -130,6 +130,8 @@ static char *parse_next_arg(const char **next)\n>      \n>         if (arg.len)\n>             return strbuf_detach(&arg, NULL);\n>     +\n>     +\tstrbuf_release(&arg);\n>         return NULL;\n>      }\n>      \n>\n> Karthik is out of office this week, so you may want to add this as a\n> \"SQUASH???\" commit on top of his topic branch to make \"seen\" pass.\n\nAlright.  Thanks.\n\n>> Also\n>> \n>>  https://github.com/git/git/actions/runs/9231313414/job/25401102951\n>> \n>> shows that t1460-refs-migrate fails on Windows.\n>\n> Hm, this one is curious. There are no leak logs at all, and the exit\n> code is 139. Might be SIGSEGV, indicating that something else is going\n> on here than a memory leak.\n\nSorry, I wasn't clear enough.  I do not suspect this is about leaks\n(and the failing job on Windows is not about leaks).\n"},{"id":"495702","messageId":"xmqqikyz9m70.fsf@gitster.g","threadId":"61535","inReplyTo":"cover.1716810168.git.ps@pks.im","subject":"Re: [PATCH v3 00/21] Various memory leak fixes","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-27T17:52:35Z","receivedAt":"2024-05-27T17:52:38Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> this is the third version of my patch series that fixes various memory\n> leaks throughout Git.\n\nThanks, let's merge the whole thing to 'next'.\n"},{"id":"495703","messageId":"xmqq8qzv9lqp.fsf@gitster.g","threadId":"61535","inReplyTo":"xmqqwmnf9mto.fsf@gitster.g","subject":"Re: [PATCH v2 00/21] Various memory leak fixes","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-27T18:02:22Z","receivedAt":"2024-05-27T18:02:24Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Patrick Steinhardt <ps@pks.im> writes:\n>\n>> Indeed. The following diff fixes the leak:\n>>\n>>     diff --git a/builtin/update-ref.c b/builtin/update-ref.c\n>>     index 7d2a419230..e54be9c429 100644\n>>     --- a/builtin/update-ref.c\n>>     +++ b/builtin/update-ref.c\n>>     @@ -130,6 +130,8 @@ static char *parse_next_arg(const char **next)\n>>      \n>>         if (arg.len)\n>>             return strbuf_detach(&arg, NULL);\n>>     +\n>>     +\tstrbuf_release(&arg);\n>>         return NULL;\n>>      }\n>>      \n>>\n>> Karthik is out of office this week, so you may want to add this as a\n>> \"SQUASH???\" commit on top of his topic branch to make \"seen\" pass.\n>\n> Alright.  Thanks.\n\nBut I am curious.  How would this leak even be possible?  \n\narg.len==0 and arg.buf != strbuf_slopbuf would mean that somebody\nwho touched arg (either parse_arg() call or strbuf_addstr() call)\nallocated arg.buf but ended up not adding any byte (or added some\nbytes but at the end rewound with \"arg.len = 0\").\n\nAhh, strbuf_addstr() does an unconditional strbuf_grow().  I wonder\nif the following patch is a good idea in general, then (not as a\nreplacement for your fix, but as a general code hygiene---unless\nyou know you need more memory, don't allocate).\n\nI have a suspicion that this is optimizing for a wrong case, though,\nso I'd probably refrain from committing it.  If the caller often has\nan empty string, and if the caller feels like it is worth checking\nto omit such an extra \"opportunistic\" allocation, the caller can do\nso.\n\n strbuf.c | 8 ++++++++\n 1 file changed, 8 insertions(+)\n\ndiff --git c/strbuf.c w/strbuf.c\nindex 0d929e4e19..c0e58f5b95 100644\n--- c/strbuf.c\n+++ w/strbuf.c\n@@ -308,6 +308,8 @@ void strbuf_remove(struct strbuf *sb, size_t pos, size_t len)\n \n void strbuf_add(struct strbuf *sb, const void *data, size_t len)\n {\n+\tif (!len)\n+\t\treturn;\n \tstrbuf_grow(sb, len);\n \tmemcpy(sb->buf + sb->len, data, len);\n \tstrbuf_setlen(sb, sb->len + len);\n@@ -315,6 +317,8 @@ void strbuf_add(struct strbuf *sb, const void *data, size_t len)\n \n void strbuf_addbuf(struct strbuf *sb, const struct strbuf *sb2)\n {\n+\tif (!sb2->len)\n+\t\treturn;\n \tstrbuf_grow(sb, sb2->len);\n \tmemcpy(sb->buf + sb->len, sb2->buf, sb2->len);\n \tstrbuf_setlen(sb, sb->len + sb2->len);\n@@ -337,6 +341,8 @@ const char *strbuf_join_argv(struct strbuf *buf,\n \n void strbuf_addchars(struct strbuf *sb, int c, size_t n)\n {\n+\tif (!n)\n+\t\treturn;\n \tstrbuf_grow(sb, n);\n \tmemset(sb->buf + sb->len, c, n);\n \tstrbuf_setlen(sb, sb->len + n);\n@@ -805,6 +811,8 @@ void strbuf_addstr_xml_quoted(struct strbuf *buf, const char *s)\n static void strbuf_add_urlencode(struct strbuf *sb, const char *s, size_t len,\n \t\t\t\t char_predicate allow_unencoded_fn)\n {\n+\tif (!len)\n+\t\treturn;\n \tstrbuf_grow(sb, len);\n \twhile (len--) {\n \t\tchar ch = *s++;\n\n"},{"id":"495715","messageId":"ZlVnJm4OcPGHdC3P@tanuki","threadId":"61535","inReplyTo":"xmqqwmnf9mto.fsf@gitster.g","subject":"Re: [PATCH v2 00/21] Various memory leak fixes","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-28T05:09:58Z","receivedAt":"2024-05-28T05:10:05Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, May 27, 2024 at 10:38:59AM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> >> Also\n> >> \n> >>  https://github.com/git/git/actions/runs/9231313414/job/25401102951\n> >> \n> >> shows that t1460-refs-migrate fails on Windows.\n> >\n> > Hm, this one is curious. There are no leak logs at all, and the exit\n> > code is 139. Might be SIGSEGV, indicating that something else is going\n> > on here than a memory leak.\n> \n> Sorry, I wasn't clear enough.  I do not suspect this is about leaks\n> (and the failing job on Windows is not about leaks).\n\nI figured this one out now: Windows of course refuses to remove some of\nthe files because they are kept open. This shouldn't lead to a segfault\nitself. But we call `free_ref_cache()` on a partially initialized files\nref store, nad that function didn't handle the case when it is being\npassed a `NULL` pointer.\n\nI've addressed this by releasing the reftable ref store before trying to\nremove it from disk so that all files are being closed. But it also\nsurfaced another bug: our worktree code creates a copy of the main ref\nstore by accident because we set up `wt->is_current` _after_ we have\ncalled `get_worktree_ref_store()`.\n\nI fixed all of this and will send a new version in a bit.\n\nPatrick\n"},{"id":"495796","messageId":"CAOLa=ZQZ2ap9rgS-u0K9LsoZVfZ5hDPLY1ALZDSnfNmQXx35Bg@mail.gmail.com","threadId":"61535","inReplyTo":"ZlQr11P_C-KyO2DF@tanuki","subject":"Re: [PATCH v2 00/21] Various memory leak fixes","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-29T08:25:12Z","receivedAt":"2024-05-29T08:25:17Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> On Fri, May 24, 2024 at 07:10:09PM -0700, Junio C Hamano wrote:\n>> Patrick Steinhardt <ps@pks.im> writes:\n>>\n>> > this is the second version of my patch series that fixes various memory\n>> > leaks in Git. Changes compared to v1:\n>> >\n>> >   - t4153 and t7006 aren't marked as passing anymore. I thought they\n>> >     pass because most of these tests were skipped because of a missing\n>> >     TTY prerequisite both on my local machine, but also in our CI.\n>> >\n>> >   - Add another patch to install the Perl IO:Pty module on Alpine and\n>> >     Ubuntu. This fulfills the TTY prerequisite and thus surfaces the\n>> >     memory leaks in both of the above tests.\n>> >\n>> >   - Add another unit test for strvec that exercise replacing a string in\n>> >     the strvec with a copy of itself.\n>> >\n>> >   - A bunch of commit message improvements.\n>>\n>> Looking very good.  This seems to reveal existing leaks when merged\n>> to 'seen'; other topics that are not in 'master' may be introducing\n>> these leaks.  I'll see if a trial merge to 'next' is leak-free (in\n>> which case I'll merge it down to 'next') or there are other topics\n>> in 'next' that are leaking (in which case we'll play by ear---either\n>> mark the tests again as non-leak-free, or plug the leak if it seems\n>> trivial).\n>>\n>>  https://github.com/git/git/actions/runs/9231313414/job/25400998823\n>>\n>> says t1400-update-ref has many \"stdin symref-update\" things are\n>> failing.\n>\n> Indeed. The following diff fixes the leak:\n>\n>     diff --git a/builtin/update-ref.c b/builtin/update-ref.c\n>     index 7d2a419230..e54be9c429 100644\n>     --- a/builtin/update-ref.c\n>     +++ b/builtin/update-ref.c\n>     @@ -130,6 +130,8 @@ static char *parse_next_arg(const char **next)\n>\n>         if (arg.len)\n>             return strbuf_detach(&arg, NULL);\n>     +\n>     +\tstrbuf_release(&arg);\n>         return NULL;\n>      }\n>\n>\n> Karthik is out of office this week, so you may want to add this as a\n> \"SQUASH???\" commit on top of his topic branch to make \"seen\" pass.\n>\n\nThanks Patrick. Indeed I'm a bit slow on my responses, since I'm on\nvacation, but yeah, I too came about adding this as a fix.\n\nI'll mostly check for all tests and send a new version based on this\nseries soon.\n\n>> Also\n>>\n>>  https://github.com/git/git/actions/runs/9231313414/job/25401102951\n>>\n>> shows that t1460-refs-migrate fails on Windows.\n>\n> Hm, this one is curious. There are no leak logs at all, and the exit\n> code is 139. Might be SIGSEGV, indicating that something else is going\n> on here than a memory leak.\n>\n> I'll investigate.\n>\n> Patrick\n"},{"id":"495801","messageId":"20240529091633.GB1098944@coredump.intra.peff.net","threadId":"61535","inReplyTo":"ZlQr3tsDTSOGvFUQ@tanuki","subject":"Re: [PATCH v2 04/21] strbuf: fix leak when `appendwholeline()` fails with EOF","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-05-29T09:16:33Z","receivedAt":"2024-05-29T09:16:36Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Mon, May 27, 2024 at 08:44:46AM +0200, Patrick Steinhardt wrote:\n\n> > diff --git a/strbuf.c b/strbuf.c\n> > diff --git a/strbuf.c b/strbuf.c\n> > index e1076c9891..aed699c6bf 100644\n> > --- a/strbuf.c\n> > +++ b/strbuf.c\n> > @@ -656,10 +656,8 @@ int strbuf_getwholeline(struct strbuf *sb, FILE *fp, int term)\n> >  \t * we can just re-init, but otherwise we should make sure that our\n> >  \t * length is empty, and that the result is NUL-terminated.\n> >  \t */\n> > -\tif (!sb->buf)\n> > -\t\tstrbuf_init(sb, 0);\n> > -\telse\n> > -\t\tstrbuf_reset(sb);\n> > +\tFREE_AND_NULL(sb->buf);\n> > +\tstrbuf_init(sb, 0);\n> >  \treturn EOF;\n> >  }\n> >  #else\n> > \n> > But I think either of those would solve your leak, _and_ would help with\n> > similar leaks of strbuf_getwholeline() and friends.\n> \n> I'm not quite convinced that `strbuf_getwholeline()` should deallocate\n> the buffer for the caller, I think that makes for quite a confusing\n> calling convention. The caller may want to reuse the buffer for other\n> operations, and it feels hostile to release the buffer under their feet.\n> \n> The only edge case where I think it would make sense to free allocated\n> data is when being passed a not-yet-allocated strbuf. But I wonder\n> whether the added complexity would be worth it.\n\nI'm not sure what they'd reuse it for. We necessarily have to reset it\nbefore reading, so the contents are now garbage. The allocated buffer\ncould be reused, but since everybody has to call strbuf_grow() before\nassuming they can write, it's not a correctness issue, but only an\noptimization. But that optimization is pretty unlikely to matter. Since\nwe hit this code only on EOF or error, it's generally going to happen\nonce in a program, and not in a tight loop.\n\nIf we really cared, though, I think you could check sb->alloc before the\ncall to getdelim(), and then we'd know whether the original held an\nallocation or not (and we could restore its state). That's what other\nsyscall-ish strbuf functions like strbuf_readlink() and strbuf_getcwd()\ndo.\n\nThat said, I agree that leaks here are not going to be common. Most\ncallers are going to call it in a loop and unconditionally release at\nthe end, whether they get multiple lines or not. The \"append\" function\nis the odd man out by reading a single line into a new buffer[1].\n\nLooking through the results of:\n\n  git grep -P '(?<!while) \\(!?strbuf_get(whole)?line'\n\nI saw only one questionable case. builtin/difftool.c does:\n\n  if (strbuf_getline_nul(&lpath, fp))\n\tbreak;\n\nwithout freeing lpath. But then...it does not free it in the case that\nwe got a value, either! So I think it is leaking either way, and the\nsolution, to strbuf_release(&lpath) outside of the loop, would fix both\ncases.\n\n> I've been going through all callsites and couldn't spot any that doesn't\n> free the buffer on EOF. So I'd propose to leave this as-is and revisit\n> if we eventually see that this is causing more memory leaks.\n\nOK. I don't feel too strongly about it, but mostly thought it seemed\ninconsistent with the philosophy of those other strbuf functions.\n\n-Peff\n\n[1] I was surprised at first that strbuf_appendwholeline() uses that\n    extra copy, as the obvious implementation is just to skip the\n    strbuf_reset() call in getwholeline(), and then implement \"get\" as a\n    wrapper around \"append\" to add back in the reset call.\n\n    But that only works for the slower getc() implementation. For the\n    getdelim()-based one, there is no notion of append (the interface\n    would have to take an offset into the buffer).  We could probably\n    optimize this at the cost of repeating ourselves, but given that\n    there's only one probably-not-very-hot call to appendwholeline, it's\n    likely not worth it.\n"},{"id":"495803","messageId":"20240529092032.GC1098944@coredump.intra.peff.net","threadId":"61535","inReplyTo":"ZlQsKp2IHt2wankn@tanuki","subject":"Re: [PATCH v2 13/21] config: plug various memory leaks","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-05-29T09:20:32Z","receivedAt":"2024-05-29T09:20:34Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Mon, May 27, 2024 at 08:46:02AM +0200, Patrick Steinhardt wrote:\n\n> Indeed, I had the exact same observation. I've already got a patch\n> series that enables `-Wwrite-strings` and that adapts our codebase to\n> compile cleanly with it. I'll send that series once this one here has\n> landed.\n> \n> So my proposal would be to leave this patch as-is for the time being,\n> but revisit it once both patch series have landed. Does that work for\n> you?\n\nYeah, I can certainly live with that. One of my biggest concerns was\nthat your patch would invalidate the investigation and work I had\nalready done, and I'd fail to follow up. But if you are putting it onto\nyour todo list, then that is even less work for me. ;)\n\n-Peff\n"},{"id":"495811","messageId":"ZlcQjvAS-27S-mjw@tanuki","threadId":"61535","inReplyTo":"20240529091633.GB1098944@coredump.intra.peff.net","subject":"Re: [PATCH v2 04/21] strbuf: fix leak when `appendwholeline()` fails with EOF","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-29T11:25:02Z","receivedAt":"2024-05-29T11:25:09Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, May 29, 2024 at 05:16:33AM -0400, Jeff King wrote:\n> On Mon, May 27, 2024 at 08:44:46AM +0200, Patrick Steinhardt wrote:\n> > > diff --git a/strbuf.c b/strbuf.c\n> > > diff --git a/strbuf.c b/strbuf.c\n> > > index e1076c9891..aed699c6bf 100644\n> > > --- a/strbuf.c\n> > > +++ b/strbuf.c\n> > > @@ -656,10 +656,8 @@ int strbuf_getwholeline(struct strbuf *sb, FILE *fp, int term)\n> > >  \t * we can just re-init, but otherwise we should make sure that our\n> > >  \t * length is empty, and that the result is NUL-terminated.\n> > >  \t */\n> > > -\tif (!sb->buf)\n> > > -\t\tstrbuf_init(sb, 0);\n> > > -\telse\n> > > -\t\tstrbuf_reset(sb);\n> > > +\tFREE_AND_NULL(sb->buf);\n> > > +\tstrbuf_init(sb, 0);\n> > >  \treturn EOF;\n> > >  }\n> > >  #else\n> > > \n> > > But I think either of those would solve your leak, _and_ would help with\n> > > similar leaks of strbuf_getwholeline() and friends.\n> > \n> > I'm not quite convinced that `strbuf_getwholeline()` should deallocate\n> > the buffer for the caller, I think that makes for quite a confusing\n> > calling convention. The caller may want to reuse the buffer for other\n> > operations, and it feels hostile to release the buffer under their feet.\n> > \n> > The only edge case where I think it would make sense to free allocated\n> > data is when being passed a not-yet-allocated strbuf. But I wonder\n> > whether the added complexity would be worth it.\n> \n> I'm not sure what they'd reuse it for. We necessarily have to reset it\n> before reading, so the contents are now garbage. The allocated buffer\n> could be reused, but since everybody has to call strbuf_grow() before\n> assuming they can write, it's not a correctness issue, but only an\n> optimization. But that optimization is pretty unlikely to matter. Since\n> we hit this code only on EOF or error, it's generally going to happen\n> once in a program, and not in a tight loop.\n> \n> If we really cared, though, I think you could check sb->alloc before the\n> call to getdelim(), and then we'd know whether the original held an\n> allocation or not (and we could restore its state). That's what other\n> syscall-ish strbuf functions like strbuf_readlink() and strbuf_getcwd()\n> do.\n\nAh, I didn't know that we did similar things in other strbuf functions.\nWith that precedence I think it's less ugly to do this dance.\n\n> That said, I agree that leaks here are not going to be common. Most\n> callers are going to call it in a loop and unconditionally release at\n> the end, whether they get multiple lines or not. The \"append\" function\n> is the odd man out by reading a single line into a new buffer[1].\n> \n> Looking through the results of:\n> \n>   git grep -P '(?<!while) \\(!?strbuf_get(whole)?line'\n> \n> I saw only one questionable case. builtin/difftool.c does:\n> \n>   if (strbuf_getline_nul(&lpath, fp))\n> \tbreak;\n> \n> without freeing lpath. But then...it does not free it in the case that\n> we got a value, either! So I think it is leaking either way, and the\n> solution, to strbuf_release(&lpath) outside of the loop, would fix both\n> cases.\n\nIndeed. We also didn't free `rpath` and `info`. I do have a follow up to\nthis series already, so let me add those leak fixes to it.\n\n> > I've been going through all callsites and couldn't spot any that doesn't\n> > free the buffer on EOF. So I'd propose to leave this as-is and revisit\n> > if we eventually see that this is causing more memory leaks.\n> \n> OK. I don't feel too strongly about it, but mostly thought it seemed\n> inconsistent with the philosophy of those other strbuf functions.\n\nI get where you're coming from now with the additional info that other\nsyscall-ish functions do a similar dance. I'll refrain from rerolling\nthis series just to fix this in a different way, also because neither of\nus did spot any additional leaks caused by this.\n\nPatrick\n"},{"id":"495885","messageId":"20240530063857.GA1942535@coredump.intra.peff.net","threadId":"61535","inReplyTo":"cover.1716810168.git.ps@pks.im","subject":"[PATCH 0/5] add-ons for ps/leakfixes","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-05-30T06:38:57Z","receivedAt":"2024-05-30T06:39:01Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"Here are a few patches to go on top of ps/leakfixes. Patches 1 and 3 fix\nfunctional problems noticed by Coverity, and then the others are just\ncleanups I noticed while there.\n\n  [1/5]: t-strvec: use va_end() to match va_start()\n  [2/5]: t-strvec: mark variable-arg helper with LAST_ARG_MUST_BE_NULL\n  [3/5]: mv: move src_dir cleanup to end of cmd_mv()\n  [4/5]: mv: factor out empty src_dir removal\n  [5/5]: mv: replace src_dir with a strvec\n\n builtin/mv.c            | 50 +++++++++++++++++++++--------------------\n t/unit-tests/t-strvec.c |  3 +++\n 2 files changed, 29 insertions(+), 24 deletions(-)\n\n-Peff\n"},{"id":"495886","messageId":"20240530063932.GA1949704@coredump.intra.peff.net","threadId":"61535","inReplyTo":"20240530063857.GA1942535@coredump.intra.peff.net","subject":"[PATCH 1/5] t-strvec: use va_end() to match va_start()","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-05-30T06:39:32Z","receivedAt":"2024-05-30T06:39:33Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"Our check_strvec_loc() helper uses a variable argument list. When we\nva_start(), we must be sure to va_end() before leaving the function.\nThis is required by the standard (though the effect of forgetting will\nvary between platforms).\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\n t/unit-tests/t-strvec.c | 2 ++\n 1 file changed, 2 insertions(+)\n\ndiff --git a/t/unit-tests/t-strvec.c b/t/unit-tests/t-strvec.c\nindex f17fb10d9e..6c1465ee61 100644\n--- a/t/unit-tests/t-strvec.c\n+++ b/t/unit-tests/t-strvec.c\n@@ -22,11 +22,13 @@ static void check_strvec_loc(const char *loc, struct strvec *vec, ...)\n \t\t\tstrbuf_addf(&msg, \"strvec index %\"PRIuMAX, (uintmax_t) nr);\n \t\t\ttest_assert(loc, msg.buf, 0);\n \t\t\tstrbuf_release(&msg);\n+\t\t\tva_end(ap);\n \t\t\treturn;\n \t\t}\n \n \t\tnr++;\n \t}\n+\tva_end(ap);\n \n \tcheck_uint(vec->nr, ==, nr);\n \tcheck_uint(vec->alloc, >=, nr);\n-- \n2.45.1.692.gbe047d9c60\n\n"},{"id":"495887","messageId":"20240530063956.GB1949704@coredump.intra.peff.net","threadId":"61535","inReplyTo":"20240530063857.GA1942535@coredump.intra.peff.net","subject":"[PATCH 2/5] t-strvec: mark variable-arg helper with LAST_ARG_MUST_BE_NULL","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-05-30T06:39:56Z","receivedAt":"2024-05-30T06:39:57Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"This will let the compiler catch a problem like:\n\n  /* oops, we forgot the NULL */\n  check_strvec(&vec, \"foo\");\n\nrather than triggering undefined behavior at runtime.\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\n t/unit-tests/t-strvec.c | 1 +\n 1 file changed, 1 insertion(+)\n\ndiff --git a/t/unit-tests/t-strvec.c b/t/unit-tests/t-strvec.c\nindex 6c1465ee61..d4615ab06d 100644\n--- a/t/unit-tests/t-strvec.c\n+++ b/t/unit-tests/t-strvec.c\n@@ -4,6 +4,7 @@\n \n #define check_strvec(vec, ...) \\\n \tcheck_strvec_loc(TEST_LOCATION(), vec, __VA_ARGS__)\n+LAST_ARG_MUST_BE_NULL\n static void check_strvec_loc(const char *loc, struct strvec *vec, ...)\n {\n \tva_list ap;\n-- \n2.45.1.692.gbe047d9c60\n\n"},{"id":"495888","messageId":"20240530064422.GC1949704@coredump.intra.peff.net","threadId":"61535","inReplyTo":"20240530063857.GA1942535@coredump.intra.peff.net","subject":"[PATCH 3/5] mv: move src_dir cleanup to end of cmd_mv()","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-05-30T06:44:22Z","receivedAt":"2024-05-30T06:44:24Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"Commit b6f51e3db9 (mv: cleanup empty WORKING_DIRECTORY, 2022-08-09)\nadded an auxiliary array where we store directory arguments that we see\nwhile processing the incoming arguments. After actually moving things,\nwe then use that array to remove now-empty directories, and then\nimmediately free the array.\n\nBut if the actual move queues any errors in only_match_skip_worktree,\nthat can cause us to jump straight to the \"out\" label to clean up,\nskipping the free() and leaking the array.\n\nLet's push the free() down past the \"out\" label so that we always clean\nup (the array is initialized to NULL, so this is always safe). We'll\nhold on to the memory a little longer than necessary, but clarity is\nmore important than micro-optimizing here.\n\nNote that the adjacent \"a_src_dir\" strbuf does not suffer the same\nproblem; it is only allocated during the removal step.\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\nReported as \"new\" by Coverity, but I think that is only because of the\n\"goto out\". Before your recent patches it was a straight \"return\", which\nwas even worse. ;)\n\n builtin/mv.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/builtin/mv.c b/builtin/mv.c\nindex 81ca910de6..852b4e92c1 100644\n--- a/builtin/mv.c\n+++ b/builtin/mv.c\n@@ -556,7 +556,6 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t}\n \n \tstrbuf_release(&a_src_dir);\n-\tfree(src_dir);\n \n \tif (dirty_paths.nr)\n \t\tadvise_on_moving_dirty_path(&dirty_paths);\n@@ -571,6 +570,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \tret = 0;\n \n out:\n+\tfree(src_dir);\n \tfree(dst_w_slash);\n \tstring_list_clear(&src_for_dst, 0);\n \tstring_list_clear(&dirty_paths, 0);\n-- \n2.45.1.692.gbe047d9c60\n\n"},{"id":"495889","messageId":"20240530064521.GD1949704@coredump.intra.peff.net","threadId":"61535","inReplyTo":"20240530063857.GA1942535@coredump.intra.peff.net","subject":"[PATCH 4/5] mv: factor out empty src_dir removal","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-05-30T06:45:21Z","receivedAt":"2024-05-30T06:45:22Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"This pulls the loop added by b6f51e3db9 (mv: cleanup empty\nWORKING_DIRECTORY, 2022-08-09) into a sub-function. That reduces clutter\nin cmd_mv() and makes it easier to see that the lifetime of the\na_src_dir strbuf is limited to this code (and thus its cleanup doesn't\nneed to go after the \"out\" label).\n\nAnother option would be to just declare the strbuf inside the loop,\nsince it is only used there. But this refactor retains the existing\nproperty that we can reuse the allocated buffer for each iteration of\nthe loop. That optimization is probably overkill, but I think the\nsub-function is more readable anyway, and then keeping the optimization\nis basically free.\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\n builtin/mv.c | 42 +++++++++++++++++++++++-------------------\n 1 file changed, 23 insertions(+), 19 deletions(-)\n\ndiff --git a/builtin/mv.c b/builtin/mv.c\nindex 852b4e92c1..01725e4a20 100644\n--- a/builtin/mv.c\n+++ b/builtin/mv.c\n@@ -156,6 +156,28 @@ static int empty_dir_has_sparse_contents(const char *name)\n \treturn ret;\n }\n \n+static void remove_empty_src_dirs(const char **src_dir, size_t src_dir_nr)\n+{\n+\tsize_t i;\n+\tstruct strbuf a_src_dir = STRBUF_INIT;\n+\n+\tfor (i = 0; i < src_dir_nr; i++) {\n+\t\tint dummy;\n+\t\tstrbuf_addstr(&a_src_dir, src_dir[i]);\n+\t\t/*\n+\t\t * if entries under a_src_dir are all moved away,\n+\t\t * recursively remove a_src_dir to cleanup\n+\t\t */\n+\t\tif (index_range_of_same_dir(a_src_dir.buf, a_src_dir.len,\n+\t\t\t\t\t    &dummy, &dummy) < 1) {\n+\t\t\tremove_dir_recursively(&a_src_dir, 0);\n+\t\t}\n+\t\tstrbuf_reset(&a_src_dir);\n+\t}\n+\n+\tstrbuf_release(&a_src_dir);\n+}\n+\n int cmd_mv(int argc, const char **argv, const char *prefix)\n {\n \tint i, flags, gitmodules_modified = 0;\n@@ -177,7 +199,6 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \tchar *dst_w_slash = NULL;\n \tconst char **src_dir = NULL;\n \tint src_dir_nr = 0, src_dir_alloc = 0;\n-\tstruct strbuf a_src_dir = STRBUF_INIT;\n \tenum update_mode *modes, dst_mode = 0;\n \tstruct stat st, dest_st;\n \tstruct string_list src_for_dst = STRING_LIST_INIT_DUP;\n@@ -538,24 +559,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\t}\n \t}\n \n-\t/*\n-\t * cleanup the empty src_dirs\n-\t */\n-\tfor (i = 0; i < src_dir_nr; i++) {\n-\t\tint dummy;\n-\t\tstrbuf_addstr(&a_src_dir, src_dir[i]);\n-\t\t/*\n-\t\t * if entries under a_src_dir are all moved away,\n-\t\t * recursively remove a_src_dir to cleanup\n-\t\t */\n-\t\tif (index_range_of_same_dir(a_src_dir.buf, a_src_dir.len,\n-\t\t\t\t\t    &dummy, &dummy) < 1) {\n-\t\t\tremove_dir_recursively(&a_src_dir, 0);\n-\t\t}\n-\t\tstrbuf_reset(&a_src_dir);\n-\t}\n-\n-\tstrbuf_release(&a_src_dir);\n+\tremove_empty_src_dirs(src_dir, src_dir_nr);\n \n \tif (dirty_paths.nr)\n \t\tadvise_on_moving_dirty_path(&dirty_paths);\n-- \n2.45.1.692.gbe047d9c60\n\n"},{"id":"495890","messageId":"20240530064638.GE1949704@coredump.intra.peff.net","threadId":"61535","inReplyTo":"20240530063857.GA1942535@coredump.intra.peff.net","subject":"[PATCH 5/5] mv: replace src_dir with a strvec","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-05-30T06:46:38Z","receivedAt":"2024-05-30T06:46:40Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"We manually manage the src_dir array with ALLOC_GROW. Using a strvec is\na little more ergonomic, and makes the memory ownership more clear. It\ndoes mean that we copy the strings (which were otherwise just pointers\ninto the \"sources\" strvec), but using the same rationale as 9fcd9e4e72\n(builtin/mv duplicate string list memory, 2024-05-27), it's just not\nenough to be worth worrying about here.\n\nAs a bonus, this gets rid of some \"int\"s used for allocation management\n(though in practice these were limited to command-line sizes and thus\nnot overflowable).\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\n builtin/mv.c | 10 ++++------\n 1 file changed, 4 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/mv.c b/builtin/mv.c\nindex 01725e4a20..6c69033c5f 100644\n--- a/builtin/mv.c\n+++ b/builtin/mv.c\n@@ -197,8 +197,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \tstruct strvec submodule_gitfiles_to_free = STRVEC_INIT;\n \tconst char **submodule_gitfiles;\n \tchar *dst_w_slash = NULL;\n-\tconst char **src_dir = NULL;\n-\tint src_dir_nr = 0, src_dir_alloc = 0;\n+\tstruct strvec src_dir = STRVEC_INIT;\n \tenum update_mode *modes, dst_mode = 0;\n \tstruct stat st, dest_st;\n \tstruct string_list src_for_dst = STRING_LIST_INIT_DUP;\n@@ -344,8 +343,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\t\t/* last - first >= 1 */\n \t\t\tmodes[i] |= WORKING_DIRECTORY;\n \n-\t\t\tALLOC_GROW(src_dir, src_dir_nr + 1, src_dir_alloc);\n-\t\t\tsrc_dir[src_dir_nr++] = src;\n+\t\t\tstrvec_push(&src_dir, src);\n \n \t\t\tn = argc + last - first;\n \t\t\tREALLOC_ARRAY(modes, n);\n@@ -559,7 +557,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\t}\n \t}\n \n-\tremove_empty_src_dirs(src_dir, src_dir_nr);\n+\tremove_empty_src_dirs(src_dir.v, src_dir.nr);\n \n \tif (dirty_paths.nr)\n \t\tadvise_on_moving_dirty_path(&dirty_paths);\n@@ -574,7 +572,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \tret = 0;\n \n out:\n-\tfree(src_dir);\n+\tstrvec_clear(&src_dir);\n \tfree(dst_w_slash);\n \tstring_list_clear(&src_for_dst, 0);\n \tstring_list_clear(&dirty_paths, 0);\n-- \n2.45.1.692.gbe047d9c60\n"},{"id":"495893","messageId":"Zlgk_MMDGlphfO9U@tanuki","threadId":"61535","inReplyTo":"20240530064422.GC1949704@coredump.intra.peff.net","subject":"Re: [PATCH 3/5] mv: move src_dir cleanup to end of cmd_mv()","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-30T07:04:28Z","receivedAt":"2024-05-30T07:04:35Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, May 30, 2024 at 02:44:22AM -0400, Jeff King wrote:\n> Commit b6f51e3db9 (mv: cleanup empty WORKING_DIRECTORY, 2022-08-09)\n> added an auxiliary array where we store directory arguments that we see\n> while processing the incoming arguments. After actually moving things,\n> we then use that array to remove now-empty directories, and then\n> immediately free the array.\n> \n> But if the actual move queues any errors in only_match_skip_worktree,\n> that can cause us to jump straight to the \"out\" label to clean up,\n> skipping the free() and leaking the array.\n> \n> Let's push the free() down past the \"out\" label so that we always clean\n> up (the array is initialized to NULL, so this is always safe). We'll\n> hold on to the memory a little longer than necessary, but clarity is\n> more important than micro-optimizing here.\n> \n> Note that the adjacent \"a_src_dir\" strbuf does not suffer the same\n> problem; it is only allocated during the removal step.\n> \n> Signed-off-by: Jeff King <peff@peff.net>\n> ---\n> Reported as \"new\" by Coverity, but I think that is only because of the\n> \"goto out\". Before your recent patches it was a straight \"return\", which\n> was even worse. ;)\n\nOuf of curiosity, did you check whether this makes any additional tests\npass with SANITIZE=leak?\n\nThe fix itself looks good to me, thanks.\n\nPatrick\n"},{"id":"495894","messageId":"ZlglMCI3GrN28A_R@tanuki","threadId":"61535","inReplyTo":"20240530063857.GA1942535@coredump.intra.peff.net","subject":"Re: [PATCH 0/5] add-ons for ps/leakfixes","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-30T07:05:20Z","receivedAt":"2024-05-30T07:05:25Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, May 30, 2024 at 02:38:57AM -0400, Jeff King wrote:\n> Here are a few patches to go on top of ps/leakfixes. Patches 1 and 3 fix\n> functional problems noticed by Coverity, and then the others are just\n> cleanups I noticed while there.\n> \n>   [1/5]: t-strvec: use va_end() to match va_start()\n>   [2/5]: t-strvec: mark variable-arg helper with LAST_ARG_MUST_BE_NULL\n>   [3/5]: mv: move src_dir cleanup to end of cmd_mv()\n>   [4/5]: mv: factor out empty src_dir removal\n>   [5/5]: mv: replace src_dir with a strvec\n\nThanks for the follow up, and thank especially for improving\nbuiltin/mv.c a bit further. The patches all look good to me.\n\nPatrick\n"},{"id":"495898","messageId":"20240530071627.GE1949834@coredump.intra.peff.net","threadId":"61535","inReplyTo":"ZlcQjvAS-27S-mjw@tanuki","subject":"Re: [PATCH v2 04/21] strbuf: fix leak when `appendwholeline()` fails with EOF","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-05-30T07:16:27Z","receivedAt":"2024-05-30T07:16:28Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Wed, May 29, 2024 at 01:25:02PM +0200, Patrick Steinhardt wrote:\n\n> > If we really cared, though, I think you could check sb->alloc before the\n> > call to getdelim(), and then we'd know whether the original held an\n> > allocation or not (and we could restore its state). That's what other\n> > syscall-ish strbuf functions like strbuf_readlink() and strbuf_getcwd()\n> > do.\n> \n> Ah, I didn't know that we did similar things in other strbuf functions.\n> With that precedence I think it's less ugly to do this dance.\n\nYeah, I probably should have explained that better in my earlier email. ;)\n\n> > I saw only one questionable case. builtin/difftool.c does:\n> > \n> >   if (strbuf_getline_nul(&lpath, fp))\n> > \tbreak;\n> > \n> > without freeing lpath. But then...it does not free it in the case that\n> > we got a value, either! So I think it is leaking either way, and the\n> > solution, to strbuf_release(&lpath) outside of the loop, would fix both\n> > cases.\n> \n> Indeed. We also didn't free `rpath` and `info`. I do have a follow up to\n> this series already, so let me add those leak fixes to it.\n\nGreat! Thanks (as usual) for being receptive to me piling more things on\nyour todo list. :)\n\n-Peff\n"},{"id":"495899","messageId":"20240530072159.GF1949834@coredump.intra.peff.net","threadId":"61535","inReplyTo":"Zlgk_MMDGlphfO9U@tanuki","subject":"Re: [PATCH 3/5] mv: move src_dir cleanup to end of cmd_mv()","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-05-30T07:21:59Z","receivedAt":"2024-05-30T07:22:01Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Thu, May 30, 2024 at 09:04:28AM +0200, Patrick Steinhardt wrote:\n\n> > But if the actual move queues any errors in only_match_skip_worktree,\n> > that can cause us to jump straight to the \"out\" label to clean up,\n> > skipping the free() and leaking the array.\n> > \n> > Let's push the free() down past the \"out\" label so that we always clean\n> > up (the array is initialized to NULL, so this is always safe). We'll\n> > hold on to the memory a little longer than necessary, but clarity is\n> > more important than micro-optimizing here.\n> [...]\n> \n> Ouf of curiosity, did you check whether this makes any additional tests\n> pass with SANITIZE=leak?\n\nNo, I didn't. I think you can only trigger it with a sparse index, at\nwhich point it seemed like diminishing returns to try to reproduce.\n\nBut running in \"check\" mode is not too hard...\n\n...time passes...\n\nLooks like no. The obvious candidate would be t7002-mv-sparse-checkout,\nbut it looks like the sparse-checkout code has minor leaks itself.\n\n-Peff\n"},{"id":"495900","messageId":"ZlgpsrKN7N38V2GP@tanuki","threadId":"61535","inReplyTo":"20240530072159.GF1949834@coredump.intra.peff.net","subject":"Re: [PATCH 3/5] mv: move src_dir cleanup to end of cmd_mv()","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-30T07:24:34Z","receivedAt":"2024-05-30T07:24:40Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, May 30, 2024 at 03:21:59AM -0400, Jeff King wrote:\n> On Thu, May 30, 2024 at 09:04:28AM +0200, Patrick Steinhardt wrote:\n> \n> > > But if the actual move queues any errors in only_match_skip_worktree,\n> > > that can cause us to jump straight to the \"out\" label to clean up,\n> > > skipping the free() and leaking the array.\n> > > \n> > > Let's push the free() down past the \"out\" label so that we always clean\n> > > up (the array is initialized to NULL, so this is always safe). We'll\n> > > hold on to the memory a little longer than necessary, but clarity is\n> > > more important than micro-optimizing here.\n> > [...]\n> > \n> > Ouf of curiosity, did you check whether this makes any additional tests\n> > pass with SANITIZE=leak?\n> \n> No, I didn't. I think you can only trigger it with a sparse index, at\n> which point it seemed like diminishing returns to try to reproduce.\n> \n> But running in \"check\" mode is not too hard...\n> \n> ...time passes...\n> \n> Looks like no. The obvious candidate would be t7002-mv-sparse-checkout,\n> but it looks like the sparse-checkout code has minor leaks itself.\n\nOkay, thanks for double checking! I was mostly asking because I plan to\nsend another leak fixes series to the mailing list later this week.\n\nPatrick\n"},{"id":"495902","messageId":"20240530081512.GG1949834@coredump.intra.peff.net","threadId":"61535","inReplyTo":"ZlgpsrKN7N38V2GP@tanuki","subject":"Re: [PATCH 3/5] mv: move src_dir cleanup to end of cmd_mv()","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-05-30T08:15:12Z","receivedAt":"2024-05-30T08:15:14Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Thu, May 30, 2024 at 09:24:34AM +0200, Patrick Steinhardt wrote:\n\n> > Looks like no. The obvious candidate would be t7002-mv-sparse-checkout,\n> > but it looks like the sparse-checkout code has minor leaks itself.\n> \n> Okay, thanks for double checking! I was mostly asking because I plan to\n> send another leak fixes series to the mailing list later this week.\n\nOK, good news. t7002 _does_ trigger the leak fixed in my patch. You just\ncan't tell because of all of the sparse-checkout leaks. ;)\n\nThe (messy) patch below gets it to a leak-free state when applied on\ntop. Do you want me to do another mini-series with it, or do you want to\njust roll it into what you're doing (I won't be surprised if you've\nalready found some of these).\n\n-Peff\n\ndiff --git a/builtin/sparse-checkout.c b/builtin/sparse-checkout.c\nindex 0f52e25249..1ed9dfa886 100644\n--- a/builtin/sparse-checkout.c\n+++ b/builtin/sparse-checkout.c\n@@ -311,6 +311,8 @@ static void write_cone_to_file(FILE *fp, struct pattern_list *pl)\n \t\tfprintf(fp, \"%s/\\n\", pattern);\n \t\tfree(pattern);\n \t}\n+\n+\tstring_list_clear(&sl, 0);\n }\n \n static int write_patterns_and_update(struct pattern_list *pl)\n@@ -471,6 +473,7 @@ static int sparse_checkout_init(int argc, const char **argv, const char *prefix)\n \t/* If we already have a sparse-checkout file, use it. */\n \tif (res >= 0) {\n \t\tfree(sparse_filename);\n+\t\tclear_pattern_list(&pl);\n \t\treturn update_working_directory(NULL);\n \t}\n \n@@ -486,6 +489,7 @@ static int sparse_checkout_init(int argc, const char **argv, const char *prefix)\n \t\t\tdie(_(\"failed to open '%s'\"), sparse_filename);\n \n \t\tfree(sparse_filename);\n+\t\tclear_pattern_list(&pl);\n \t\tfprintf(fp, \"/*\\n!/*/\\n\");\n \t\tfclose(fp);\n \t\treturn 0;\n@@ -525,6 +529,10 @@ static void insert_recursive_pattern(struct pattern_list *pl, struct strbuf *pat\n \n \t\tif (!hashmap_get_entry(&pl->parent_hashmap, e, ent, NULL))\n \t\t\thashmap_add(&pl->parent_hashmap, &e->ent);\n+\t\telse {\n+\t\t\tfree(e->pattern);\n+\t\t\tfree(e);\n+\t\t}\n \t}\n }\n \n@@ -891,7 +899,6 @@ static int sparse_checkout_disable(int argc, const char **argv,\n \t\tOPT_END(),\n \t};\n \tstruct pattern_list pl;\n-\tstruct strbuf match_all = STRBUF_INIT;\n \n \t/*\n \t * We do not exit early if !core_apply_sparse_checkout; due to the\n@@ -917,8 +924,7 @@ static int sparse_checkout_disable(int argc, const char **argv,\n \tpl.use_cone_patterns = 0;\n \tcore_apply_sparse_checkout = 1;\n \n-\tstrbuf_addstr(&match_all, \"/*\");\n-\tadd_pattern(strbuf_detach(&match_all, NULL), empty_base, 0, &pl, 0);\n+\tadd_pattern(\"/*\", empty_base, 0, &pl, 0);\n \n \tprepare_repo_settings(the_repository);\n \tthe_repository->settings.sparse_index = 0;\ndiff --git a/dir.c b/dir.c\nindex 2d83f3311a..5769c4e693 100644\n--- a/dir.c\n+++ b/dir.c\n@@ -799,6 +799,8 @@ static void add_pattern_to_hashsets(struct pattern_list *pl, struct path_pattern\n \n \tif (given->patternlen > 2 &&\n \t    !strcmp(given->pattern + given->patternlen - 2, \"/*\")) {\n+\t\tstruct pattern_entry *old;\n+\n \t\tif (!(given->flags & PATTERN_FLAG_NEGATIVE)) {\n \t\t\t/* Not a cone pattern. */\n \t\t\twarning(_(\"unrecognized pattern: '%s'\"), given->pattern);\n@@ -824,7 +826,11 @@ static void add_pattern_to_hashsets(struct pattern_list *pl, struct path_pattern\n \t\t}\n \n \t\thashmap_add(&pl->parent_hashmap, &translated->ent);\n-\t\thashmap_remove(&pl->recursive_hashmap, &translated->ent, &data);\n+\t\told = hashmap_remove_entry(&pl->recursive_hashmap, translated, ent, &data);\n+\t\tif (old) {\n+\t\t\tfree(old->pattern);\n+\t\t\tfree(old);\n+\t\t}\n \t\tfree(data);\n \t\treturn;\n \t}\n@@ -855,6 +861,7 @@ static void add_pattern_to_hashsets(struct pattern_list *pl, struct path_pattern\n \n clear_hashmaps:\n \twarning(_(\"disabling cone pattern matching\"));\n+\t/* should free ent->pattern too; refactor clear_pattern_list? */\n \thashmap_clear_and_free(&pl->parent_hashmap, struct pattern_entry, ent);\n \thashmap_clear_and_free(&pl->recursive_hashmap, struct pattern_entry, ent);\n \tpl->use_cone_patterns = 0;\n@@ -950,13 +957,20 @@ static int read_skip_worktree_file_from_index(struct index_state *istate,\n  */\n void clear_pattern_list(struct pattern_list *pl)\n {\n+\tstruct hashmap_iter iter;\n+\tstruct pattern_entry *entry;\n \tint i;\n \n \tfor (i = 0; i < pl->nr; i++)\n \t\tfree(pl->patterns[i]);\n \tfree(pl->patterns);\n \tfree(pl->filebuf);\n+\n+\thashmap_for_each_entry(&pl->recursive_hashmap, &iter, entry, ent)\n+\t\tfree(entry->pattern);\n \thashmap_clear_and_free(&pl->recursive_hashmap, struct pattern_entry, ent);\n+\thashmap_for_each_entry(&pl->parent_hashmap, &iter, entry, ent)\n+\t\tfree(entry->pattern);\n \thashmap_clear_and_free(&pl->parent_hashmap, struct pattern_entry, ent);\n \n \tmemset(pl, 0, sizeof(*pl));\n-- \n2.45.1.692.gbe047d9c60\n\n"},{"id":"495904","messageId":"Zlg2nWhRaZxpO9fm@tanuki","threadId":"61535","inReplyTo":"20240530081512.GG1949834@coredump.intra.peff.net","subject":"Re: [PATCH 3/5] mv: move src_dir cleanup to end of cmd_mv()","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-30T08:19:41Z","receivedAt":"2024-05-30T08:19:47Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, May 30, 2024 at 04:15:12AM -0400, Jeff King wrote:\n> On Thu, May 30, 2024 at 09:24:34AM +0200, Patrick Steinhardt wrote:\n> \n> > > Looks like no. The obvious candidate would be t7002-mv-sparse-checkout,\n> > > but it looks like the sparse-checkout code has minor leaks itself.\n> > \n> > Okay, thanks for double checking! I was mostly asking because I plan to\n> > send another leak fixes series to the mailing list later this week.\n> \n> OK, good news. t7002 _does_ trigger the leak fixed in my patch. You just\n> can't tell because of all of the sparse-checkout leaks. ;)\n> \n> The (messy) patch below gets it to a leak-free state when applied on\n> top. Do you want me to do another mini-series with it, or do you want to\n> just roll it into what you're doing (I won't be surprised if you've\n> already found some of these).\n\nWell, with all the leaks that we have in our codebase it's not all that\nlikely that we actually work on the same ones :) For the record, my\npatch series is 29 patches long by now and makes another 76 test suites\npass. But none of those fixes touch any of the code you touched.\n\nSo given that my patch series is already way too long, and given that\nthere shouldn't be any conflicts, I wouldn't mind if this was spun out\ninto a separate series.\n\nPatrick\n"},{"id":"495910","messageId":"20240530082818.GA2668158@coredump.intra.peff.net","threadId":"61535","inReplyTo":"Zlg2nWhRaZxpO9fm@tanuki","subject":"Re: [PATCH 3/5] mv: move src_dir cleanup to end of cmd_mv()","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-05-30T08:28:18Z","receivedAt":"2024-05-30T08:28:19Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Thu, May 30, 2024 at 10:19:41AM +0200, Patrick Steinhardt wrote:\n\n> > The (messy) patch below gets it to a leak-free state when applied on\n> > top. Do you want me to do another mini-series with it, or do you want to\n> > just roll it into what you're doing (I won't be surprised if you've\n> > already found some of these).\n> \n> Well, with all the leaks that we have in our codebase it's not all that\n> likely that we actually work on the same ones :) For the record, my\n> patch series is 29 patches long by now and makes another 76 test suites\n> pass. But none of those fixes touch any of the code you touched.\n> \n> So given that my patch series is already way too long, and given that\n> there shouldn't be any conflicts, I wouldn't mind if this was spun out\n> into a separate series.\n\nSounds good. I just wanted to make sure we didn't end up duplicating or\nconflicting.\n\n-Peff\n"},{"id":"495960","messageId":"xmqqo78nqpl2.fsf@gitster.g","threadId":"61535","inReplyTo":"20240530064638.GE1949704@coredump.intra.peff.net","subject":"Re: [PATCH 5/5] mv: replace src_dir with a strvec","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-30T15:36:25Z","receivedAt":"2024-05-30T15:36:32Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n> We manually manage the src_dir array with ALLOC_GROW. Using a strvec is\n> a little more ergonomic, and makes the memory ownership more clear. It\n> does mean that we copy the strings (which were otherwise just pointers\n> into the \"sources\" strvec), but using the same rationale as 9fcd9e4e72\n> (builtin/mv duplicate string list memory, 2024-05-27), it's just not\n> enough to be worth worrying about here.\n\nHmph, the rationale given by 9fcd9e4e (builtin/mv duplicate string\nlist memory, 2024-05-27) essentially is \"the number of elements are\nthe same as the number of command line parameters\", but I do not\nthink that is quite correct.\n\nWhen you do \"mv srcA srcB ... dst\", you'd inspect the command line\narguments from left to right, notice that srcA is a directory, find\nthe cache entries for paths that are inside srcA, append the paths\nin that directory to source[] and destination[] array, and extend\nargc.  \"for (i = 0; i < argc; i++)\" loop that appends one element to\nsrc_for_dst per iteration ends up running the number of paths being\nmoved, which can be order of magnitude more than the command line\nparameters.\n\nOf course, if we needed to make copies for correctness reasons (or\nto clarify memory ownership semantics), that alone may be a good\njustification and we do not need an excuse \"it's just a handful of\nelements anyway\" to begin with.\n\nAnyway, that is about somebody else's patch, not this one ;-).\n\nThe rationale *does* apply to this change; src_dir is a list of\ndirectories we found on the command line, so the number of elements\nin it is reasonably bounded.\n\n> As a bonus, this gets rid of some \"int\"s used for allocation management\n> (though in practice these were limited to command-line sizes and thus\n> not overflowable).\n\nAgain, correct.\n\n> Signed-off-by: Jeff King <peff@peff.net>\n> ---\n>  builtin/mv.c | 10 ++++------\n>  1 file changed, 4 insertions(+), 6 deletions(-)\n>\n> diff --git a/builtin/mv.c b/builtin/mv.c\n> index 01725e4a20..6c69033c5f 100644\n> --- a/builtin/mv.c\n> +++ b/builtin/mv.c\n> @@ -197,8 +197,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n>  \tstruct strvec submodule_gitfiles_to_free = STRVEC_INIT;\n>  \tconst char **submodule_gitfiles;\n>  \tchar *dst_w_slash = NULL;\n> -\tconst char **src_dir = NULL;\n> -\tint src_dir_nr = 0, src_dir_alloc = 0;\n> +\tstruct strvec src_dir = STRVEC_INIT;\n>  \tenum update_mode *modes, dst_mode = 0;\n>  \tstruct stat st, dest_st;\n>  \tstruct string_list src_for_dst = STRING_LIST_INIT_DUP;\n> @@ -344,8 +343,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n>  \t\t\t/* last - first >= 1 */\n>  \t\t\tmodes[i] |= WORKING_DIRECTORY;\n>  \n> -\t\t\tALLOC_GROW(src_dir, src_dir_nr + 1, src_dir_alloc);\n> -\t\t\tsrc_dir[src_dir_nr++] = src;\n> +\t\t\tstrvec_push(&src_dir, src);\n>  \n>  \t\t\tn = argc + last - first;\n>  \t\t\tREALLOC_ARRAY(modes, n);\n> @@ -559,7 +557,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n>  \t\t}\n>  \t}\n>  \n> -\tremove_empty_src_dirs(src_dir, src_dir_nr);\n> +\tremove_empty_src_dirs(src_dir.v, src_dir.nr);\n>  \n>  \tif (dirty_paths.nr)\n>  \t\tadvise_on_moving_dirty_path(&dirty_paths);\n> @@ -574,7 +572,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n>  \tret = 0;\n>  \n>  out:\n> -\tfree(src_dir);\n> +\tstrvec_clear(&src_dir);\n>  \tfree(dst_w_slash);\n>  \tstring_list_clear(&src_for_dst, 0);\n>  \tstring_list_clear(&dirty_paths, 0);\n"},{"id":"495984","messageId":"20240531111205.GA3608259@coredump.intra.peff.net","threadId":"61535","inReplyTo":"xmqqo78nqpl2.fsf@gitster.g","subject":"Re: [PATCH 5/5] mv: replace src_dir with a strvec","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-05-31T11:12:05Z","receivedAt":"2024-05-31T11:12:14Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Thu, May 30, 2024 at 08:36:25AM -0700, Junio C Hamano wrote:\n\n> Hmph, the rationale given by 9fcd9e4e (builtin/mv duplicate string\n> list memory, 2024-05-27) essentially is \"the number of elements are\n> the same as the number of command line parameters\", but I do not\n> think that is quite correct.\n> \n> When you do \"mv srcA srcB ... dst\", you'd inspect the command line\n> arguments from left to right, notice that srcA is a directory, find\n> the cache entries for paths that are inside srcA, append the paths\n> in that directory to source[] and destination[] array, and extend\n> argc.  \"for (i = 0; i < argc; i++)\" loop that appends one element to\n> src_for_dst per iteration ends up running the number of paths being\n> moved, which can be order of magnitude more than the command line\n> parameters.\n> \n> Of course, if we needed to make copies for correctness reasons (or\n> to clarify memory ownership semantics), that alone may be a good\n> justification and we do not need an excuse \"it's just a handful of\n> elements anyway\" to begin with.\n> \n> Anyway, that is about somebody else's patch, not this one ;-).\n\nHeh, good digging. I actually wondered if I was making the same mistake\nwhile writing mine, but double-checked that src_dir is not expanded in\nthat way. But I didn't think to check Patrick's original. ;)\n\nIMHO it is probably still OK. We are bounded by the number of entries in\nthe index (and we already use proportional memory for other parts of the\noperation).\n\n-Peff\n"},{"id":"496011","messageId":"xmqqttiehvxe.fsf@gitster.g","threadId":"61535","inReplyTo":"20240531111205.GA3608259@coredump.intra.peff.net","subject":"Re: [PATCH 5/5] mv: replace src_dir with a strvec","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-31T14:56:29Z","receivedAt":"2024-05-31T14:56:34Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n>> Anyway, that is about somebody else's patch, not this one ;-).\n>\n> Heh, good digging. I actually wondered if I was making the same mistake\n> while writing mine, but double-checked that src_dir is not expanded in\n> that way. But I didn't think to check Patrick's original. ;)\n>\n> IMHO it is probably still OK. We are bounded by the number of entries in\n> the index (and we already use proportional memory for other parts of the\n> operation).\n\nYes, I agree it is OK.  We have two arrays whose strings have to be\nallocated, source[] and destination[], and used to have another\nwhose elements borrows from one of these, but with the change\ninstead of borrowing we now duplicate, but as long as the other\nbehefit outweigh the cost of additional memory usage, it is an\nacceptable trade-off.\n\nThanks.\n"}]}