From: Lucien Kong <Lucien.Kong@ensimag.imag.fr>
To: git@vger.kernel.org
Cc: Lucien Kong <Lucien.Kong@ensimag.imag.fr>,
Valentin Duperray <Valentin.Duperray@ensimag.imag.fr>,
Franck Jonas <Franck.Jonas@ensimag.imag.fr>,
Thomas Nguy <Thomas.Nguy@ensimag.imag.fr>,
Huynh Khoi Nguyen Nguyen
<Huynh-Khoi-Nguyen.Nguyen@ensimag.imag.fr>,
Matthieu Moy <Matthieu.Moy@grenoble-inp.fr>
Subject: [PATCHv3 2/2] Warnings before amending published history
Date: Mon, 11 Jun 2012 23:56:21 +0200 [thread overview]
Message-ID: <1339451781-29324-2-git-send-email-Lucien.Kong@ensimag.imag.fr> (raw)
In-Reply-To: <1339451781-29324-1-git-send-email-Lucien.Kong@ensimag.imag.fr>
This code detects that one is rewriting a commit that is an ancestor
of a remote-tracking branch with "git commit --amend", and warns the
user through the editor.
Signed-off-by: Lucien Kong <Lucien.Kong@ensimag.imag.fr>
Signed-off-by: Valentin Duperray <Valentin.Duperray@ensimag.imag.fr>
Signed-off-by: Franck Jonas <Franck.Jonas@ensimag.imag.fr>
Signed-off-by: Thomas Nguy <Thomas.Nguy@ensimag.imag.fr>
Signed-off-by: Huynh Khoi Nguyen Nguyen <Huynh-Khoi-Nguyen.Nguyen@ensimag.imag.fr>
Signed-off-by: Matthieu Moy <Matthieu.Moy@grenoble-inp.fr>
---
At this point, this feature is not controlled by the config key
rebase.checkremoterefs. Also, the code can only detects the commits
that were published on the same current branch.
builtin/commit.c | 82 ++++++++++++++++++++++++++
sha1_name.c | 95 +++++++-----------------------
sha1_name.h | 130 +++++++++++++++++++++++++++++++++++++++++
t/t3404-rebase-interactive.sh | 65 ++++++++++++++++++++
4 files changed, 298 insertions(+), 74 deletions(-)
create mode 100644 sha1_name.h
diff --git a/builtin/commit.c b/builtin/commit.c
index f43eaaf..53fe120 100644
--- a/builtin/commit.c
+++ b/builtin/commit.c
@@ -28,6 +28,7 @@
#include "submodule.h"
#include "gpg-interface.h"
#include "column.h"
+#include "sha1_name.h"
static const char * const builtin_commit_usage[] = {
"git commit [options] [--] <filepattern>...",
@@ -584,6 +585,83 @@ static char *cut_ident_timestamp_part(char *string)
return ket;
}
+static char *read_line_from_git_path(const char *filename)
+{
+ struct strbuf buf = STRBUF_INIT;
+ FILE *fp = fopen(git_path("%s", filename), "r");
+ if (!fp) {
+ strbuf_release(&buf);
+ return NULL;
+ }
+ strbuf_getline(&buf, fp, '\n');
+ if (!fclose(fp))
+ return strbuf_detach(&buf, NULL);
+ else
+ return NULL;
+}
+
+static int insert_first_line_file(char *to_write, char *file_to_modify)
+{
+ int c;
+ FILE *tmp = tmpfile();
+ FILE *file = fopen(file_to_modify, "r+");
+ if (!file || !tmp)
+ return 0;
+
+ while ((c = fgetc(file)) != EOF)
+ fputc(c, tmp);
+
+ rewind(file);
+ rewind(tmp);
+ fputs(to_write, file);
+ while ((c = fgetc(tmp)) != EOF)
+ fputc(c, file);
+
+ fclose(tmp);
+ fclose(file);
+ return 1;
+}
+
+static int amend_warn_published(void)
+{
+ char *head_path = read_line_from_git_path("HEAD");
+ char *last_commit_sha1;
+ char remote_path[PATH_MAX] = "refs/remotes/origin/";
+ char *remote_branch;
+ unsigned char nth_ancestor_remote_sha1[20];
+ int i = 0;
+
+ if (!head_path)
+ return 0;
+
+ strtok(head_path, " ");
+ last_commit_sha1 = read_line_from_git_path(strtok(NULL, ""));
+ if (!last_commit_sha1)
+ return 0;
+
+ remote_branch = read_line_from_git_path("HEAD");
+ if (!remote_branch)
+ return 0;
+
+ strtok(remote_branch, "/");
+ strtok(NULL, "/");
+ strcat(remote_path, strtok(NULL, ""));
+
+ while (!get_nth_ancestor(remote_path, 40, nth_ancestor_remote_sha1, i)) {
+ if (!strcmp(sha1_to_hex(nth_ancestor_remote_sha1), last_commit_sha1)) {
+ insert_first_line_file("# The commit to reword is already published.\n\n",
+ git_path(commit_editmsg));
+ break;
+ }
+ i++;
+ }
+
+ free(head_path);
+ free(last_commit_sha1);
+ free(remote_branch);
+ return 1;
+}
+
static int prepare_to_commit(const char *index_file, const char *prefix,
struct commit *current_head,
struct wt_status *s,
@@ -840,6 +918,10 @@ static int prepare_to_commit(const char *index_file, const char *prefix,
const char *env[2] = { NULL };
env[0] = index;
snprintf(index, sizeof(index), "GIT_INDEX_FILE=%s", index_file);
+
+ if (amend)
+ amend_warn_published();
+
if (launch_editor(git_path(commit_editmsg), NULL, env)) {
fprintf(stderr,
_("Please supply the message using either -m or -F option.\n"));
diff --git a/sha1_name.c b/sha1_name.c
index c633113..14a0e96 100644
--- a/sha1_name.c
+++ b/sha1_name.c
@@ -1,4 +1,5 @@
#include "cache.h"
+#include "sha1_name.h"
#include "tag.h"
#include "commit.h"
#include "tree.h"
@@ -7,9 +8,7 @@
#include "refs.h"
#include "remote.h"
-static int get_sha1_oneline(const char *, unsigned char *, struct commit_list *);
-
-static int find_short_object_filename(int len, const char *name, unsigned char *sha1)
+int find_short_object_filename(int len, const char *name, unsigned char *sha1)
{
struct alternate_object_database *alt;
char hex[40];
@@ -56,7 +55,7 @@ static int find_short_object_filename(int len, const char *name, unsigned char *
return found;
}
-static int match_sha(unsigned len, const unsigned char *a, const unsigned char *b)
+int match_sha(unsigned len, const unsigned char *a, const unsigned char *b)
{
do {
if (*a != *b)
@@ -71,7 +70,7 @@ static int match_sha(unsigned len, const unsigned char *a, const unsigned char *
return 1;
}
-static int find_short_packed_object(int len, const unsigned char *match, unsigned char *sha1)
+int find_short_packed_object(int len, const unsigned char *match, unsigned char *sha1)
{
struct packed_git *p;
const unsigned char *found_sha1 = NULL;
@@ -130,10 +129,7 @@ static int find_short_packed_object(int len, const unsigned char *match, unsigne
return found;
}
-#define SHORT_NAME_NOT_FOUND (-1)
-#define SHORT_NAME_AMBIGUOUS (-2)
-
-static int find_unique_short_object(int len, char *canonical,
+int find_unique_short_object(int len, char *canonical,
unsigned char *res, unsigned char *sha1)
{
int has_unpacked, has_packed;
@@ -157,7 +153,7 @@ static int find_unique_short_object(int len, char *canonical,
return 0;
}
-static int get_short_sha1(const char *name, int len, unsigned char *sha1,
+int get_short_sha1(const char *name, int len, unsigned char *sha1,
int quietly)
{
int i, status;
@@ -216,7 +212,7 @@ const char *find_unique_abbrev(const unsigned char *sha1, int len)
return hex;
}
-static int ambiguous_path(const char *path, int len)
+int ambiguous_path(const char *path, int len)
{
int slash = 1;
int cnt;
@@ -241,7 +237,7 @@ static int ambiguous_path(const char *path, int len)
return slash;
}
-static inline int upstream_mark(const char *string, int len)
+inline int upstream_mark(const char *string, int len)
{
const char *suffix[] = { "@{upstream}", "@{u}" };
int i;
@@ -255,9 +251,7 @@ static inline int upstream_mark(const char *string, int len)
return 0;
}
-static int get_sha1_1(const char *name, int len, unsigned char *sha1);
-
-static int get_sha1_basic(const char *str, int len, unsigned char *sha1)
+int get_sha1_basic(const char *str, int len, unsigned char *sha1)
{
static const char *warn_msg = "refname '%.*s' is ambiguous.";
char *real_ref = NULL;
@@ -358,7 +352,7 @@ static int get_sha1_basic(const char *str, int len, unsigned char *sha1)
return 0;
}
-static int get_parent(const char *name, int len,
+int get_parent(const char *name, int len,
unsigned char *result, int idx)
{
unsigned char sha1[20];
@@ -388,7 +382,7 @@ static int get_parent(const char *name, int len,
return -1;
}
-static int get_nth_ancestor(const char *name, int len,
+int get_nth_ancestor(const char *name, int len,
unsigned char *result, int generation)
{
unsigned char sha1[20];
@@ -436,7 +430,7 @@ struct object *peel_to_type(const char *name, int namelen,
}
}
-static int peel_onion(const char *name, int len, unsigned char *sha1)
+int peel_onion(const char *name, int len, unsigned char *sha1)
{
unsigned char outer[20];
const char *sp;
@@ -522,7 +516,7 @@ static int peel_onion(const char *name, int len, unsigned char *sha1)
return 0;
}
-static int get_describe_name(const char *name, int len, unsigned char *sha1)
+int get_describe_name(const char *name, int len, unsigned char *sha1)
{
const char *cp;
@@ -542,7 +536,7 @@ static int get_describe_name(const char *name, int len, unsigned char *sha1)
return -1;
}
-static int get_sha1_1(const char *name, int len, unsigned char *sha1)
+int get_sha1_1(const char *name, int len, unsigned char *sha1)
{
int ret, has_suffix;
const char *cp;
@@ -590,17 +584,7 @@ static int get_sha1_1(const char *name, int len, unsigned char *sha1)
return get_short_sha1(name, len, sha1, 0);
}
-/*
- * This interprets names like ':/Initial revision of "git"' by searching
- * through history and returning the first commit whose message starts
- * the given regular expression.
- *
- * For future extension, ':/!' is reserved. If you want to match a message
- * beginning with a '!', you have to repeat the exclamation mark.
- */
-#define ONELINE_SEEN (1u<<20)
-
-static int handle_one_ref(const char *path,
+int handle_one_ref(const char *path,
const unsigned char *sha1, int flag, void *cb_data)
{
struct commit_list **list = cb_data;
@@ -618,7 +602,7 @@ static int handle_one_ref(const char *path,
return 0;
}
-static int get_sha1_oneline(const char *prefix, unsigned char *sha1,
+int get_sha1_oneline(const char *prefix, unsigned char *sha1,
struct commit_list *list)
{
struct commit_list *backup = NULL, *l;
@@ -675,12 +659,7 @@ static int get_sha1_oneline(const char *prefix, unsigned char *sha1,
return found ? 0 : -1;
}
-struct grab_nth_branch_switch_cbdata {
- long cnt, alloc;
- struct strbuf *buf;
-};
-
-static int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
+int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
const char *email, unsigned long timestamp, int tz,
const char *message, void *cb_data)
{
@@ -704,11 +683,7 @@ static int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
return 0;
}
-/*
- * Parse @{-N} syntax, return the number of characters parsed
- * if successful; otherwise signal an error with negative value.
- */
-static int interpret_nth_prior_checkout(const char *name, struct strbuf *buf)
+int interpret_nth_prior_checkout(const char *name, struct strbuf *buf)
{
long nth;
int i, retval;
@@ -794,27 +769,6 @@ int get_sha1_mb(const char *name, unsigned char *sha1)
return st;
}
-/*
- * This reads short-hand syntax that not only evaluates to a commit
- * object name, but also can act as if the end user spelled the name
- * of the branch from the command line.
- *
- * - "@{-N}" finds the name of the Nth previous branch we were on, and
- * places the name of the branch in the given buf and returns the
- * number of characters parsed if successful.
- *
- * - "<branch>@{upstream}" finds the name of the other ref that
- * <branch> is configured to merge with (missing <branch> defaults
- * to the current branch), and places the name of the branch in the
- * given buf and returns the number of characters parsed if
- * successful.
- *
- * If the input is not of the accepted format, it returns a negative
- * number to signal an error.
- *
- * If the input was ok but there are not N branch switches in the
- * reflog, it returns 0.
- */
int interpret_branch_name(const char *name, struct strbuf *buf)
{
char *cp;
@@ -898,18 +852,13 @@ int strbuf_check_branch_ref(struct strbuf *sb, const char *name)
return check_refname_format(sb->buf, 0);
}
-/*
- * This is like "get_sha1_basic()", except it allows "sha1 expressions",
- * notably "xyz^" for "parent of xyz"
- */
int get_sha1(const char *name, unsigned char *sha1)
{
struct object_context unused;
return get_sha1_with_context(name, sha1, &unused);
}
-/* Must be called only when object_name:filename doesn't exist. */
-static void diagnose_invalid_sha1_path(const char *prefix,
+void diagnose_invalid_sha1_path(const char *prefix,
const char *filename,
const unsigned char *tree_sha1,
const char *object_name)
@@ -946,8 +895,7 @@ static void diagnose_invalid_sha1_path(const char *prefix,
}
}
-/* Must be called only when :stage:filename doesn't exist. */
-static void diagnose_invalid_index_path(int stage,
+void diagnose_invalid_index_path(int stage,
const char *prefix,
const char *filename)
{
@@ -1003,7 +951,6 @@ static void diagnose_invalid_index_path(int stage,
free(fullname);
}
-
int get_sha1_with_mode_1(const char *name, unsigned char *sha1, unsigned *mode,
int only_to_die, const char *prefix)
{
@@ -1014,7 +961,7 @@ int get_sha1_with_mode_1(const char *name, unsigned char *sha1, unsigned *mode,
return ret;
}
-static char *resolve_relative_path(const char *rel)
+char *resolve_relative_path(const char *rel)
{
if (prefixcmp(rel, "./") && prefixcmp(rel, "../"))
return NULL;
diff --git a/sha1_name.h b/sha1_name.h
new file mode 100644
index 0000000..bdbcecd
--- /dev/null
+++ b/sha1_name.h
@@ -0,0 +1,130 @@
+#ifndef SHA1_NAME_H
+#define SHA1_NAME_H
+
+#include "commit.h"
+
+int find_short_object_filename(int len, const char *name, unsigned char *sha1);
+
+int match_sha(unsigned len, const unsigned char *a, const unsigned char *b);
+
+int find_short_packed_object(int len, const unsigned char *match, unsigned char *sha1);
+
+#define SHORT_NAME_NOT_FOUND (-1)
+#define SHORT_NAME_AMBIGUOUS (-2)
+
+int find_unique_short_object(int len, char *canonical,
+ unsigned char *res, unsigned char *sha1);
+
+int get_short_sha1(const char *name, int len, unsigned char *sha1,
+ int quietly);
+
+const char *find_unique_abbrev(const unsigned char *sha1, int len);
+
+int ambiguous_path(const char *path, int len);
+
+inline int upstream_mark(const char *string, int len);
+
+int get_sha1_basic(const char *str, int len, unsigned char *sha1);
+
+int get_parent(const char *name, int len,
+ unsigned char *result, int idx);
+
+int get_nth_ancestor(const char *name, int len,
+ unsigned char *result, int generation);
+
+struct object *peel_to_type(const char *name, int namelen,
+ struct object *o, enum object_type expected_type);
+
+int peel_onion(const char *name, int len, unsigned char *sha1);
+
+int get_describe_name(const char *name, int len, unsigned char *sha1);
+
+int get_sha1_1(const char *name, int len, unsigned char *sha1);
+
+/*
+ * This interprets names like ':/Initial revision of "git"' by searching
+ * through history and returning the first commit whose message starts
+ * the given regular expression.
+ *
+ * For future extension, ':/!' is reserved. If you want to match a message
+ * beginning with a '!', you have to repeat the exclamation mark.
+ */
+#define ONELINE_SEEN (1u<<20)
+
+int handle_one_ref(const char *path,
+ const unsigned char *sha1, int flag, void *cb_data);
+
+int get_sha1_oneline(const char *prefix, unsigned char *sha1,
+ struct commit_list *list);
+
+struct grab_nth_branch_switch_cbdata {
+ long cnt, alloc;
+ struct strbuf *buf;
+};
+
+int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
+ const char *email, unsigned long timestamp, int tz,
+ const char *message, void *cb_data);
+
+/*
+ * Parse @{-N} syntax, return the number of characters parsed
+ * if successful; otherwise signal an error with negative value.
+ */
+int interpret_nth_prior_checkout(const char *name, struct strbuf *buf);
+
+int get_sha1_mb(const char *name, unsigned char *sha1);
+
+/*
+ * This reads short-hand syntax that not only evaluates to a commit
+ * object name, but also can act as if the end user spelled the name
+ * of the branch from the command line.
+ *
+ * - "@{-N}" finds the name of the Nth previous branch we were on, and
+ * places the name of the branch in the given buf and returns the
+ * number of characters parsed if successful.
+ *
+ * - "<branch>@{upstream}" finds the name of the other ref that
+ * <branch> is configured to merge with (missing <branch> defaults
+ * to the current branch), and places the name of the branch in the
+ * given buf and returns the number of characters parsed if
+ * successful.
+ *
+ * If the input is not of the accepted format, it returns a negative
+ * number to signal an error.
+ *
+ * If the input was ok but there are not N branch switches in the
+ * reflog, it returns 0.
+ */
+int interpret_branch_name(const char *name, struct strbuf *buf);
+
+int strbuf_branchname(struct strbuf *sb, const char *name);
+
+int strbuf_check_branch_ref(struct strbuf *sb, const char *name);
+
+/*
+ * This is like "get_sha1_basic()", except it allows "sha1 expressions",
+ * notably "xyz^" for "parent of xyz"
+ */
+int get_sha1(const char *name, unsigned char *sha1);
+
+/* Must be called only when object_name:filename doesn't exist. */
+void diagnose_invalid_sha1_path(const char *prefix,
+ const char *filename,
+ const unsigned char *tree_sha1,
+ const char *object_name);
+
+/* Must be called only when :stage:filename doesn't exist. */
+void diagnose_invalid_index_path(int stage,
+ const char *prefix,
+ const char *filename);
+
+int get_sha1_with_mode_1(const char *name, unsigned char *sha1, unsigned *mode,
+ int only_to_die, const char *prefix);
+
+char *resolve_relative_path(const char *rel);
+
+int get_sha1_with_context_1(const char *name, unsigned char *sha1,
+ struct object_context *oc,
+ int only_to_die, const char *prefix);
+
+#endif
diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh
index 72934a5..fec448b 100755
--- a/t/t3404-rebase-interactive.sh
+++ b/t/t3404-rebase-interactive.sh
@@ -782,4 +782,69 @@ test_expect_success 'warn before rewriting published history' '
)
'
+test_expect_success 'warn before rewriting published history: one user' '
+ test_when_finished "rm -rf git.git git" &&
+ git init git.git &&
+ git clone git &&
+ (
+ cd git &&
+ git config rebase.checkremoterefs true &&
+ test_commit one_commit main.txt one_commit &&
+ git push --all
+ set_copy_editor &&
+ TODO_DUMP=actual EDITOR=./editor.sh git commit --amend &&
+ tmp=$(cat actual | sed -n 1p) &&
+ echo "$tmp" >actual &&
+ echo "# The commit to reword is already published." >expected &&
+ test_cmp expected actual
+ )
+'
+
+test_expect_success 'warn before rewriting published history: several users' '
+ test_when_finished "rm -rf git.git git1 git2" &&
+ git init --bare --share git.git &&
+ git clone git.git git1 &&
+ git clone git.git git2 &&
+ (
+ cd git1 &&
+ test_commit one_commit main.txt one_commit &&
+ git push --all
+ ) &&
+ (
+ cd git2 &&
+ git pull &&
+ test_commit two_commit main.txt two_commit &&
+ git push --all
+ ) &&
+ (
+ cd git1 &&
+ git config rebase.checkremoterefs true &&
+ set_copy_editor &&
+ TODO_DUMP=actual EDITOR=./editor.sh git commit --amend &&
+ tmp=$(cat actual | sed -n 1p) &&
+ echo "$tmp" >actual &&
+ echo "# The commit to reword is already published." >expected &&
+ test_cmp expected actual
+ )
+'
+
+test_expect_success 'warn before rewriting published history: shared branch' '
+ test_when_finished "rm -rf git.git git" &&
+ git init git.git &&
+ git clone git &&
+ (
+ cd git &&
+ git config rebase.checkremoterefs true &&
+ git checkout -b sharedbranch &&
+ test_commit one_commit main.txt one_commit &&
+ git push --set-upstream origin sharedbranch &&
+ set_copy_editor &&
+ TODO_DUMP=actual EDITOR=./editor.sh git commit --amend &&
+ tmp=$(cat actual | sed -n 1p) &&
+ echo "$tmp" >actual &&
+ echo "# The commit to reword is already published." >expected &&
+ test_cmp expected actual
+ )
+'
+
test_done
--
1.7.8
next prev parent reply other threads:[~2012-06-11 21:56 UTC|newest]
Thread overview: 25+ messages / expand[flat|nested] mbox.gz Atom feed top
2012-06-07 21:20 [PATCH] Warnings before rebasing -i published history Lucien Kong
2012-06-07 22:04 ` Matthieu Moy
2012-06-08 14:03 ` konglu
2012-06-08 14:25 ` Matthieu Moy
2012-06-08 14:57 ` Junio C Hamano
2012-06-07 22:49 ` Junio C Hamano
2012-06-08 7:32 ` konglu
2012-06-08 8:52 ` Matthieu Moy
2012-06-08 9:18 ` Tomas Carnecky
2012-06-08 9:23 ` Matthieu Moy
2012-06-08 14:55 ` Junio C Hamano
2012-06-11 10:04 ` [PATCHv2] " Lucien Kong
2012-06-11 10:55 ` Matthieu Moy
2012-06-11 11:36 ` konglu
2012-06-11 11:39 ` Matthieu Moy
2012-06-11 11:46 ` branch --contains is unbearably slow [Re: [PATCHv2] Warnings before rebasing -i published history] Thomas Rast
2012-06-11 16:16 ` Junio C Hamano
2012-06-11 22:04 ` Thomas Rast
2012-06-11 22:08 ` Thomas Rast
2012-06-11 23:04 ` Junio C Hamano
2012-06-11 21:56 ` [PATCHv3 1/2] Warnings before rebasing -i published history Lucien Kong
2012-06-11 21:56 ` Lucien Kong [this message]
2012-06-12 7:34 ` [PATCHv3 2/2] Warnings before amending " Matthieu Moy
2012-06-12 15:22 ` Junio C Hamano
2012-06-12 7:45 ` [PATCHv3 1/2] Warnings before rebasing -i " Nguy Thomas
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=1339451781-29324-2-git-send-email-Lucien.Kong@ensimag.imag.fr \
--to=lucien.kong@ensimag.imag.fr \
--cc=Franck.Jonas@ensimag.imag.fr \
--cc=Huynh-Khoi-Nguyen.Nguyen@ensimag.imag.fr \
--cc=Matthieu.Moy@grenoble-inp.fr \
--cc=Thomas.Nguy@ensimag.imag.fr \
--cc=Valentin.Duperray@ensimag.imag.fr \
--cc=git@vger.kernel.org \
/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).