From: "M Hickford via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: Eric Sunshine <sunshine@sunshineco.com>,
Jeff King <peff@peff.net>, Cheetham <mjcheetham@outlook.com>,
Dennington <lessleydennington@gmail.com>,
M Hickford <mirth.hickford@gmail.com>,
M Hickford <mirth.hickford@gmail.com>
Subject: [PATCH v3] credential: new attribute password_expiry_utc
Date: Sat, 04 Feb 2023 21:16:12 +0000 [thread overview]
Message-ID: <pull.1443.v3.git.git.1675545372271.gitgitgadget@gmail.com> (raw)
In-Reply-To: <pull.1443.v2.git.git.1675244392025.gitgitgadget@gmail.com>
From: M Hickford <mirth.hickford@gmail.com>
Some passwords have an expiry date known at generation. This may be
years away for a personal access token or hours for an OAuth access
token.
When multiple credential helpers are configured, `credential fill` tries
each helper in turn until it has a username and password, returning
early. If Git authentication succeeds, `credential approve`
stores the successful credential in all helpers. If authentication
fails, `credential reject` erases matching credentials in all helpers.
Helpers implement corresponding operations: get, store, erase.
The credential protocol has no expiry attribute, so helpers cannot
store expiry information. (Even if a helper returned an improvised
expiry attribute, git credential discards unrecognised attributes
between operations and between helpers.)
As a workaround, whenever monolithic helper Git Credential Manager (GCM)
retrieves an OAuth credential from its storage, it makes a HTTP request
to check whether the OAuth token has expired [1]. This complicates and
slows the authentication happy path.
Worse is the case that a storage helper and a credential-generating
helper are configured together:
[credential]
helper = storage # eg. cache or osxkeychain
helper = generate # eg. oauth or manager
`credential approve` stores the generated credential in both helpers
without expiry information. Later `credential fill` may return an
expired credential from storage. There is no workaround, no matter how
clever the second helper.
Introduce a password expiry attribute. In `credential fill`, ignore
expired passwords and continue to query subsequent helpers.
In the example above, `credential fill` ignores the expired credential
and a fresh credential is generated. If authentication succeeds,
`credential approve` replaces the expired credential in storage.
If authentication fails, the expired credential is erased by
`credential reject`. It is unnecessary but harmless for storage
helpers to self prune expired credentials.
Add support for the new attribute to credential-cache.
Eventually, I hope to see support in other storage helpers.
Example usage in a credential-generating helper
https://github.com/hickford/git-credential-oauth/pull/16
[1] https://github.com/GitCredentialManager/git-credential-manager/blob/66b94e489ad8cc1982836355493e369770b30211/src/shared/GitLab/GitLabHostProvider.cs#L217
Signed-off-by: M Hickford <mirth.hickford@gmail.com>
---
credential: new attribute password_expiry_utc
details in commit message
Changes in patch v3:
* Tests
* Simplified credential_read, moved expiry logic to credential_fill
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1443%2Fhickford%2Fpassword-expiry-v3
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1443/hickford/password-expiry-v3
Pull-Request: https://github.com/git/git/pull/1443
Range-diff vs v2:
1: b9ee729ee4d ! 1: 1846815a5c1 credential: new attribute password_expiry_utc
@@ Commit message
years away for a personal access token or hours for an OAuth access
token.
- Currently the credential protocol has no expiry attribute. When multiple
- helpers are configured, `credential fill` tries each helper in turn
- until it has a username and password, returning early.
+ When multiple credential helpers are configured, `credential fill` tries
+ each helper in turn until it has a username and password, returning
+ early. If Git authentication succeeds, `credential approve`
+ stores the successful credential in all helpers. If authentication
+ fails, `credential reject` erases matching credentials in all helpers.
+ Helpers implement corresponding operations: get, store, erase.
- When a storage helper and a credential-generating helper are configured
- together, the credential is necessarily stored without expiry, so
- `credential fill` may later return an expired credential from storage.
+ The credential protocol has no expiry attribute, so helpers cannot
+ store expiry information. (Even if a helper returned an improvised
+ expiry attribute, git credential discards unrecognised attributes
+ between operations and between helpers.)
- ```
- [credential]
- helper = storage # eg. cache or osxkeychain
- helper = generate # eg. oauth
- ```
+ As a workaround, whenever monolithic helper Git Credential Manager (GCM)
+ retrieves an OAuth credential from its storage, it makes a HTTP request
+ to check whether the OAuth token has expired [1]. This complicates and
+ slows the authentication happy path.
- An improvement is to introduce a password expiry attribute to the
- credential protocol. If the expiry date has passed, `credential fill`
- ignores the password attribute, so subsequent helpers can generate a
- fresh credential. This is backwards compatible -- no change in
- behaviour with helpers that discard the expiry attribute.
+ Worse is the case that a storage helper and a credential-generating
+ helper are configured together:
- Note that the expiry logic is entirely within the credential layer.
- Compatible helpers store and retrieve the new attribute like any other.
- This keeps the helper contract simple.
+ [credential]
+ helper = storage # eg. cache or osxkeychain
+ helper = generate # eg. oauth or manager
- This patch adds support for the new attribute to cache.
+ `credential approve` stores the generated credential in both helpers
+ without expiry information. Later `credential fill` may return an
+ expired credential from storage. There is no workaround, no matter how
+ clever the second helper.
+
+ Introduce a password expiry attribute. In `credential fill`, ignore
+ expired passwords and continue to query subsequent helpers.
+
+ In the example above, `credential fill` ignores the expired credential
+ and a fresh credential is generated. If authentication succeeds,
+ `credential approve` replaces the expired credential in storage.
+ If authentication fails, the expired credential is erased by
+ `credential reject`. It is unnecessary but harmless for storage
+ helpers to self prune expired credentials.
+
+ Add support for the new attribute to credential-cache.
+ Eventually, I hope to see support in other storage helpers.
Example usage in a credential-generating helper
https://github.com/hickford/git-credential-oauth/pull/16
- Future ideas: make it possible for a storage helper to provide OAuth
- refresh token to subsequent helpers.
- https://github.com/gitgitgadget/git/pull/1394
+ [1] https://github.com/GitCredentialManager/git-credential-manager/blob/66b94e489ad8cc1982836355493e369770b30211/src/shared/GitLab/GitLabHostProvider.cs#L217
Signed-off-by: M Hickford <mirth.hickford@gmail.com>
@@ Documentation/git-credential.txt: Git understands the following attributes:
+`password_expiry_utc`::
+
-+ If password is a personal access token or OAuth access token, it may have an
-+ expiry date. When getting credentials from a helper, `git credential fill`
-+ ignores the password attribute if the expiry date has passed. Storage
-+ helpers should store this attribute if possible. Helpers should not
-+ implement expiry logic themselves. Represented as Unix time UTC, seconds
-+ since 1970.
++ Generated passwords such as an OAuth access token may have an expiry date.
++ When reading credentials from helpers, `git credential fill` ignores expired
++ passwords. Represented as Unix time UTC, seconds since 1970.
+
`url`::
When this special attribute is read by `git credential`, the
+ ## Documentation/gitcredentials.txt ##
+@@ Documentation/gitcredentials.txt: helper::
+ If there are multiple instances of the `credential.helper` configuration
+ variable, each helper will be tried in turn, and may provide a username,
+ password, or nothing. Once Git has acquired both a username and a
+-password, no more helpers will be tried.
++unexpired password, no more helpers will be tried.
+ +
+ If `credential.helper` is configured to the empty string, this resets
+ the helper list to empty (so you may override a helper set by a
+
## builtin/credential-cache--daemon.c ##
@@ builtin/credential-cache--daemon.c: static void serve_one_client(FILE *in, FILE *out)
if (e) {
@@ credential.c
void credential_init(struct credential *c)
{
-@@ credential.c: static void credential_getpass(struct credential *c)
- if (!c->username)
- c->username = credential_ask_one("Username", c,
- PROMPT_ASKPASS|PROMPT_ECHO);
-- if (!c->password)
-+ if (!c->password || c->password_expiry_utc < time(NULL)) {
-+ c->password_expiry_utc = TIME_MAX;
- c->password = credential_ask_one("Password", c,
- PROMPT_ASKPASS);
-+ }
- }
-
- int credential_read(struct credential *c, FILE *fp)
- {
- struct strbuf line = STRBUF_INIT;
-
-+ int password_updated = 0;
-+ timestamp_t this_password_expiry = TIME_MAX;
-+
- while (strbuf_getline(&line, fp) != EOF) {
- char *key = line.buf;
- char *value = strchr(key, '=');
-@@ credential.c: int credential_read(struct credential *c, FILE *fp)
- } else if (!strcmp(key, "password")) {
- free(c->password);
- c->password = xstrdup(value);
-+ password_updated = 1;
- } else if (!strcmp(key, "protocol")) {
- free(c->protocol);
- c->protocol = xstrdup(value);
@@ credential.c: int credential_read(struct credential *c, FILE *fp)
} else if (!strcmp(key, "path")) {
free(c->path);
c->path = xstrdup(value);
+ } else if (!strcmp(key, "password_expiry_utc")) {
-+ this_password_expiry = parse_timestamp(value, NULL, 10);
-+ if (this_password_expiry == 0 || errno) {
-+ this_password_expiry = TIME_MAX;
-+ }
++ c->password_expiry_utc = parse_timestamp(value, NULL, 10);
++ if (c->password_expiry_utc == 0 || errno)
++ c->password_expiry_utc = TIME_MAX;
} else if (!strcmp(key, "url")) {
credential_from_url(c, value);
} else if (!strcmp(key, "quit")) {
-@@ credential.c: int credential_read(struct credential *c, FILE *fp)
- */
- }
-
-+ if (password_updated)
-+ c->password_expiry_utc = this_password_expiry;
-+
- strbuf_release(&line);
- return 0;
- }
@@ credential.c: void credential_write(const struct credential *c, FILE *fp)
credential_write_item(fp, "path", c->path, 0);
credential_write_item(fp, "username", c->username, 0);
@@ credential.c: void credential_fill(struct credential *c)
for (i = 0; i < c->helpers.nr; i++) {
credential_do(c, c->helpers.items[i].string, "get");
-- if (c->username && c->password)
-+ if (c->username && c->password && time(NULL) < c->password_expiry_utc)
++ if (c->password_expiry_utc < time(NULL)) {
++ FREE_AND_NULL(c->password);
++ c->password_expiry_utc = TIME_MAX;
++ }
+ if (c->username && c->password)
return;
if (c->quit)
- die("credential helper '%s' told us to quit",
+@@ credential.c: void credential_approve(struct credential *c)
+
+ if (c->approved)
+ return;
+- if (!c->username || !c->password)
++ if (!c->username || !c->password || c->password_expiry_utc < time(NULL))
+ return;
+
+ credential_apply_config(c);
+@@ credential.c: void credential_reject(struct credential *c)
+
+ FREE_AND_NULL(c->username);
+ FREE_AND_NULL(c->password);
++ c->password_expiry_utc = TIME_MAX;
+ c->approved = 0;
+ }
+
## credential.h ##
@@ credential.h: struct credential {
@@ credential.h: struct credential {
}
/* Initialize a credential structure, setting all fields to empty. */
+
+ ## t/t0300-credentials.sh ##
+@@ t/t0300-credentials.sh: test_expect_success 'setup helper scripts' '
+ test -z "$pass" || echo password=$pass
+ EOF
+
++ write_script git-credential-verbatim-with-expiry <<-\EOF &&
++ user=$1; shift
++ pass=$1; shift
++ pexpiry=$1; shift
++ . ./dump
++ test -z "$user" || echo username=$user
++ test -z "$pass" || echo password=$pass
++ test -z "$pexpiry" || echo password_expiry_utc=$pexpiry
++ EOF
++
+ PATH="$PWD:$PATH"
+ '
+
+@@ t/t0300-credentials.sh: test_expect_success 'credential_fill continues through partial response' '
+ EOF
+ '
+
++test_expect_success 'credential_fill populates password_expiry_utc' '
++ check fill "verbatim-with-expiry one two 9999999999" <<-\EOF
++ protocol=http
++ host=example.com
++ --
++ protocol=http
++ host=example.com
++ username=one
++ password=two
++ password_expiry_utc=9999999999
++ --
++ verbatim-with-expiry: get
++ verbatim-with-expiry: protocol=http
++ verbatim-with-expiry: host=example.com
++ EOF
++'
++
++test_expect_success 'credential_fill continues through expired password' '
++ check fill "verbatim-with-expiry one two 5" "verbatim three four" <<-\EOF
++ protocol=http
++ host=example.com
++ --
++ protocol=http
++ host=example.com
++ username=three
++ password=four
++ --
++ verbatim-with-expiry: get
++ verbatim-with-expiry: protocol=http
++ verbatim-with-expiry: host=example.com
++ verbatim: get
++ verbatim: protocol=http
++ verbatim: host=example.com
++ verbatim: username=one
++ EOF
++'
++
+ test_expect_success 'credential_fill passes along metadata' '
+ check fill "verbatim one two" <<-\EOF
+ protocol=ftp
+@@ t/t0300-credentials.sh: test_expect_success 'credential_approve calls all helpers' '
+ EOF
+ '
+
++test_expect_success 'credential_approve stores password expiry' '
++ check approve useless <<-\EOF
++ protocol=http
++ host=example.com
++ username=foo
++ password=bar
++ password_expiry_utc=9999999999
++ --
++ --
++ useless: store
++ useless: protocol=http
++ useless: host=example.com
++ useless: username=foo
++ useless: password=bar
++ useless: password_expiry_utc=9999999999
++ EOF
++'
++
+ test_expect_success 'do not bother storing password-less credential' '
+ check approve useless <<-\EOF
+ protocol=http
+@@ t/t0300-credentials.sh: test_expect_success 'do not bother storing password-less credential' '
+ EOF
+ '
+
++test_expect_success 'credential_approve does not store expired credential' '
++ check approve useless <<-\EOF
++ protocol=http
++ host=example.com
++ username=foo
++ password=bar
++ password_expiry_utc=5
++ --
++ --
++ EOF
++'
+
+ test_expect_success 'credential_reject calls all helpers' '
+ check reject useless "verbatim one two" <<-\EOF
+@@ t/t0300-credentials.sh: test_expect_success 'credential_reject calls all helpers' '
+ EOF
+ '
+
++test_expect_success 'credential_reject erases expired credential' '
++ check reject useless <<-\EOF
++ protocol=http
++ host=example.com
++ username=foo
++ password=bar
++ password_expiry_utc=5
++ --
++ --
++ useless: erase
++ useless: protocol=http
++ useless: host=example.com
++ useless: username=foo
++ useless: password=bar
++ useless: password_expiry_utc=5
++ EOF
++'
++
+ test_expect_success 'usernames can be preserved' '
+ check fill "verbatim \"\" three" <<-\EOF
+ protocol=http
Documentation/git-credential.txt | 6 ++
Documentation/gitcredentials.txt | 2 +-
builtin/credential-cache--daemon.c | 3 +
credential.c | 17 +++++-
credential.h | 2 +
t/t0300-credentials.sh | 94 ++++++++++++++++++++++++++++++
6 files changed, 122 insertions(+), 2 deletions(-)
diff --git a/Documentation/git-credential.txt b/Documentation/git-credential.txt
index ac2818b9f66..29d184ab824 100644
--- a/Documentation/git-credential.txt
+++ b/Documentation/git-credential.txt
@@ -144,6 +144,12 @@ Git understands the following attributes:
The credential's password, if we are asking it to be stored.
+`password_expiry_utc`::
+
+ Generated passwords such as an OAuth access token may have an expiry date.
+ When reading credentials from helpers, `git credential fill` ignores expired
+ passwords. Represented as Unix time UTC, seconds since 1970.
+
`url`::
When this special attribute is read by `git credential`, the
diff --git a/Documentation/gitcredentials.txt b/Documentation/gitcredentials.txt
index 4522471c337..95636b18439 100644
--- a/Documentation/gitcredentials.txt
+++ b/Documentation/gitcredentials.txt
@@ -167,7 +167,7 @@ helper::
If there are multiple instances of the `credential.helper` configuration
variable, each helper will be tried in turn, and may provide a username,
password, or nothing. Once Git has acquired both a username and a
-password, no more helpers will be tried.
+unexpired password, no more helpers will be tried.
+
If `credential.helper` is configured to the empty string, this resets
the helper list to empty (so you may override a helper set by a
diff --git a/builtin/credential-cache--daemon.c b/builtin/credential-cache--daemon.c
index f3c89831d4a..338058be7f9 100644
--- a/builtin/credential-cache--daemon.c
+++ b/builtin/credential-cache--daemon.c
@@ -127,6 +127,9 @@ static void serve_one_client(FILE *in, FILE *out)
if (e) {
fprintf(out, "username=%s\n", e->item.username);
fprintf(out, "password=%s\n", e->item.password);
+ if (e->item.password_expiry_utc != TIME_MAX)
+ fprintf(out, "password_expiry_utc=%"PRItime"\n",
+ e->item.password_expiry_utc);
}
}
else if (!strcmp(action.buf, "exit")) {
diff --git a/credential.c b/credential.c
index f6389a50684..d3e1bf7a679 100644
--- a/credential.c
+++ b/credential.c
@@ -7,6 +7,7 @@
#include "prompt.h"
#include "sigchain.h"
#include "urlmatch.h"
+#include "git-compat-util.h"
void credential_init(struct credential *c)
{
@@ -234,6 +235,10 @@ int credential_read(struct credential *c, FILE *fp)
} else if (!strcmp(key, "path")) {
free(c->path);
c->path = xstrdup(value);
+ } else if (!strcmp(key, "password_expiry_utc")) {
+ c->password_expiry_utc = parse_timestamp(value, NULL, 10);
+ if (c->password_expiry_utc == 0 || errno)
+ c->password_expiry_utc = TIME_MAX;
} else if (!strcmp(key, "url")) {
credential_from_url(c, value);
} else if (!strcmp(key, "quit")) {
@@ -269,6 +274,11 @@ void credential_write(const struct credential *c, FILE *fp)
credential_write_item(fp, "path", c->path, 0);
credential_write_item(fp, "username", c->username, 0);
credential_write_item(fp, "password", c->password, 0);
+ if (c->password_expiry_utc != TIME_MAX) {
+ char *s = xstrfmt("%"PRItime, c->password_expiry_utc);
+ credential_write_item(fp, "password_expiry_utc", s, 0);
+ free(s);
+ }
}
static int run_credential_helper(struct credential *c,
@@ -342,6 +352,10 @@ void credential_fill(struct credential *c)
for (i = 0; i < c->helpers.nr; i++) {
credential_do(c, c->helpers.items[i].string, "get");
+ if (c->password_expiry_utc < time(NULL)) {
+ FREE_AND_NULL(c->password);
+ c->password_expiry_utc = TIME_MAX;
+ }
if (c->username && c->password)
return;
if (c->quit)
@@ -360,7 +374,7 @@ void credential_approve(struct credential *c)
if (c->approved)
return;
- if (!c->username || !c->password)
+ if (!c->username || !c->password || c->password_expiry_utc < time(NULL))
return;
credential_apply_config(c);
@@ -381,6 +395,7 @@ void credential_reject(struct credential *c)
FREE_AND_NULL(c->username);
FREE_AND_NULL(c->password);
+ c->password_expiry_utc = TIME_MAX;
c->approved = 0;
}
diff --git a/credential.h b/credential.h
index f430e77fea4..935b28a70f1 100644
--- a/credential.h
+++ b/credential.h
@@ -126,10 +126,12 @@ struct credential {
char *protocol;
char *host;
char *path;
+ timestamp_t password_expiry_utc;
};
#define CREDENTIAL_INIT { \
.helpers = STRING_LIST_INIT_DUP, \
+ .password_expiry_utc = TIME_MAX, \
}
/* Initialize a credential structure, setting all fields to empty. */
diff --git a/t/t0300-credentials.sh b/t/t0300-credentials.sh
index 3485c0534e6..96391015af5 100755
--- a/t/t0300-credentials.sh
+++ b/t/t0300-credentials.sh
@@ -35,6 +35,16 @@ test_expect_success 'setup helper scripts' '
test -z "$pass" || echo password=$pass
EOF
+ write_script git-credential-verbatim-with-expiry <<-\EOF &&
+ user=$1; shift
+ pass=$1; shift
+ pexpiry=$1; shift
+ . ./dump
+ test -z "$user" || echo username=$user
+ test -z "$pass" || echo password=$pass
+ test -z "$pexpiry" || echo password_expiry_utc=$pexpiry
+ EOF
+
PATH="$PWD:$PATH"
'
@@ -109,6 +119,43 @@ test_expect_success 'credential_fill continues through partial response' '
EOF
'
+test_expect_success 'credential_fill populates password_expiry_utc' '
+ check fill "verbatim-with-expiry one two 9999999999" <<-\EOF
+ protocol=http
+ host=example.com
+ --
+ protocol=http
+ host=example.com
+ username=one
+ password=two
+ password_expiry_utc=9999999999
+ --
+ verbatim-with-expiry: get
+ verbatim-with-expiry: protocol=http
+ verbatim-with-expiry: host=example.com
+ EOF
+'
+
+test_expect_success 'credential_fill continues through expired password' '
+ check fill "verbatim-with-expiry one two 5" "verbatim three four" <<-\EOF
+ protocol=http
+ host=example.com
+ --
+ protocol=http
+ host=example.com
+ username=three
+ password=four
+ --
+ verbatim-with-expiry: get
+ verbatim-with-expiry: protocol=http
+ verbatim-with-expiry: host=example.com
+ verbatim: get
+ verbatim: protocol=http
+ verbatim: host=example.com
+ verbatim: username=one
+ EOF
+'
+
test_expect_success 'credential_fill passes along metadata' '
check fill "verbatim one two" <<-\EOF
protocol=ftp
@@ -149,6 +196,24 @@ test_expect_success 'credential_approve calls all helpers' '
EOF
'
+test_expect_success 'credential_approve stores password expiry' '
+ check approve useless <<-\EOF
+ protocol=http
+ host=example.com
+ username=foo
+ password=bar
+ password_expiry_utc=9999999999
+ --
+ --
+ useless: store
+ useless: protocol=http
+ useless: host=example.com
+ useless: username=foo
+ useless: password=bar
+ useless: password_expiry_utc=9999999999
+ EOF
+'
+
test_expect_success 'do not bother storing password-less credential' '
check approve useless <<-\EOF
protocol=http
@@ -159,6 +224,17 @@ test_expect_success 'do not bother storing password-less credential' '
EOF
'
+test_expect_success 'credential_approve does not store expired credential' '
+ check approve useless <<-\EOF
+ protocol=http
+ host=example.com
+ username=foo
+ password=bar
+ password_expiry_utc=5
+ --
+ --
+ EOF
+'
test_expect_success 'credential_reject calls all helpers' '
check reject useless "verbatim one two" <<-\EOF
@@ -181,6 +257,24 @@ test_expect_success 'credential_reject calls all helpers' '
EOF
'
+test_expect_success 'credential_reject erases expired credential' '
+ check reject useless <<-\EOF
+ protocol=http
+ host=example.com
+ username=foo
+ password=bar
+ password_expiry_utc=5
+ --
+ --
+ useless: erase
+ useless: protocol=http
+ useless: host=example.com
+ useless: username=foo
+ useless: password=bar
+ useless: password_expiry_utc=5
+ EOF
+'
+
test_expect_success 'usernames can be preserved' '
check fill "verbatim \"\" three" <<-\EOF
protocol=http
base-commit: 2fc9e9ca3c7505bc60069f11e7ef09b1aeeee473
--
gitgitgadget
next prev parent reply other threads:[~2023-02-04 21:16 UTC|newest]
Thread overview: 26+ messages / expand[flat|nested] mbox.gz Atom feed top
2023-01-28 14:04 [PATCH] credential: new attribute password_expiry_utc M Hickford via GitGitGadget
2023-01-29 20:17 ` Junio C Hamano
2023-02-01 8:29 ` M Hickford
2023-02-01 18:50 ` Junio C Hamano
2023-01-30 0:59 ` Eric Sunshine
2023-02-05 6:49 ` M Hickford
2023-02-01 9:39 ` [PATCH v2] " M Hickford via GitGitGadget
2023-02-01 12:10 ` Jeff King
2023-02-01 17:12 ` Junio C Hamano
2023-02-02 0:12 ` Jeff King
2023-02-01 20:02 ` Matthew John Cheetham
2023-02-02 0:23 ` Jeff King
2023-02-05 6:45 ` M Hickford
2023-02-06 18:59 ` Matthew John Cheetham
2023-02-05 6:34 ` M Hickford
2023-02-04 21:16 ` M Hickford via GitGitGadget [this message]
2023-02-14 1:59 ` [PATCH v3] " Junio C Hamano
2023-02-14 22:36 ` M Hickford
2023-02-17 21:44 ` Lessley Dennington
2023-02-17 21:59 ` Junio C Hamano
2023-02-18 8:00 ` M Hickford
2023-02-14 8:03 ` Martin Ågren
2023-02-16 19:16 ` Calvin Wan
2023-02-18 8:00 ` M Hickford
2023-02-18 6:32 ` [PATCH v4] " M Hickford via GitGitGadget
2023-02-22 19:22 ` Calvin Wan
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
List information: http://vger.kernel.org/majordomo-info.html
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=pull.1443.v3.git.git.1675545372271.gitgitgadget@gmail.com \
--to=gitgitgadget@gmail.com \
--cc=git@vger.kernel.org \
--cc=lessleydennington@gmail.com \
--cc=mirth.hickford@gmail.com \
--cc=mjcheetham@outlook.com \
--cc=peff@peff.net \
--cc=sunshine@sunshineco.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
Code repositories for project(s) associated with this public inbox
https://80x24.org/mirrors/git.git
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).