{"thread":{"id":"64475","subject":"[PATCH] osxkeychain: avoid incorrectly skipping store operation","startedAt":"2025-11-13T15:26:42Z","lastAt":"2025-11-18T09:57:16Z","messageCount":6,"participants":["Koji Nakamaru via GitGitGadget","Junio C Hamano","Koji Nakamaru","Jeff King"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"530648","messageId":"pull.1999.git.1763047599254.gitgitgadget@gmail.com","threadId":"64475","inReplyTo":null,"subject":"[PATCH] osxkeychain: avoid incorrectly skipping store operation","fromName":"Koji Nakamaru via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-11-13T15:26:39Z","receivedAt":"2025-11-13T15:26:42Z","isPatch":true,"sender":{"key":"koji.nakamaru@gree.net","avatar":"https://avatars.githubusercontent.com/u/2645978?v=4"},"body":"From: Koji Nakamaru <koji.nakamaru@gree.net>\n\ngit-credential-osxkeychain skips storing a credential if its \"get\"\naction sets \"state[]=osxkeychain:seen=1\". This behavior was introduced\nin e1ab45b2 (osxkeychain: state to skip unnecessary store operations,\n2024-05-15), which appeared in v2.46.\n\nHowever, this state[] persists even if a credential returned by\n\"git-credential-osxkeychain get\" is invalid and a subsequent helper's\n\"get\" operation returns a valid credential. Another subsequent helper\n(such as [1]) may expect git-credential-osxkeychain to store the valid\ncredential, but the \"store\" operation is incorrectly skipped because it\nonly checks \"state[]=osxkeychain:seen=1\".\n\nTo solve this issue, \"state[]=osxkeychain:seen\" needs to contain enough\ninformation to identify whether the current \"store\" input matches the\noutput from the previous \"get\" operation (and not a credential from\nanother helper).\n\nSet \"state[]=osxkeychain:seen\" to a value encoding the credential output\nby \"get\", and compare it with a value encoding the credential input by\n\"store\".\n\n[1]: https://github.com/hickford/git-credential-oauth\n\nReported-by: Petter Sælen <petter@saelen.eu>\nHelped-by: Junio C Hamano <gitster@pobox.com>\nSigned-off-by: Koji Nakamaru <koji.nakamaru@gree.net>\n---\n    osxkeychain: avoid incorrectly skipping store operation\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-1999%2FKojiNakamaru%2Ffix%2Fosxkeychain-state-seen-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1999/KojiNakamaru/fix/osxkeychain-state-seen-v1\nPull-Request: https://github.com/gitgitgadget/git/pull/1999\n\n .../osxkeychain/git-credential-osxkeychain.c  | 158 +++++++++++++++++-\n 1 file changed, 151 insertions(+), 7 deletions(-)\n\ndiff --git a/contrib/credential/osxkeychain/git-credential-osxkeychain.c b/contrib/credential/osxkeychain/git-credential-osxkeychain.c\nindex 611c9798b3..c973e844a5 100644\n--- a/contrib/credential/osxkeychain/git-credential-osxkeychain.c\n+++ b/contrib/credential/osxkeychain/git-credential-osxkeychain.c\n@@ -12,7 +12,7 @@ static CFStringRef username;\n static CFDataRef password;\n static CFDataRef password_expiry_utc;\n static CFDataRef oauth_refresh_token;\n-static int state_seen;\n+static char *state_seen;\n \n static void clear_credential(void)\n {\n@@ -61,6 +61,12 @@ static void die(const char *err, ...)\n \texit(1);\n }\n \n+/*\n+ * NOTE: We could use functions in strbuf.h and/or wrapper.h, but those\n+ * introduce significant dependencies. Therefore, we define simplified\n+ * versions here to keep this code self-contained.\n+ */\n+\n static void *xmalloc(size_t len)\n {\n \tvoid *ret = malloc(len);\n@@ -69,6 +75,30 @@ static void *xmalloc(size_t len)\n \treturn ret;\n }\n \n+static void *xcalloc(size_t count, size_t size)\n+{\n+\tvoid *ret = calloc(count, size);\n+\tif (!ret)\n+\t\tdie(\"Out of memory\");\n+\treturn ret;\n+}\n+\n+static void *xrealloc(void *ptr, size_t size)\n+{\n+\tvoid *ret = realloc(ptr, size);\n+\tif (!ret)\n+\t\tdie(\"Out of memory\");\n+\treturn ret;\n+}\n+\n+static char *xstrdup(const char *str)\n+{\n+\tchar *ret = strdup(str);\n+\tif (!ret)\n+\t\tdie(\"Out of memory\");\n+\treturn ret;\n+}\n+\n static CFDictionaryRef create_dictionary(CFAllocatorRef allocator, ...)\n {\n \tva_list args;\n@@ -112,6 +142,98 @@ static void write_item(const char *what, const char *buf, size_t len)\n \tputchar('\\n');\n }\n \n+struct sb {\n+\tchar *buf;\n+\tint size;\n+};\n+\n+static void sb_init(struct sb *sb)\n+{\n+\tsb->size = 1024;\n+\tsb->buf = xcalloc(sb->size, 1);\n+}\n+\n+static void sb_release(struct sb *sb)\n+{\n+\tif (sb->buf) {\n+\t\tfree(sb->buf);\n+\t\tsb->buf = NULL;\n+\t\tsb->size = 0;\n+\t}\n+}\n+\n+static void sb_add(struct sb *sb, const char *s, int n)\n+{\n+\tint len = strlen(sb->buf);\n+\tint size = sb->size;\n+\tif (size < len + n + 1) {\n+\t\tsb->size = len + n + 1;\n+\t\tsb->buf = xrealloc(sb->buf, sb->size);\n+\t}\n+\tstrncat(sb->buf, s, n);\n+\tsb->buf[len + n] = '\\0';\n+}\n+\n+static void write_item_sb(struct sb *sb, const char *what, const char *buf, int n)\n+{\n+\tchar s[32];\n+\n+\tsprintf(s, \"__%s=\", what);\n+\tsb_add(sb, s, strlen(s));\n+\tsb_add(sb, buf, n);\n+}\n+\n+static void write_item_sb_cfstring(struct sb *sb, const char *what, CFStringRef ref)\n+{\n+\tchar *buf;\n+\tint len;\n+\n+\tif (!ref)\n+\t\treturn;\n+\tlen = CFStringGetMaximumSizeForEncoding(CFStringGetLength(ref), ENCODING) + 1;\n+\tbuf = xmalloc(len);\n+\tif (CFStringGetCString(ref, buf, len, ENCODING))\n+\t\twrite_item_sb(sb, what, buf, strlen(buf));\n+\tfree(buf);\n+}\n+\n+static void write_item_sb_cfnumber(struct sb *sb, const char *what, CFNumberRef ref)\n+{\n+\tshort n;\n+\tchar buf[32];\n+\n+\tif (!ref)\n+\t\treturn;\n+\tif (!CFNumberGetValue(ref, kCFNumberShortType, &n))\n+\t\treturn;\n+\tsprintf(buf, \"%d\", n);\n+\twrite_item_sb(sb, what, buf, strlen(buf));\n+}\n+\n+static void write_item_sb_cfdata(struct sb *sb, const char *what, CFDataRef ref)\n+{\n+\tchar *buf;\n+\tint len;\n+\n+\tif (!ref)\n+\t\treturn;\n+\tbuf = (char *)CFDataGetBytePtr(ref);\n+\tif (!buf || strlen(buf) == 0)\n+\t\treturn;\n+\tlen = CFDataGetLength(ref);\n+\twrite_item_sb(sb, what, buf, len);\n+}\n+\n+static void encode_state_seen(struct sb *sb)\n+{\n+\tsb_add(sb, \"osxkeychain:seen=\", strlen(\"osxkeychain:seen=\"));\n+\twrite_item_sb_cfstring(sb, \"host\", host);\n+\twrite_item_sb_cfnumber(sb, \"port\", port);\n+\twrite_item_sb_cfstring(sb, \"path\", path);\n+\twrite_item_sb_cfstring(sb, \"username\", username);\n+\twrite_item_sb_cfdata(sb, \"password\", password);\n+}\n+\n static void find_username_in_item(CFDictionaryRef item)\n {\n \tCFStringRef account_ref;\n@@ -124,6 +246,7 @@ static void find_username_in_item(CFDictionaryRef item)\n \t\twrite_item(\"username\", \"\", 0);\n \t\treturn;\n \t}\n+\tusername = CFStringCreateCopy(kCFAllocatorDefault, account_ref);\n \n \tusername_buf = (char *)CFStringGetCStringPtr(account_ref, ENCODING);\n \tif (username_buf)\n@@ -163,6 +286,7 @@ static OSStatus find_internet_password(void)\n \t}\n \n \tdata = CFDictionaryGetValue(item, kSecValueData);\n+\tpassword = CFDataCreateCopy(kCFAllocatorDefault, data);\n \n \twrite_item(\"password\",\n \t\t   (const char *)CFDataGetBytePtr(data),\n@@ -173,7 +297,14 @@ static OSStatus find_internet_password(void)\n \tCFRelease(item);\n \n \twrite_item(\"capability[]\", \"state\", strlen(\"state\"));\n-\twrite_item(\"state[]\", \"osxkeychain:seen=1\", strlen(\"osxkeychain:seen=1\"));\n+\t{\n+\t\tstruct sb sb;\n+\n+\t\tsb_init(&sb);\n+\t\tencode_state_seen(&sb);\n+\t\twrite_item(\"state[]\", sb.buf, strlen(sb.buf));\n+\t\tsb_release(&sb);\n+\t}\n \n out:\n \tCFRelease(attrs);\n@@ -288,13 +419,22 @@ static OSStatus add_internet_password(void)\n \tCFDictionaryRef attrs;\n \tOSStatus result;\n \n-\tif (state_seen)\n-\t\treturn errSecSuccess;\n-\n \t/* Only store complete credentials */\n \tif (!protocol || !host || !username || !password)\n \t\treturn -1;\n \n+\tif (state_seen) {\n+\t\tstruct sb sb;\n+\n+\t\tsb_init(&sb);\n+\t\tencode_state_seen(&sb);\n+\t\tif (!strcmp(state_seen, sb.buf)) {\n+\t\t\tsb_release(&sb);\n+\t\t\treturn errSecSuccess;\n+\t\t}\n+\t\tsb_release(&sb);\n+\t}\n+\n \tdata = CFDataCreateMutableCopy(kCFAllocatorDefault, 0, password);\n \tif (password_expiry_utc) {\n \t\tCFDataAppendBytes(data,\n@@ -403,8 +543,9 @@ static void read_credential(void)\n \t\t\t\t\t\t\t   (UInt8 *)v,\n \t\t\t\t\t\t\t   strlen(v));\n \t\telse if (!strcmp(buf, \"state[]\")) {\n-\t\t\tif (!strcmp(v, \"osxkeychain:seen=1\"))\n-\t\t\t\tstate_seen = 1;\n+\t\t\tint len = strlen(\"osxkeychain:seen=\");\n+\t\t\tif (!strncmp(v, \"osxkeychain:seen=\", len))\n+\t\t\t\tstate_seen = xstrdup(v);\n \t\t}\n \t\t/*\n \t\t * Ignore other lines; we don't know what they mean, but\n@@ -443,5 +584,8 @@ int main(int argc, const char **argv)\n \n \tclear_credential();\n \n+\tif (state_seen)\n+\t\tfree(state_seen);\n+\n \treturn 0;\n }\n\nbase-commit: 4badef0c3503dc29059d678abba7fac0f042bc84\n-- \ngitgitgadget\n"},{"id":"530663","messageId":"xmqqo6p5llsw.fsf@gitster.g","threadId":"64475","inReplyTo":"pull.1999.git.1763047599254.gitgitgadget@gmail.com","subject":"Re: [PATCH] osxkeychain: avoid incorrectly skipping store operation","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-13T20:28:15Z","receivedAt":"2025-11-13T20:28:18Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Koji Nakamaru via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> +/*\n> + * NOTE: We could use functions in strbuf.h and/or wrapper.h, but those\n> + * introduce significant dependencies. Therefore, we define simplified\n> + * versions here to keep this code self-contained.\n> + */\n\nSorry, but I do not quite understand this comment.  The program is\nshipped as a part of Git, and using these functions and linking with\nlibgit.a may pull strbuf.o and some other *.o files out of libgit.a\nto link with git-credential-osxkeychain.o to produce the executable,\nbut how can that be \"significant dependencies\"?  For anybody who is\nbuilding git-credential-osxkeychain, the necessary sources come for\nfree.\n\nIt is not like we are forcing git-credential-osxkeychain to link\nwith a shared object libgit.so and making git-credential-osxkeychain\ndepend on it, or anything like that, which may require consumers of\nbinary distribution of git-credential-osxkeychain to also install\nanother package that has libgit.so in it (which is likely to be the\n\"git\" package).  Even if it were the case (which is not), what good\nwould it be to have git-credential-osxkeychain on your system\nwithout having git on the same system?\n\n"},{"id":"530665","messageId":"xmqqecq1llgj.fsf@gitster.g","threadId":"64475","inReplyTo":"xmqqo6p5llsw.fsf@gitster.g","subject":"Re: [PATCH] osxkeychain: avoid incorrectly skipping store operation","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-13T20:35:40Z","receivedAt":"2025-11-13T20:35:42Z","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> \"Koji Nakamaru via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n>> +/*\n>> + * NOTE: We could use functions in strbuf.h and/or wrapper.h, but those\n>> + * introduce significant dependencies. Therefore, we define simplified\n>> + * versions here to keep this code self-contained.\n>> + */\n>\n> Sorry, but I do not quite understand this comment.  The program is\n> shipped as a part of Git, and using these functions and linking with\n> libgit.a may pull strbuf.o and some other *.o files out of libgit.a\n> to link with git-credential-osxkeychain.o to produce the executable,\n> but how can that be \"significant dependencies\"?  For anybody who is\n> building git-credential-osxkeychain, the necessary sources come for\n> free.\n>\n> It is not like we are forcing git-credential-osxkeychain to link\n> with a shared object libgit.so and making git-credential-osxkeychain\n> depend on it, or anything like that, which may require consumers of\n> binary distribution of git-credential-osxkeychain to also install\n> another package that has libgit.so in it (which is likely to be the\n> \"git\" package).  Even if it were the case (which is not), what good\n> would it be to have git-credential-osxkeychain on your system\n> without having git on the same system?\n\nThe rest of the patch, excluding the poor-man's reimplementation of\nhelper functions, looked like they match what the proposed log\nmessage described.\n\nIt seems that credential material like username and password are\nincluded in plaintext as part of the state[], but is this a safe\nthing to do?  The keychain will give out the credential material in\na way the requestor with sufficient priviledges can read, and this\nstate[] is stored in the same place, so I am guessing that this is\nnot adding any extra security concerns, but I just wanted to make\nsure you've considered any security implications.\n\nThanks.\n"},{"id":"530674","messageId":"CAOTNsDwSUCqrGW1A4LvGYCseFZ6=XZ16C0OS85s27SyzrrfYPw@mail.gmail.com","threadId":"64475","inReplyTo":"xmqqecq1llgj.fsf@gitster.g","subject":"Re: [PATCH] osxkeychain: avoid incorrectly skipping store operation","fromName":"Koji Nakamaru","fromEmail":"koji.nakamaru@gree.net","sentAt":"2025-11-14T03:23:12Z","receivedAt":"2025-11-14T03:23:24Z","isPatch":true,"sender":{"key":"koji.nakamaru@gree.net","avatar":"https://avatars.githubusercontent.com/u/2645978?v=4"},"body":"On Fri, Nov 14, 2025 at 5:35 AM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Junio C Hamano <gitster@pobox.com> writes:\n>\n> > \"Koji Nakamaru via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n> >\n> >> +/*\n> >> + * NOTE: We could use functions in strbuf.h and/or wrapper.h, but those\n> >> + * introduce significant dependencies. Therefore, we define simplified\n> >> + * versions here to keep this code self-contained.\n> >> + */\n> >\n> > Sorry, but I do not quite understand this comment.  The program is\n> > shipped as a part of Git, and using these functions and linking with\n> > libgit.a may pull strbuf.o and some other *.o files out of libgit.a\n> > to link with git-credential-osxkeychain.o to produce the executable,\n> > but how can that be \"significant dependencies\"?  For anybody who is\n> > building git-credential-osxkeychain, the necessary sources come for\n> > free.\n> >\n> > It is not like we are forcing git-credential-osxkeychain to link\n> > with a shared object libgit.so and making git-credential-osxkeychain\n> > depend on it, or anything like that, which may require consumers of\n> > binary distribution of git-credential-osxkeychain to also install\n> > another package that has libgit.so in it (which is likely to be the\n> > \"git\" package).  Even if it were the case (which is not), what good\n> > would it be to have git-credential-osxkeychain on your system\n> > without having git on the same system?\n\nI see your point. I was following the current implementation's approach\n(it has its own xmalloc() and die()) and thought the comment would be\nappropriate if we continued that approach. I will refactor the code to\nuse libgit instead.\n\n> The rest of the patch, excluding the poor-man's reimplementation of\n> helper functions, looked like they match what the proposed log\n> message described.\n>\n> It seems that credential material like username and password are\n> included in plaintext as part of the state[], but is this a safe\n> thing to do?  The keychain will give out the credential material in\n> a way the requestor with sufficient priviledges can read, and this\n> state[] is stored in the same place, so I am guessing that this is\n> not adding any extra security concerns, but I just wanted to make\n> sure you've considered any security implications.\n\nYes, that was considered. The credential helper protocol already\npasses credentials in plaintext between helpers via the \"store\"\noperation. Since the data in state[] is handled in the same\nmanner, it doesn't introduce an additional security risk beyond\nwhat the existing protocol already entails.\n\n--\nKoji Nakamaru\n"},{"id":"530682","messageId":"pull.1999.v2.git.1763100270949.gitgitgadget@gmail.com","threadId":"64475","inReplyTo":"pull.1999.git.1763047599254.gitgitgadget@gmail.com","subject":"[PATCH v2] osxkeychain: avoid incorrectly skipping store operation","fromName":"Koji Nakamaru via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-11-14T06:04:30Z","receivedAt":"2025-11-14T06:04:34Z","isPatch":true,"sender":{"key":"koji.nakamaru@gree.net","avatar":"https://avatars.githubusercontent.com/u/2645978?v=4"},"body":"From: Koji Nakamaru <koji.nakamaru@gree.net>\n\ngit-credential-osxkeychain skips storing a credential if its \"get\"\naction sets \"state[]=osxkeychain:seen=1\". This behavior was introduced\nin e1ab45b2 (osxkeychain: state to skip unnecessary store operations,\n2024-05-15), which appeared in v2.46.\n\nHowever, this state[] persists even if a credential returned by\n\"git-credential-osxkeychain get\" is invalid and a subsequent helper's\n\"get\" operation returns a valid credential. Another subsequent helper\n(such as [1]) may expect git-credential-osxkeychain to store the valid\ncredential, but the \"store\" operation is incorrectly skipped because it\nonly checks \"state[]=osxkeychain:seen=1\".\n\nTo solve this issue, \"state[]=osxkeychain:seen\" needs to contain enough\ninformation to identify whether the current \"store\" input matches the\noutput from the previous \"get\" operation (and not a credential from\nanother helper).\n\nSet \"state[]=osxkeychain:seen\" to a value encoding the credential output\nby \"get\", and compare it with a value encoding the credential input by\n\"store\".\n\n[1]: https://github.com/hickford/git-credential-oauth\n\nReported-by: Petter Sælen <petter@saelen.eu>\nHelped-by: Junio C Hamano <gitster@pobox.com>\nHelped-by: brian m. carlson <sandals@crustytoothpaste.net>\nSigned-off-by: Koji Nakamaru <koji.nakamaru@gree.net>\n---\n    osxkeychain: avoid incorrectly skipping store operation\n    \n    Changes since v1:\n    \n     * Use functions provided by wrapper.h and strbuf.h instead of\n       self-defined ones.\n     * Adjust Makefile and meson.build to use these functions.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-1999%2FKojiNakamaru%2Ffix%2Fosxkeychain-state-seen-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1999/KojiNakamaru/fix/osxkeychain-state-seen-v2\nPull-Request: https://github.com/gitgitgadget/git/pull/1999\n\nRange-diff vs v1:\n\n 1:  b14045b4de ! 1:  dd36d290ff osxkeychain: avoid incorrectly skipping store operation\n     @@ Commit message\n      \n          Reported-by: Petter Sælen <petter@saelen.eu>\n          Helped-by: Junio C Hamano <gitster@pobox.com>\n     +    Helped-by: brian m. carlson <sandals@crustytoothpaste.net>\n          Signed-off-by: Koji Nakamaru <koji.nakamaru@gree.net>\n      \n     + ## contrib/credential/osxkeychain/Makefile ##\n     +@@\n     + # The default target of this Makefile is...\n     + all:: git-credential-osxkeychain\n     + \n     ++include ../../../config.mak.uname\n     + -include ../../../config.mak.autogen\n     + -include ../../../config.mak\n     + \n     ++ifdef ZLIB_NG\n     ++\tBASIC_CFLAGS += -DHAVE_ZLIB_NG\n     ++        ifdef ZLIB_NG_PATH\n     ++\t\tBASIC_CFLAGS += -I$(ZLIB_NG_PATH)/include\n     ++\t\tEXTLIBS += $(call libpath_template,$(ZLIB_NG_PATH)/$(lib))\n     ++        endif\n     ++\tEXTLIBS += -lz-ng\n     ++else\n     ++        ifdef ZLIB_PATH\n     ++\t\tBASIC_CFLAGS += -I$(ZLIB_PATH)/include\n     ++\t\tEXTLIBS += $(call libpath_template,$(ZLIB_PATH)/$(lib))\n     ++        endif\n     ++\tEXTLIBS += -lz\n     ++endif\n     ++ifndef NO_ICONV\n     ++        ifdef NEEDS_LIBICONV\n     ++                ifdef ICONVDIR\n     ++\t\t\tBASIC_CFLAGS += -I$(ICONVDIR)/include\n     ++\t\t\tICONV_LINK = $(call libpath_template,$(ICONVDIR)/$(lib))\n     ++                else\n     ++\t\t\tICONV_LINK =\n     ++                endif\n     ++                ifdef NEEDS_LIBINTL_BEFORE_LIBICONV\n     ++\t\t\tICONV_LINK += -lintl\n     ++                endif\n     ++\t\tEXTLIBS += $(ICONV_LINK) -liconv\n     ++        endif\n     ++endif\n     ++ifndef LIBC_CONTAINS_LIBINTL\n     ++\tEXTLIBS += -lintl\n     ++endif\n     ++\n     + prefix ?= /usr/local\n     + gitexecdir ?= $(prefix)/libexec/git-core\n     + \n     + CC ?= gcc\n     +-CFLAGS ?= -g -O2 -Wall\n     ++CFLAGS ?= -g -O2 -Wall -I../../.. $(BASIC_CFLAGS)\n     ++LDFLAGS ?= $(BASIC_LDFLAGS) $(EXTLIBS)\n     + INSTALL ?= install\n     + RM ?= rm -f\n     + \n     + %.o: %.c\n     + \t$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ -c $<\n     + \n     +-git-credential-osxkeychain: git-credential-osxkeychain.o\n     ++git-credential-osxkeychain: git-credential-osxkeychain.o ../../../libgit.a\n     + \t$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) \\\n     + \t\t-framework Security -framework CoreFoundation\n     + \n     +@@ contrib/credential/osxkeychain/Makefile: install: git-credential-osxkeychain\n     + \t$(INSTALL) -d -m 755 $(DESTDIR)$(gitexecdir)\n     + \t$(INSTALL) -m 755 $< $(DESTDIR)$(gitexecdir)\n     + \n     ++../../../libgit.a:\n     ++\tcd ../../..; make libgit.a\n     ++\n     + clean:\n     + \t$(RM) git-credential-osxkeychain git-credential-osxkeychain.o\n     + \n     +\n       ## contrib/credential/osxkeychain/git-credential-osxkeychain.c ##\n     +@@\n     + #include <string.h>\n     + #include <stdlib.h>\n     + #include <Security/Security.h>\n     ++#include \"git-compat-util.h\"\n     ++#include \"strbuf.h\"\n     ++#include \"wrapper.h\"\n     + \n     + #define ENCODING kCFStringEncodingUTF8\n     + static CFStringRef protocol; /* Stores constant strings - not memory managed */\n      @@ contrib/credential/osxkeychain/git-credential-osxkeychain.c: static CFStringRef username;\n       static CFDataRef password;\n       static CFDataRef password_expiry_utc;\n     @@ contrib/credential/osxkeychain/git-credential-osxkeychain.c: static CFStringRef\n       \n       static void clear_credential(void)\n       {\n     -@@ contrib/credential/osxkeychain/git-credential-osxkeychain.c: static void die(const char *err, ...)\n     - \texit(1);\n     - }\n     +@@ contrib/credential/osxkeychain/git-credential-osxkeychain.c: static void clear_credential(void)\n       \n     -+/*\n     -+ * NOTE: We could use functions in strbuf.h and/or wrapper.h, but those\n     -+ * introduce significant dependencies. Therefore, we define simplified\n     -+ * versions here to keep this code self-contained.\n     -+ */\n     -+\n     - static void *xmalloc(size_t len)\n     - {\n     - \tvoid *ret = malloc(len);\n     -@@ contrib/credential/osxkeychain/git-credential-osxkeychain.c: static void *xmalloc(size_t len)\n     - \treturn ret;\n     - }\n     + #define STRING_WITH_LENGTH(s) s, sizeof(s) - 1\n       \n     -+static void *xcalloc(size_t count, size_t size)\n     -+{\n     -+\tvoid *ret = calloc(count, size);\n     -+\tif (!ret)\n     -+\t\tdie(\"Out of memory\");\n     -+\treturn ret;\n     -+}\n     -+\n     -+static void *xrealloc(void *ptr, size_t size)\n     -+{\n     -+\tvoid *ret = realloc(ptr, size);\n     -+\tif (!ret)\n     -+\t\tdie(\"Out of memory\");\n     -+\treturn ret;\n     -+}\n     -+\n     -+static char *xstrdup(const char *str)\n     -+{\n     -+\tchar *ret = strdup(str);\n     -+\tif (!ret)\n     -+\t\tdie(\"Out of memory\");\n     -+\treturn ret;\n     -+}\n     -+\n     +-__attribute__((format (printf, 1, 2), __noreturn__))\n     +-static void die(const char *err, ...)\n     +-{\n     +-\tchar msg[4096];\n     +-\tva_list params;\n     +-\tva_start(params, err);\n     +-\tvsnprintf(msg, sizeof(msg), err, params);\n     +-\tfprintf(stderr, \"%s\\n\", msg);\n     +-\tva_end(params);\n     +-\tclear_credential();\n     +-\texit(1);\n     +-}\n     +-\n     +-static void *xmalloc(size_t len)\n     +-{\n     +-\tvoid *ret = malloc(len);\n     +-\tif (!ret)\n     +-\t\tdie(\"Out of memory\");\n     +-\treturn ret;\n     +-}\n     +-\n       static CFDictionaryRef create_dictionary(CFAllocatorRef allocator, ...)\n       {\n       \tva_list args;\n     @@ contrib/credential/osxkeychain/git-credential-osxkeychain.c: static void write_i\n       \tputchar('\\n');\n       }\n       \n     -+struct sb {\n     -+\tchar *buf;\n     -+\tint size;\n     -+};\n     -+\n     -+static void sb_init(struct sb *sb)\n     -+{\n     -+\tsb->size = 1024;\n     -+\tsb->buf = xcalloc(sb->size, 1);\n     -+}\n     -+\n     -+static void sb_release(struct sb *sb)\n     -+{\n     -+\tif (sb->buf) {\n     -+\t\tfree(sb->buf);\n     -+\t\tsb->buf = NULL;\n     -+\t\tsb->size = 0;\n     -+\t}\n     -+}\n     -+\n     -+static void sb_add(struct sb *sb, const char *s, int n)\n     -+{\n     -+\tint len = strlen(sb->buf);\n     -+\tint size = sb->size;\n     -+\tif (size < len + n + 1) {\n     -+\t\tsb->size = len + n + 1;\n     -+\t\tsb->buf = xrealloc(sb->buf, sb->size);\n     -+\t}\n     -+\tstrncat(sb->buf, s, n);\n     -+\tsb->buf[len + n] = '\\0';\n     -+}\n     -+\n     -+static void write_item_sb(struct sb *sb, const char *what, const char *buf, int n)\n     ++static void write_item_strbuf(struct strbuf *sb, const char *what, const char *buf, int n)\n      +{\n      +\tchar s[32];\n      +\n     -+\tsprintf(s, \"__%s=\", what);\n     -+\tsb_add(sb, s, strlen(s));\n     -+\tsb_add(sb, buf, n);\n     ++\txsnprintf(s, sizeof(s), \"__%s=\", what);\n     ++\tstrbuf_add(sb, s, strlen(s));\n     ++\tstrbuf_add(sb, buf, n);\n      +}\n      +\n     -+static void write_item_sb_cfstring(struct sb *sb, const char *what, CFStringRef ref)\n     ++static void write_item_strbuf_cfstring(struct strbuf *sb, const char *what, CFStringRef ref)\n      +{\n      +\tchar *buf;\n      +\tint len;\n     @@ contrib/credential/osxkeychain/git-credential-osxkeychain.c: static void write_i\n      +\tlen = CFStringGetMaximumSizeForEncoding(CFStringGetLength(ref), ENCODING) + 1;\n      +\tbuf = xmalloc(len);\n      +\tif (CFStringGetCString(ref, buf, len, ENCODING))\n     -+\t\twrite_item_sb(sb, what, buf, strlen(buf));\n     ++\t\twrite_item_strbuf(sb, what, buf, strlen(buf));\n      +\tfree(buf);\n      +}\n      +\n     -+static void write_item_sb_cfnumber(struct sb *sb, const char *what, CFNumberRef ref)\n     ++static void write_item_strbuf_cfnumber(struct strbuf *sb, const char *what, CFNumberRef ref)\n      +{\n      +\tshort n;\n      +\tchar buf[32];\n     @@ contrib/credential/osxkeychain/git-credential-osxkeychain.c: static void write_i\n      +\t\treturn;\n      +\tif (!CFNumberGetValue(ref, kCFNumberShortType, &n))\n      +\t\treturn;\n     -+\tsprintf(buf, \"%d\", n);\n     -+\twrite_item_sb(sb, what, buf, strlen(buf));\n     ++\txsnprintf(buf, sizeof(buf), \"%d\", n);\n     ++\twrite_item_strbuf(sb, what, buf, strlen(buf));\n      +}\n      +\n     -+static void write_item_sb_cfdata(struct sb *sb, const char *what, CFDataRef ref)\n     ++static void write_item_strbuf_cfdata(struct strbuf *sb, const char *what, CFDataRef ref)\n      +{\n      +\tchar *buf;\n      +\tint len;\n     @@ contrib/credential/osxkeychain/git-credential-osxkeychain.c: static void write_i\n      +\tif (!buf || strlen(buf) == 0)\n      +\t\treturn;\n      +\tlen = CFDataGetLength(ref);\n     -+\twrite_item_sb(sb, what, buf, len);\n     ++\twrite_item_strbuf(sb, what, buf, len);\n      +}\n      +\n     -+static void encode_state_seen(struct sb *sb)\n     ++static void encode_state_seen(struct strbuf *sb)\n      +{\n     -+\tsb_add(sb, \"osxkeychain:seen=\", strlen(\"osxkeychain:seen=\"));\n     -+\twrite_item_sb_cfstring(sb, \"host\", host);\n     -+\twrite_item_sb_cfnumber(sb, \"port\", port);\n     -+\twrite_item_sb_cfstring(sb, \"path\", path);\n     -+\twrite_item_sb_cfstring(sb, \"username\", username);\n     -+\twrite_item_sb_cfdata(sb, \"password\", password);\n     ++\tstrbuf_add(sb, \"osxkeychain:seen=\", strlen(\"osxkeychain:seen=\"));\n     ++\twrite_item_strbuf_cfstring(sb, \"host\", host);\n     ++\twrite_item_strbuf_cfnumber(sb, \"port\", port);\n     ++\twrite_item_strbuf_cfstring(sb, \"path\", path);\n     ++\twrite_item_strbuf_cfstring(sb, \"username\", username);\n     ++\twrite_item_strbuf_cfdata(sb, \"password\", password);\n      +}\n      +\n       static void find_username_in_item(CFDictionaryRef item)\n     @@ contrib/credential/osxkeychain/git-credential-osxkeychain.c: static OSStatus fin\n       \twrite_item(\"capability[]\", \"state\", strlen(\"state\"));\n      -\twrite_item(\"state[]\", \"osxkeychain:seen=1\", strlen(\"osxkeychain:seen=1\"));\n      +\t{\n     -+\t\tstruct sb sb;\n     ++\t\tstruct strbuf sb;\n      +\n     -+\t\tsb_init(&sb);\n     ++\t\tstrbuf_init(&sb, 1024);\n      +\t\tencode_state_seen(&sb);\n      +\t\twrite_item(\"state[]\", sb.buf, strlen(sb.buf));\n     -+\t\tsb_release(&sb);\n     ++\t\tstrbuf_release(&sb);\n      +\t}\n       \n       out:\n     @@ contrib/credential/osxkeychain/git-credential-osxkeychain.c: static OSStatus add\n       \t\treturn -1;\n       \n      +\tif (state_seen) {\n     -+\t\tstruct sb sb;\n     ++\t\tstruct strbuf sb;\n      +\n     -+\t\tsb_init(&sb);\n     ++\t\tstrbuf_init(&sb, 1024);\n      +\t\tencode_state_seen(&sb);\n      +\t\tif (!strcmp(state_seen, sb.buf)) {\n     -+\t\t\tsb_release(&sb);\n     ++\t\t\tstrbuf_release(&sb);\n      +\t\t\treturn errSecSuccess;\n      +\t\t}\n     -+\t\tsb_release(&sb);\n     ++\t\tstrbuf_release(&sb);\n      +\t}\n      +\n       \tdata = CFDataCreateMutableCopy(kCFAllocatorDefault, 0, password);\n     @@ contrib/credential/osxkeychain/git-credential-osxkeychain.c: int main(int argc,\n      +\n       \treturn 0;\n       }\n     +\n     + ## contrib/credential/osxkeychain/meson.build ##\n     +@@\n     + executable('git-credential-osxkeychain',\n     +   sources: 'git-credential-osxkeychain.c',\n     +   dependencies: [\n     ++    libgit,\n     +     dependency('CoreFoundation'),\n     +     dependency('Security'),\n     +   ],\n\n\n contrib/credential/osxkeychain/Makefile       |  41 +++++-\n .../osxkeychain/git-credential-osxkeychain.c  | 120 ++++++++++++++----\n contrib/credential/osxkeychain/meson.build    |   1 +\n 3 files changed, 132 insertions(+), 30 deletions(-)\n\ndiff --git a/contrib/credential/osxkeychain/Makefile b/contrib/credential/osxkeychain/Makefile\nindex 9680717abe..c68445b82d 100644\n--- a/contrib/credential/osxkeychain/Makefile\n+++ b/contrib/credential/osxkeychain/Makefile\n@@ -1,21 +1,55 @@\n # The default target of this Makefile is...\n all:: git-credential-osxkeychain\n \n+include ../../../config.mak.uname\n -include ../../../config.mak.autogen\n -include ../../../config.mak\n \n+ifdef ZLIB_NG\n+\tBASIC_CFLAGS += -DHAVE_ZLIB_NG\n+        ifdef ZLIB_NG_PATH\n+\t\tBASIC_CFLAGS += -I$(ZLIB_NG_PATH)/include\n+\t\tEXTLIBS += $(call libpath_template,$(ZLIB_NG_PATH)/$(lib))\n+        endif\n+\tEXTLIBS += -lz-ng\n+else\n+        ifdef ZLIB_PATH\n+\t\tBASIC_CFLAGS += -I$(ZLIB_PATH)/include\n+\t\tEXTLIBS += $(call libpath_template,$(ZLIB_PATH)/$(lib))\n+        endif\n+\tEXTLIBS += -lz\n+endif\n+ifndef NO_ICONV\n+        ifdef NEEDS_LIBICONV\n+                ifdef ICONVDIR\n+\t\t\tBASIC_CFLAGS += -I$(ICONVDIR)/include\n+\t\t\tICONV_LINK = $(call libpath_template,$(ICONVDIR)/$(lib))\n+                else\n+\t\t\tICONV_LINK =\n+                endif\n+                ifdef NEEDS_LIBINTL_BEFORE_LIBICONV\n+\t\t\tICONV_LINK += -lintl\n+                endif\n+\t\tEXTLIBS += $(ICONV_LINK) -liconv\n+        endif\n+endif\n+ifndef LIBC_CONTAINS_LIBINTL\n+\tEXTLIBS += -lintl\n+endif\n+\n prefix ?= /usr/local\n gitexecdir ?= $(prefix)/libexec/git-core\n \n CC ?= gcc\n-CFLAGS ?= -g -O2 -Wall\n+CFLAGS ?= -g -O2 -Wall -I../../.. $(BASIC_CFLAGS)\n+LDFLAGS ?= $(BASIC_LDFLAGS) $(EXTLIBS)\n INSTALL ?= install\n RM ?= rm -f\n \n %.o: %.c\n \t$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ -c $<\n \n-git-credential-osxkeychain: git-credential-osxkeychain.o\n+git-credential-osxkeychain: git-credential-osxkeychain.o ../../../libgit.a\n \t$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) \\\n \t\t-framework Security -framework CoreFoundation\n \n@@ -23,6 +57,9 @@ install: git-credential-osxkeychain\n \t$(INSTALL) -d -m 755 $(DESTDIR)$(gitexecdir)\n \t$(INSTALL) -m 755 $< $(DESTDIR)$(gitexecdir)\n \n+../../../libgit.a:\n+\tcd ../../..; make libgit.a\n+\n clean:\n \t$(RM) git-credential-osxkeychain git-credential-osxkeychain.o\n \ndiff --git a/contrib/credential/osxkeychain/git-credential-osxkeychain.c b/contrib/credential/osxkeychain/git-credential-osxkeychain.c\nindex 611c9798b3..b180267034 100644\n--- a/contrib/credential/osxkeychain/git-credential-osxkeychain.c\n+++ b/contrib/credential/osxkeychain/git-credential-osxkeychain.c\n@@ -2,6 +2,9 @@\n #include <string.h>\n #include <stdlib.h>\n #include <Security/Security.h>\n+#include \"git-compat-util.h\"\n+#include \"strbuf.h\"\n+#include \"wrapper.h\"\n \n #define ENCODING kCFStringEncodingUTF8\n static CFStringRef protocol; /* Stores constant strings - not memory managed */\n@@ -12,7 +15,7 @@ static CFStringRef username;\n static CFDataRef password;\n static CFDataRef password_expiry_utc;\n static CFDataRef oauth_refresh_token;\n-static int state_seen;\n+static char *state_seen;\n \n static void clear_credential(void)\n {\n@@ -48,27 +51,6 @@ static void clear_credential(void)\n \n #define STRING_WITH_LENGTH(s) s, sizeof(s) - 1\n \n-__attribute__((format (printf, 1, 2), __noreturn__))\n-static void die(const char *err, ...)\n-{\n-\tchar msg[4096];\n-\tva_list params;\n-\tva_start(params, err);\n-\tvsnprintf(msg, sizeof(msg), err, params);\n-\tfprintf(stderr, \"%s\\n\", msg);\n-\tva_end(params);\n-\tclear_credential();\n-\texit(1);\n-}\n-\n-static void *xmalloc(size_t len)\n-{\n-\tvoid *ret = malloc(len);\n-\tif (!ret)\n-\t\tdie(\"Out of memory\");\n-\treturn ret;\n-}\n-\n static CFDictionaryRef create_dictionary(CFAllocatorRef allocator, ...)\n {\n \tva_list args;\n@@ -112,6 +94,66 @@ static void write_item(const char *what, const char *buf, size_t len)\n \tputchar('\\n');\n }\n \n+static void write_item_strbuf(struct strbuf *sb, const char *what, const char *buf, int n)\n+{\n+\tchar s[32];\n+\n+\txsnprintf(s, sizeof(s), \"__%s=\", what);\n+\tstrbuf_add(sb, s, strlen(s));\n+\tstrbuf_add(sb, buf, n);\n+}\n+\n+static void write_item_strbuf_cfstring(struct strbuf *sb, const char *what, CFStringRef ref)\n+{\n+\tchar *buf;\n+\tint len;\n+\n+\tif (!ref)\n+\t\treturn;\n+\tlen = CFStringGetMaximumSizeForEncoding(CFStringGetLength(ref), ENCODING) + 1;\n+\tbuf = xmalloc(len);\n+\tif (CFStringGetCString(ref, buf, len, ENCODING))\n+\t\twrite_item_strbuf(sb, what, buf, strlen(buf));\n+\tfree(buf);\n+}\n+\n+static void write_item_strbuf_cfnumber(struct strbuf *sb, const char *what, CFNumberRef ref)\n+{\n+\tshort n;\n+\tchar buf[32];\n+\n+\tif (!ref)\n+\t\treturn;\n+\tif (!CFNumberGetValue(ref, kCFNumberShortType, &n))\n+\t\treturn;\n+\txsnprintf(buf, sizeof(buf), \"%d\", n);\n+\twrite_item_strbuf(sb, what, buf, strlen(buf));\n+}\n+\n+static void write_item_strbuf_cfdata(struct strbuf *sb, const char *what, CFDataRef ref)\n+{\n+\tchar *buf;\n+\tint len;\n+\n+\tif (!ref)\n+\t\treturn;\n+\tbuf = (char *)CFDataGetBytePtr(ref);\n+\tif (!buf || strlen(buf) == 0)\n+\t\treturn;\n+\tlen = CFDataGetLength(ref);\n+\twrite_item_strbuf(sb, what, buf, len);\n+}\n+\n+static void encode_state_seen(struct strbuf *sb)\n+{\n+\tstrbuf_add(sb, \"osxkeychain:seen=\", strlen(\"osxkeychain:seen=\"));\n+\twrite_item_strbuf_cfstring(sb, \"host\", host);\n+\twrite_item_strbuf_cfnumber(sb, \"port\", port);\n+\twrite_item_strbuf_cfstring(sb, \"path\", path);\n+\twrite_item_strbuf_cfstring(sb, \"username\", username);\n+\twrite_item_strbuf_cfdata(sb, \"password\", password);\n+}\n+\n static void find_username_in_item(CFDictionaryRef item)\n {\n \tCFStringRef account_ref;\n@@ -124,6 +166,7 @@ static void find_username_in_item(CFDictionaryRef item)\n \t\twrite_item(\"username\", \"\", 0);\n \t\treturn;\n \t}\n+\tusername = CFStringCreateCopy(kCFAllocatorDefault, account_ref);\n \n \tusername_buf = (char *)CFStringGetCStringPtr(account_ref, ENCODING);\n \tif (username_buf)\n@@ -163,6 +206,7 @@ static OSStatus find_internet_password(void)\n \t}\n \n \tdata = CFDictionaryGetValue(item, kSecValueData);\n+\tpassword = CFDataCreateCopy(kCFAllocatorDefault, data);\n \n \twrite_item(\"password\",\n \t\t   (const char *)CFDataGetBytePtr(data),\n@@ -173,7 +217,14 @@ static OSStatus find_internet_password(void)\n \tCFRelease(item);\n \n \twrite_item(\"capability[]\", \"state\", strlen(\"state\"));\n-\twrite_item(\"state[]\", \"osxkeychain:seen=1\", strlen(\"osxkeychain:seen=1\"));\n+\t{\n+\t\tstruct strbuf sb;\n+\n+\t\tstrbuf_init(&sb, 1024);\n+\t\tencode_state_seen(&sb);\n+\t\twrite_item(\"state[]\", sb.buf, strlen(sb.buf));\n+\t\tstrbuf_release(&sb);\n+\t}\n \n out:\n \tCFRelease(attrs);\n@@ -288,13 +339,22 @@ static OSStatus add_internet_password(void)\n \tCFDictionaryRef attrs;\n \tOSStatus result;\n \n-\tif (state_seen)\n-\t\treturn errSecSuccess;\n-\n \t/* Only store complete credentials */\n \tif (!protocol || !host || !username || !password)\n \t\treturn -1;\n \n+\tif (state_seen) {\n+\t\tstruct strbuf sb;\n+\n+\t\tstrbuf_init(&sb, 1024);\n+\t\tencode_state_seen(&sb);\n+\t\tif (!strcmp(state_seen, sb.buf)) {\n+\t\t\tstrbuf_release(&sb);\n+\t\t\treturn errSecSuccess;\n+\t\t}\n+\t\tstrbuf_release(&sb);\n+\t}\n+\n \tdata = CFDataCreateMutableCopy(kCFAllocatorDefault, 0, password);\n \tif (password_expiry_utc) {\n \t\tCFDataAppendBytes(data,\n@@ -403,8 +463,9 @@ static void read_credential(void)\n \t\t\t\t\t\t\t   (UInt8 *)v,\n \t\t\t\t\t\t\t   strlen(v));\n \t\telse if (!strcmp(buf, \"state[]\")) {\n-\t\t\tif (!strcmp(v, \"osxkeychain:seen=1\"))\n-\t\t\t\tstate_seen = 1;\n+\t\t\tint len = strlen(\"osxkeychain:seen=\");\n+\t\t\tif (!strncmp(v, \"osxkeychain:seen=\", len))\n+\t\t\t\tstate_seen = xstrdup(v);\n \t\t}\n \t\t/*\n \t\t * Ignore other lines; we don't know what they mean, but\n@@ -443,5 +504,8 @@ int main(int argc, const char **argv)\n \n \tclear_credential();\n \n+\tif (state_seen)\n+\t\tfree(state_seen);\n+\n \treturn 0;\n }\ndiff --git a/contrib/credential/osxkeychain/meson.build b/contrib/credential/osxkeychain/meson.build\nindex 3c7677f736..ec91d0c14b 100644\n--- a/contrib/credential/osxkeychain/meson.build\n+++ b/contrib/credential/osxkeychain/meson.build\n@@ -1,6 +1,7 @@\n executable('git-credential-osxkeychain',\n   sources: 'git-credential-osxkeychain.c',\n   dependencies: [\n+    libgit,\n     dependency('CoreFoundation'),\n     dependency('Security'),\n   ],\n\nbase-commit: fd372d9b1a69a01a676398882bbe3840bf51fe72\n-- \ngitgitgadget\n"},{"id":"530895","messageId":"20251118095714.GD530545@coredump.intra.peff.net","threadId":"64475","inReplyTo":"xmqqo6p5llsw.fsf@gitster.g","subject":"Re: [PATCH] osxkeychain: avoid incorrectly skipping store operation","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-11-18T09:57:14Z","receivedAt":"2025-11-18T09:57:16Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Thu, Nov 13, 2025 at 12:28:15PM -0800, Junio C Hamano wrote:\n\n> \"Koji Nakamaru via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n> \n> > +/*\n> > + * NOTE: We could use functions in strbuf.h and/or wrapper.h, but those\n> > + * introduce significant dependencies. Therefore, we define simplified\n> > + * versions here to keep this code self-contained.\n> > + */\n> \n> Sorry, but I do not quite understand this comment.  The program is\n> shipped as a part of Git, and using these functions and linking with\n> libgit.a may pull strbuf.o and some other *.o files out of libgit.a\n> to link with git-credential-osxkeychain.o to produce the executable,\n> but how can that be \"significant dependencies\"?  For anybody who is\n> building git-credential-osxkeychain, the necessary sources come for\n> free.\n\nBack when we added the contrib/credential helpers, I tried to avoid\nlinking with Git for two reasons:\n\n  1. The idea was that these _could_ be independent projects, and we\n     would not be on the hook for writing or maintaining everyone's pet\n     platform helper. So even though they are in our tree, the hope was\n     that they'd be simple enough to be totally independent programs\n     (and would not even have to be written in C). And avoiding any\n     dependencies kept us honest there.\n\n     It may be that the cost of not being able to re-use our usual code\n     is too high for the philosophical benefit, though.\n\n  2. If stuff in contrib/ depends on code in libgit.a, then changes in\n     the latter can break them. And I don't think we have a great flow\n     for detecting such breakage. Maybe one of the CI jobs builds\n     osxkeychain now? I'm not even sure.\n\n     The xmalloc and strbuf interfaces are pretty stable, so it may be\n     that the right rule is \"you can depend on libgit.a, but only\n     lightly\".\n\nMostly just offering my two cents (and a little backstory). I'm not\nterribly opposed to loosening the rule, but we may expect some breakage\nvia (2) from time to time.\n\n-Peff\n"}]}