* [PATCH] checkout: add --autostash option for branch switching
From: Harald Nordgren @ 2026-04-28 15:16 UTC (permalink / raw)
To: phillip.wood123
Cc: chris.torek, git, gitgitgadget, haraldnordgren, peff,
phillip.wood
In-Reply-To: <6d60573f-a02d-4aea-b891-6dd52e2d7048@gmail.com>
> This uses the existing label which is sensible, but I wonder if "Stash
> HEAD" would be a better choice as the merge base is always HEAD commit
> that the stash is based on.
>
> We can always change that later
Yeah, seems better to do later.
Harald
^ permalink raw reply
* [PATCH] checkout: add --autostash option for branch switching
From: Harald Nordgren @ 2026-04-28 15:21 UTC (permalink / raw)
To: phillip.wood123
Cc: chris.torek, git, gitgitgadget, haraldnordgren, peff,
phillip.wood
In-Reply-To: <66e3b1f0-e00d-4b9b-8ee7-ca71444cf56d@gmail.com>
> It may well be that fixing all that turns out to be a lot of work as it
> would mean modifying do_create_stash() to allow the branch name to be
> overridden and modifying store_stash() to use the commit subject as the
> reflog message in which case we should leave that for a future series.
I suspect that it is a lot of work, so maybe also better to do later.
Harald
^ permalink raw reply
* [PATCH 0/6] Handle cloning of objects larger than 4GB on Windows
From: Johannes Schindelin via GitGitGadget @ 2026-04-28 16:26 UTC (permalink / raw)
To: git; +Cc: Johannes Schindelin
On Windows, unsigned long is 32-bit even on 64-bit systems. This causes
multiple problems when Git handles objects larger than 4GB. This patch
series is a very targeted fix for a very early part of the problem: it
addresses the most fundamental truncation points that prevent a >4GB object
from surviving a clone at all.
Specifically, this fixes:
* zlib's uLong wrapping and triggering BUG() assertions in the git_zstream
wrapper
* Object sizes being truncated in pack streaming, delta headers, and
index-pack/unpack-objects
* pack-objects re-encoding reused pack entries with a truncated size,
producing corrupt packs on the wire
Many other code paths still use unsigned long for object sizes (e.g.,
cat-file -s, object_info.sizep, the delta machinery) and will need their own
conversions. This series does not attempt to fix those.
Based on work by @LordKiRon in git-for-windows/git#6076.
The last two commits add a test helper that synthesizes a pack with a >4GB
blob and regression tests that clone it via both the unpack-objects and
index-pack code paths using file:// transport.
Johannes Schindelin (6):
index-pack, unpack-objects: use size_t for object size
git-zlib: handle data streams larger than 4GB
odb, packfile: use size_t for streaming object sizes
delta, packfile: use size_t for delta header sizes
test-tool: add a helper to synthesize large packfiles
t5608: add regression test for >4GB object clone
Makefile | 1 +
builtin/index-pack.c | 9 +-
builtin/pack-objects.c | 23 +++-
builtin/unpack-objects.c | 5 +-
compat/zlib-compat.h | 2 +
delta.h | 14 +-
git-zlib.c | 25 ++--
git-zlib.h | 4 +-
object-file.c | 12 +-
odb/streaming.c | 13 +-
odb/streaming.h | 2 +-
oss-fuzz/fuzz-pack-headers.c | 2 +-
pack-bitmap.c | 2 +-
pack-check.c | 6 +-
packfile.c | 57 +++++---
packfile.h | 4 +-
t/helper/meson.build | 1 +
t/helper/test-synthesize.c | 250 +++++++++++++++++++++++++++++++++++
t/helper/test-tool.c | 1 +
t/helper/test-tool.h | 1 +
t/t5608-clone-2gb.sh | 37 ++++++
21 files changed, 418 insertions(+), 53 deletions(-)
create mode 100644 t/helper/test-synthesize.c
base-commit: 94f057755b7941b321fd11fec1b2e3ca5313a4e0
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2102%2Fdscho%2Ffix-large-clones-on-windows-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2102/dscho/fix-large-clones-on-windows-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/2102
--
gitgitgadget
^ permalink raw reply
* [PATCH 1/6] index-pack, unpack-objects: use size_t for object size
From: Johannes Schindelin via GitGitGadget @ 2026-04-28 16:26 UTC (permalink / raw)
To: git; +Cc: Johannes Schindelin, Johannes Schindelin
In-Reply-To: <pull.2102.git.1777393580.gitgitgadget@gmail.com>
From: Johannes Schindelin <johannes.schindelin@gmx.de>
When unpacking objects from a packfile, the object size is decoded
from a variable-length encoding. On platforms where unsigned long is
32-bit (such as Windows, even in 64-bit builds), the shift operation
overflows when decoding sizes larger than 4GB. The result is a
truncated size value, causing the unpacked object to be corrupted or
rejected.
Fix this by changing the size variable to size_t, which is 64-bit on
64-bit platforms, and ensuring the shift arithmetic occurs in 64-bit
space.
This was originally authored by LordKiRon <https://github.com/LordKiRon>,
who preferred not to reveal their real name and therefore agreed that I
take over authorship.
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
builtin/index-pack.c | 9 +++++----
builtin/unpack-objects.c | 5 +++--
2 files changed, 8 insertions(+), 6 deletions(-)
diff --git a/builtin/index-pack.c b/builtin/index-pack.c
index ca7784dc2c..cc660582e9 100644
--- a/builtin/index-pack.c
+++ b/builtin/index-pack.c
@@ -37,7 +37,7 @@ static const char index_pack_usage[] =
struct object_entry {
struct pack_idx_entry idx;
- unsigned long size;
+ size_t size;
unsigned char hdr_size;
signed char type;
signed char real_type;
@@ -469,7 +469,7 @@ static int is_delta_type(enum object_type type)
return (type == OBJ_REF_DELTA || type == OBJ_OFS_DELTA);
}
-static void *unpack_entry_data(off_t offset, unsigned long size,
+static void *unpack_entry_data(off_t offset, size_t size,
enum object_type type, struct object_id *oid)
{
static char fixed_buf[8192];
@@ -524,7 +524,8 @@ static void *unpack_raw_entry(struct object_entry *obj,
struct object_id *oid)
{
unsigned char *p;
- unsigned long size, c;
+ size_t size;
+ unsigned long c;
off_t base_offset;
unsigned shift;
void *data;
@@ -542,7 +543,7 @@ static void *unpack_raw_entry(struct object_entry *obj,
p = fill(1);
c = *p;
use(1);
- size += (c & 0x7f) << shift;
+ size += ((size_t)c & 0x7f) << shift;
shift += 7;
}
obj->size = size;
diff --git a/builtin/unpack-objects.c b/builtin/unpack-objects.c
index e01cf6e360..59a36c2481 100644
--- a/builtin/unpack-objects.c
+++ b/builtin/unpack-objects.c
@@ -533,7 +533,8 @@ static void unpack_one(unsigned nr)
{
unsigned shift;
unsigned char *pack;
- unsigned long size, c;
+ size_t size;
+ unsigned long c;
enum object_type type;
obj_list[nr].offset = consumed_bytes;
@@ -548,7 +549,7 @@ static void unpack_one(unsigned nr)
pack = fill(1);
c = *pack;
use(1);
- size += (c & 0x7f) << shift;
+ size += ((size_t)c & 0x7f) << shift;
shift += 7;
}
--
gitgitgadget
^ permalink raw reply related
* [PATCH 2/6] git-zlib: handle data streams larger than 4GB
From: Johannes Schindelin via GitGitGadget @ 2026-04-28 16:26 UTC (permalink / raw)
To: git; +Cc: Johannes Schindelin, Johannes Schindelin
In-Reply-To: <pull.2102.git.1777393580.gitgitgadget@gmail.com>
From: Johannes Schindelin <johannes.schindelin@gmx.de>
On Windows, zlib's `uLong` type is 32-bit even on 64-bit systems. When
processing data streams larger than 4GB, the `total_in` and `total_out`
fields in zlib's `z_stream` structure wrap around, which caused the
sanity checks in `zlib_post_call()` to trigger `BUG()` assertions.
The git_zstream wrapper now tracks its own 64-bit totals rather than
copying them from zlib. The sanity checks compare only the low bits,
using `maximum_unsigned_value_of_type(uLong)` to mask appropriately for
the platform's `uLong` size.
This is based on work by LordKiRon in git-for-windows#6076.
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
git-zlib.c | 25 +++++++++++++++++--------
git-zlib.h | 4 ++--
object-file.c | 2 +-
3 files changed, 20 insertions(+), 11 deletions(-)
diff --git a/git-zlib.c b/git-zlib.c
index df9604910e..b91cb323ae 100644
--- a/git-zlib.c
+++ b/git-zlib.c
@@ -30,6 +30,9 @@ static const char *zerr_to_string(int status)
*/
/* #define ZLIB_BUF_MAX ((uInt)-1) */
#define ZLIB_BUF_MAX ((uInt) 1024 * 1024 * 1024) /* 1GB */
+
+/* uLong is 32-bit on Windows, even on 64-bit systems */
+#define ULONG_MAX_VALUE maximum_unsigned_value_of_type(uLong)
static inline uInt zlib_buf_cap(unsigned long len)
{
return (ZLIB_BUF_MAX < len) ? ZLIB_BUF_MAX : len;
@@ -39,31 +42,37 @@ static void zlib_pre_call(git_zstream *s)
{
s->z.next_in = s->next_in;
s->z.next_out = s->next_out;
- s->z.total_in = s->total_in;
- s->z.total_out = s->total_out;
+ s->z.total_in = (uLong)(s->total_in & ULONG_MAX_VALUE);
+ s->z.total_out = (uLong)(s->total_out & ULONG_MAX_VALUE);
s->z.avail_in = zlib_buf_cap(s->avail_in);
s->z.avail_out = zlib_buf_cap(s->avail_out);
}
static void zlib_post_call(git_zstream *s, int status)
{
- unsigned long bytes_consumed;
- unsigned long bytes_produced;
+ size_t bytes_consumed;
+ size_t bytes_produced;
bytes_consumed = s->z.next_in - s->next_in;
bytes_produced = s->z.next_out - s->next_out;
- if (s->z.total_out != s->total_out + bytes_produced)
+ /*
+ * zlib's total_out/total_in are uLong which may wrap for >4GB.
+ * We track our own totals and verify only the low bits match.
+ */
+ if ((s->z.total_out & ULONG_MAX_VALUE) !=
+ ((s->total_out + bytes_produced) & ULONG_MAX_VALUE))
BUG("total_out mismatch");
/*
* zlib does not update total_in when it returns Z_NEED_DICT,
* causing a mismatch here. Skip the sanity check in that case.
*/
if (status != Z_NEED_DICT &&
- s->z.total_in != s->total_in + bytes_consumed)
+ (s->z.total_in & ULONG_MAX_VALUE) !=
+ ((s->total_in + bytes_consumed) & ULONG_MAX_VALUE))
BUG("total_in mismatch");
- s->total_out = s->z.total_out;
- s->total_in = s->z.total_in;
+ s->total_out += bytes_produced;
+ s->total_in += bytes_consumed;
/* zlib-ng marks `next_in` as `const`, so we have to cast it away. */
s->next_in = (unsigned char *) s->z.next_in;
s->next_out = s->z.next_out;
diff --git a/git-zlib.h b/git-zlib.h
index 0e66fefa8c..44380e8ad3 100644
--- a/git-zlib.h
+++ b/git-zlib.h
@@ -7,8 +7,8 @@ typedef struct git_zstream {
struct z_stream_s z;
unsigned long avail_in;
unsigned long avail_out;
- unsigned long total_in;
- unsigned long total_out;
+ size_t total_in;
+ size_t total_out;
unsigned char *next_in;
unsigned char *next_out;
} git_zstream;
diff --git a/object-file.c b/object-file.c
index 2acc9522df..086b2b65ff 100644
--- a/object-file.c
+++ b/object-file.c
@@ -1118,7 +1118,7 @@ int odb_source_loose_write_stream(struct odb_source *source,
} while (ret == Z_OK || ret == Z_BUF_ERROR);
if (stream.total_in != len + hdrlen)
- die(_("write stream object %ld != %"PRIuMAX), stream.total_in,
+ die(_("write stream object %"PRIuMAX" != %"PRIuMAX), (uintmax_t)stream.total_in,
(uintmax_t)len + hdrlen);
/*
--
gitgitgadget
^ permalink raw reply related
* [PATCH 3/6] odb, packfile: use size_t for streaming object sizes
From: Johannes Schindelin via GitGitGadget @ 2026-04-28 16:26 UTC (permalink / raw)
To: git; +Cc: Johannes Schindelin, Johannes Schindelin
In-Reply-To: <pull.2102.git.1777393580.gitgitgadget@gmail.com>
From: Johannes Schindelin <johannes.schindelin@gmx.de>
The odb_read_stream structure uses unsigned long for the size field,
which is 32-bit on Windows even in 64-bit builds. When streaming
objects larger than 4GB, the size would be truncated to zero or an
incorrect value, resulting in empty files being written to disk.
Change the size field in odb_read_stream to size_t and introduce
unpack_object_header_sz() to return sizes via size_t pointer. Since
object_info.sizep remains unsigned long for API compatibility, use
temporary variables where the types differ, with comments noting the
truncation limitation for code paths that still use unsigned long.
This was originally authored by LordKiRon <https://github.com/LordKiRon>,
who preferred not to reveal their real name and therefore agreed that I
take over authorship.
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
builtin/pack-objects.c | 23 ++++++++++++++++-------
object-file.c | 10 +++++++++-
odb/streaming.c | 13 ++++++++++++-
odb/streaming.h | 2 +-
| 2 +-
pack-bitmap.c | 2 +-
pack-check.c | 6 ++++--
packfile.c | 24 +++++++++++++++---------
packfile.h | 4 ++--
9 files changed, 61 insertions(+), 25 deletions(-)
diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c
index dd2480a73d..aa4b1cb9b8 100644
--- a/builtin/pack-objects.c
+++ b/builtin/pack-objects.c
@@ -629,14 +629,21 @@ static off_t write_reuse_object(struct hashfile *f, struct object_entry *entry,
struct packed_git *p = IN_PACK(entry);
struct pack_window *w_curs = NULL;
uint32_t pos;
- off_t offset;
+ off_t offset, cur;
enum object_type type = oe_type(entry);
+ enum object_type in_pack_type;
off_t datalen;
unsigned char header[MAX_PACK_OBJECT_HEADER],
dheader[MAX_PACK_OBJECT_HEADER];
unsigned hdrlen;
const unsigned hashsz = the_hash_algo->rawsz;
- unsigned long entry_size = SIZE(entry);
+ size_t entry_size;
+
+ cur = entry->in_pack_offset;
+ in_pack_type = unpack_object_header(p, &w_curs, &cur, &entry_size);
+ if (in_pack_type < 0)
+ die(_("write_reuse_object: unable to parse object header of %s"),
+ oid_to_hex(&entry->idx.oid));
if (DELTA(entry))
type = (allow_ofs_delta && DELTA(entry)->idx.offset) ?
@@ -1087,7 +1094,7 @@ static void write_reused_pack_one(struct packed_git *reuse_packfile,
{
off_t offset, next, cur;
enum object_type type;
- unsigned long size;
+ size_t size;
offset = pack_pos_to_offset(reuse_packfile, pos);
next = pack_pos_to_offset(reuse_packfile, pos + 1);
@@ -2243,7 +2250,7 @@ static void check_object(struct object_entry *entry, uint32_t object_index)
off_t ofs;
unsigned char *buf, c;
enum object_type type;
- unsigned long in_pack_size;
+ size_t in_pack_size;
buf = use_pack(p, &w_curs, entry->in_pack_offset, &avail);
@@ -2734,16 +2741,18 @@ unsigned long oe_get_size_slow(struct packing_data *pack,
struct pack_window *w_curs;
unsigned char *buf;
enum object_type type;
- unsigned long used, avail, size;
+ unsigned long used, avail;
+ size_t size;
if (e->type_ != OBJ_OFS_DELTA && e->type_ != OBJ_REF_DELTA) {
+ unsigned long sz;
packing_data_lock(&to_pack);
if (odb_read_object_info(the_repository->objects,
- &e->idx.oid, &size) < 0)
+ &e->idx.oid, &sz) < 0)
die(_("unable to get size of %s"),
oid_to_hex(&e->idx.oid));
packing_data_unlock(&to_pack);
- return size;
+ return sz;
}
p = oe_in_pack(pack, e);
diff --git a/object-file.c b/object-file.c
index 086b2b65ff..0be2981c7a 100644
--- a/object-file.c
+++ b/object-file.c
@@ -2326,6 +2326,7 @@ int odb_source_loose_read_object_stream(struct odb_read_stream **out,
struct object_info oi = OBJECT_INFO_INIT;
struct odb_loose_read_stream *st;
unsigned long mapsize;
+ unsigned long size_ul;
void *mapped;
mapped = odb_source_loose_map_object(source, oid, &mapsize);
@@ -2349,11 +2350,18 @@ int odb_source_loose_read_object_stream(struct odb_read_stream **out,
goto error;
}
- oi.sizep = &st->base.size;
+ /*
+ * object_info.sizep is unsigned long* (32-bit on Windows), but
+ * st->base.size is size_t (64-bit). Use temporary variable.
+ * Note: loose objects >4GB would still truncate here, but such
+ * large loose objects are uncommon (they'd normally be packed).
+ */
+ oi.sizep = &size_ul;
oi.typep = &st->base.type;
if (parse_loose_header(st->hdr, &oi) < 0 || st->base.type < 0)
goto error;
+ st->base.size = size_ul;
st->mapped = mapped;
st->mapsize = mapsize;
diff --git a/odb/streaming.c b/odb/streaming.c
index 5927a12954..af2adf5ce7 100644
--- a/odb/streaming.c
+++ b/odb/streaming.c
@@ -157,15 +157,26 @@ static int open_istream_incore(struct odb_read_stream **out,
.base.read = read_istream_incore,
};
struct odb_incore_read_stream *st;
+ unsigned long size_ul;
int ret;
oi.typep = &stream.base.type;
- oi.sizep = &stream.base.size;
+ /*
+ * object_info.sizep is unsigned long* (32-bit on Windows), but
+ * stream.base.size is size_t (64-bit). We use a temporary variable
+ * because the types are incompatible. Note: this path still truncates
+ * for >4GB objects, but large objects should use pack streaming
+ * (packfile_store_read_object_stream) which handles size_t properly.
+ * This incore fallback is only used for small objects or when pack
+ * streaming is unavailable.
+ */
+ oi.sizep = &size_ul;
oi.contentp = (void **)&stream.buf;
ret = odb_read_object_info_extended(odb, oid, &oi,
OBJECT_INFO_DIE_IF_CORRUPT);
if (ret)
return ret;
+ stream.base.size = size_ul;
CALLOC_ARRAY(st, 1);
*st = stream;
diff --git a/odb/streaming.h b/odb/streaming.h
index c7861f7e13..517e2ea2d3 100644
--- a/odb/streaming.h
+++ b/odb/streaming.h
@@ -21,7 +21,7 @@ struct odb_read_stream {
odb_read_stream_close_fn close;
odb_read_stream_read_fn read;
enum object_type type;
- unsigned long size; /* inflated size of full object */
+ size_t size; /* inflated size of full object */
};
/*
--git a/oss-fuzz/fuzz-pack-headers.c b/oss-fuzz/fuzz-pack-headers.c
index 150c0f5fa2..ef61ab577c 100644
--- a/oss-fuzz/fuzz-pack-headers.c
+++ b/oss-fuzz/fuzz-pack-headers.c
@@ -6,7 +6,7 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
{
enum object_type type;
- unsigned long len;
+ size_t len;
unpack_object_header_buffer((const unsigned char *)data,
(unsigned long)size, &type, &len);
diff --git a/pack-bitmap.c b/pack-bitmap.c
index f6ec18d83a..f9af8a96bd 100644
--- a/pack-bitmap.c
+++ b/pack-bitmap.c
@@ -2270,7 +2270,7 @@ static int try_partial_reuse(struct bitmap_index *bitmap_git,
{
off_t delta_obj_offset;
enum object_type type;
- unsigned long size;
+ size_t size;
if (pack_pos >= pack->p->num_objects)
return -1; /* not actually in the pack */
diff --git a/pack-check.c b/pack-check.c
index 79992bb509..2792f34d25 100644
--- a/pack-check.c
+++ b/pack-check.c
@@ -110,7 +110,7 @@ static int verify_packfile(struct repository *r,
void *data;
struct object_id oid;
enum object_type type;
- unsigned long size;
+ size_t size;
off_t curpos;
int data_valid;
@@ -143,7 +143,9 @@ static int verify_packfile(struct repository *r,
data = NULL;
data_valid = 0;
} else {
- data = unpack_entry(r, p, entries[i].offset, &type, &size);
+ unsigned long sz;
+ data = unpack_entry(r, p, entries[i].offset, &type, &sz);
+ size = sz;
data_valid = 1;
}
diff --git a/packfile.c b/packfile.c
index b012d648ad..fdae91dd11 100644
--- a/packfile.c
+++ b/packfile.c
@@ -1133,7 +1133,7 @@ out:
}
unsigned long unpack_object_header_buffer(const unsigned char *buf,
- unsigned long len, enum object_type *type, unsigned long *sizep)
+ unsigned long len, enum object_type *type, size_t *sizep)
{
unsigned shift;
size_t size, c;
@@ -1144,7 +1144,11 @@ unsigned long unpack_object_header_buffer(const unsigned char *buf,
size = c & 15;
shift = 4;
while (c & 0x80) {
- if (len <= used || (bitsizeof(long) - 7) < shift) {
+ /*
+ * Each continuation byte adds 7 bits. Ensure shift won't
+ * overflow size_t (use size_t not long for 64-bit on Windows).
+ */
+ if (len <= used || (bitsizeof(size_t) - 7) < shift) {
error("bad object header");
size = used = 0;
break;
@@ -1153,7 +1157,7 @@ unsigned long unpack_object_header_buffer(const unsigned char *buf,
size = st_add(size, st_left_shift(c & 0x7f, shift));
shift += 7;
}
- *sizep = cast_size_t_to_ulong(size);
+ *sizep = size;
return used;
}
@@ -1215,7 +1219,7 @@ unsigned long get_size_from_delta(struct packed_git *p,
int unpack_object_header(struct packed_git *p,
struct pack_window **w_curs,
off_t *curpos,
- unsigned long *sizep)
+ size_t *sizep)
{
unsigned char *base;
unsigned long left;
@@ -1367,7 +1371,7 @@ static enum object_type packed_to_object_type(struct repository *r,
while (type == OBJ_OFS_DELTA || type == OBJ_REF_DELTA) {
off_t base_offset;
- unsigned long size;
+ size_t size;
/* Push the object we're going to leave behind */
if (poi_stack_nr >= poi_stack_alloc && poi_stack == small_poi_stack) {
poi_stack_alloc = alloc_nr(poi_stack_nr);
@@ -1586,7 +1590,7 @@ static int packed_object_info_with_index_pos(struct packed_git *p, off_t obj_off
uint32_t *maybe_index_pos, struct object_info *oi)
{
struct pack_window *w_curs = NULL;
- unsigned long size;
+ size_t size;
off_t curpos = obj_offset;
enum object_type type = OBJ_NONE;
uint32_t pack_pos;
@@ -1778,7 +1782,7 @@ void *unpack_entry(struct repository *r, struct packed_git *p, off_t obj_offset,
struct pack_window *w_curs = NULL;
off_t curpos = obj_offset;
void *data = NULL;
- unsigned long size;
+ size_t size;
enum object_type type;
struct unpack_entry_stack_ent small_delta_stack[UNPACK_ENTRY_STACK_PREALLOC];
struct unpack_entry_stack_ent *delta_stack = small_delta_stack;
@@ -1943,8 +1947,10 @@ void *unpack_entry(struct repository *r, struct packed_git *p, off_t obj_offset,
(uintmax_t)curpos, p->pack_name);
data = NULL;
} else {
+ unsigned long sz;
data = patch_delta(base, base_size, delta_data,
- delta_size, &size);
+ delta_size, &sz);
+ size = sz;
/*
* We could not apply the delta; warn the user, but
@@ -2929,7 +2935,7 @@ int packfile_read_object_stream(struct odb_read_stream **out,
struct odb_packed_read_stream *stream;
struct pack_window *window = NULL;
enum object_type in_pack_type;
- unsigned long size;
+ size_t size;
in_pack_type = unpack_object_header(pack, &window, &offset, &size);
unuse_pack(&window);
diff --git a/packfile.h b/packfile.h
index 9b647da7dd..49d6bdecf6 100644
--- a/packfile.h
+++ b/packfile.h
@@ -456,9 +456,9 @@ off_t find_pack_entry_one(const struct object_id *oid, struct packed_git *);
int is_pack_valid(struct packed_git *);
void *unpack_entry(struct repository *r, struct packed_git *, off_t, enum object_type *, unsigned long *);
-unsigned long unpack_object_header_buffer(const unsigned char *buf, unsigned long len, enum object_type *type, unsigned long *sizep);
+unsigned long unpack_object_header_buffer(const unsigned char *buf, unsigned long len, enum object_type *type, size_t *sizep);
unsigned long get_size_from_delta(struct packed_git *, struct pack_window **, off_t);
-int unpack_object_header(struct packed_git *, struct pack_window **, off_t *, unsigned long *);
+int unpack_object_header(struct packed_git *, struct pack_window **, off_t *, size_t *);
off_t get_delta_base(struct packed_git *p, struct pack_window **w_curs,
off_t *curpos, enum object_type type,
off_t delta_obj_offset);
--
gitgitgadget
^ permalink raw reply related
* [PATCH 4/6] delta, packfile: use size_t for delta header sizes
From: Johannes Schindelin via GitGitGadget @ 2026-04-28 16:26 UTC (permalink / raw)
To: git; +Cc: Johannes Schindelin, Johannes Schindelin
In-Reply-To: <pull.2102.git.1777393580.gitgitgadget@gmail.com>
From: Johannes Schindelin <johannes.schindelin@gmx.de>
The delta header decoding functions return unsigned long, which
truncates on Windows for objects larger than 4GB. Introduce size_t
variants get_delta_hdr_size_sz() and get_size_from_delta_sz() that
preserve the full 64-bit size, and use them in packed_object_info()
where the size is needed for streaming decisions.
This was originally authored by LordKiRon <https://github.com/LordKiRon>,
who preferred not to reveal their real name and therefore agreed that I
take over authorship.
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
delta.h | 14 ++++++++++++--
packfile.c | 33 ++++++++++++++++++++++++---------
2 files changed, 36 insertions(+), 11 deletions(-)
diff --git a/delta.h b/delta.h
index 8a56ec0799..fad68cfc45 100644
--- a/delta.h
+++ b/delta.h
@@ -86,8 +86,11 @@ void *patch_delta(const void *src_buf, unsigned long src_size,
* This must be called twice on the delta data buffer, first to get the
* expected source buffer size, and again to get the target buffer size.
*/
-static inline unsigned long get_delta_hdr_size(const unsigned char **datap,
- const unsigned char *top)
+/*
+ * Size_t variant that doesn't truncate - use for >4GB objects on Windows.
+ */
+static inline size_t get_delta_hdr_size_sz(const unsigned char **datap,
+ const unsigned char *top)
{
const unsigned char *data = *datap;
size_t cmd, size = 0;
@@ -98,6 +101,13 @@ static inline unsigned long get_delta_hdr_size(const unsigned char **datap,
i += 7;
} while (cmd & 0x80 && data < top);
*datap = data;
+ return size;
+}
+
+static inline unsigned long get_delta_hdr_size(const unsigned char **datap,
+ const unsigned char *top)
+{
+ size_t size = get_delta_hdr_size_sz(datap, top);
return cast_size_t_to_ulong(size);
}
diff --git a/packfile.c b/packfile.c
index fdae91dd11..4208f53046 100644
--- a/packfile.c
+++ b/packfile.c
@@ -1161,9 +1161,12 @@ unsigned long unpack_object_header_buffer(const unsigned char *buf,
return used;
}
-unsigned long get_size_from_delta(struct packed_git *p,
- struct pack_window **w_curs,
- off_t curpos)
+/*
+ * Size_t variant for >4GB delta results on Windows.
+ */
+static size_t get_size_from_delta_sz(struct packed_git *p,
+ struct pack_window **w_curs,
+ off_t curpos)
{
const unsigned char *data;
unsigned char delta_head[20], *in;
@@ -1210,10 +1213,18 @@ unsigned long get_size_from_delta(struct packed_git *p,
data = delta_head;
/* ignore base size */
- get_delta_hdr_size(&data, delta_head+sizeof(delta_head));
+ get_delta_hdr_size_sz(&data, delta_head+sizeof(delta_head));
/* Read the result size */
- return get_delta_hdr_size(&data, delta_head+sizeof(delta_head));
+ return get_delta_hdr_size_sz(&data, delta_head+sizeof(delta_head));
+}
+
+unsigned long get_size_from_delta(struct packed_git *p,
+ struct pack_window **w_curs,
+ off_t curpos)
+{
+ size_t size = get_size_from_delta_sz(p, w_curs, curpos);
+ return cast_size_t_to_ulong(size);
}
int unpack_object_header(struct packed_git *p,
@@ -1618,14 +1629,18 @@ static int packed_object_info_with_index_pos(struct packed_git *p, off_t obj_off
ret = -1;
goto out;
}
- *oi->sizep = get_size_from_delta(p, &w_curs, tmp_pos);
- if (*oi->sizep == 0) {
+ /*
+ * Use size_t variant to avoid die() on >4GB deltas.
+ * oi->sizep is unsigned long, so truncation may occur,
+ * but streaming code uses its own size_t tracking.
+ */
+ size = get_size_from_delta_sz(p, &w_curs, tmp_pos);
+ if (size == 0) {
ret = -1;
goto out;
}
- } else {
- *oi->sizep = size;
}
+ *oi->sizep = (unsigned long)size;
}
if (oi->disk_sizep || (oi->mtimep && p->is_cruft)) {
--
gitgitgadget
^ permalink raw reply related
* [PATCH 5/6] test-tool: add a helper to synthesize large packfiles
From: Johannes Schindelin via GitGitGadget @ 2026-04-28 16:26 UTC (permalink / raw)
To: git; +Cc: Johannes Schindelin, Johannes Schindelin
In-Reply-To: <pull.2102.git.1777393580.gitgitgadget@gmail.com>
From: Johannes Schindelin <johannes.schindelin@gmx.de>
To test Git's behavior with very large pack files, we need a way to
generate such files quickly.
A naive approach using only readily-available Git commands would take
over 10 hours for a 4GB pack file, which is prohibitive.
Side-stepping Git's machinery and actual zlib compression by writing
uncompressed content with the appropriate zlib header makes things
much faster. The fastest method using this approach generates many
small, unreachable blob objects and takes about 1.5 minutes for 4GB.
However, this cannot be used because we need to test git clone, which
requires a reachable commit history.
Generating many reachable commits with small, uncompressed blobs takes
about 4 minutes for 4GB. But this approach 1) does not reproduce the
issues we want to fix (which require individual objects larger than
4GB) and 2) is comparatively slow because of the many SHA-1
calculations.
The approach taken here generates a single large blob (filled with NUL
bytes), along with the trees and commits needed to make it reachable.
This takes about 2.5 minutes for 4.5GB, which is the fastest option
that produces a valid, clonable repository with an object large enough
to trigger the bugs we want to test.
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
Makefile | 1 +
compat/zlib-compat.h | 2 +
t/helper/meson.build | 1 +
t/helper/test-synthesize.c | 250 +++++++++++++++++++++++++++++++++++++
t/helper/test-tool.c | 1 +
t/helper/test-tool.h | 1 +
6 files changed, 256 insertions(+)
create mode 100644 t/helper/test-synthesize.c
diff --git a/Makefile b/Makefile
index cedc234173..85405cb5b8 100644
--- a/Makefile
+++ b/Makefile
@@ -872,6 +872,7 @@ TEST_BUILTINS_OBJS += test-submodule-config.o
TEST_BUILTINS_OBJS += test-submodule-nested-repo-config.o
TEST_BUILTINS_OBJS += test-submodule.o
TEST_BUILTINS_OBJS += test-subprocess.o
+TEST_BUILTINS_OBJS += test-synthesize.o
TEST_BUILTINS_OBJS += test-trace2.o
TEST_BUILTINS_OBJS += test-truncate.o
TEST_BUILTINS_OBJS += test-userdiff.o
diff --git a/compat/zlib-compat.h b/compat/zlib-compat.h
index ac08276622..5078c5ef6c 100644
--- a/compat/zlib-compat.h
+++ b/compat/zlib-compat.h
@@ -7,6 +7,8 @@
# define z_stream_s zng_stream_s
# define gz_header_s zng_gz_header_s
+# define adler32(adler, buf, len) zng_adler32(adler, buf, len)
+
# define crc32(crc, buf, len) zng_crc32(crc, buf, len)
# define inflate(strm, bits) zng_inflate(strm, bits)
diff --git a/t/helper/meson.build b/t/helper/meson.build
index 675e64c010..3235f10ab8 100644
--- a/t/helper/meson.build
+++ b/t/helper/meson.build
@@ -69,6 +69,7 @@ test_tool_sources = [
'test-submodule-nested-repo-config.c',
'test-submodule.c',
'test-subprocess.c',
+ 'test-synthesize.c',
'test-tool.c',
'test-trace2.c',
'test-truncate.c',
diff --git a/t/helper/test-synthesize.c b/t/helper/test-synthesize.c
new file mode 100644
index 0000000000..3ce7078078
--- /dev/null
+++ b/t/helper/test-synthesize.c
@@ -0,0 +1,250 @@
+#define USE_THE_REPOSITORY_VARIABLE
+
+#include "test-tool.h"
+#include "git-compat-util.h"
+#include "git-zlib.h"
+#include "hash.h"
+#include "hex.h"
+#include "object-file.h"
+#include "object.h"
+#include "pack.h"
+#include "parse-options.h"
+#include "parse.h"
+#include "repository.h"
+#include "setup.h"
+#include "strbuf.h"
+#include "write-or-die.h"
+
+#define BLOCK_SIZE 0xffff
+static const unsigned char zeros[BLOCK_SIZE];
+
+/*
+ * Write data as an uncompressed zlib stream.
+ * For data larger than 64KB, writes multiple uncompressed blocks.
+ * If data is NULL, writes zeros.
+ * Updates the pack checksum context.
+ */
+static void write_uncompressed_zlib(FILE *f, struct git_hash_ctx *pack_ctx,
+ const void *data, size_t len,
+ const struct git_hash_algo *algo)
+{
+ unsigned char zlib_header[2] = { 0x78, 0x01 }; /* CMF, FLG */
+ unsigned char block_header[5];
+ const unsigned char *p = data;
+ size_t remaining = len;
+ uint32_t adler = 1L; /* adler32 initial value */
+ unsigned char adler_buf[4];
+
+ /* Write zlib header */
+ fwrite_or_die(f, zlib_header, sizeof(zlib_header));
+ algo->update_fn(pack_ctx, zlib_header, 2);
+
+ /* Write uncompressed blocks (max 64KB each) */
+ do {
+ size_t block_len = remaining > BLOCK_SIZE ? BLOCK_SIZE : remaining;
+ int is_final = (block_len == remaining);
+ const unsigned char *block_data = data ? p : zeros;
+
+ block_header[0] = is_final ? 0x01 : 0x00;
+ block_header[1] = block_len & 0xff;
+ block_header[2] = (block_len >> 8) & 0xff;
+ block_header[3] = block_header[1] ^ 0xff;
+ block_header[4] = block_header[2] ^ 0xff;
+
+ fwrite_or_die(f, block_header, sizeof(block_header));
+ algo->update_fn(pack_ctx, block_header, 5);
+
+ if (block_len) {
+ fwrite_or_die(f, block_data, block_len);
+ algo->update_fn(pack_ctx, block_data, block_len);
+ adler = adler32(adler, block_data, block_len);
+ }
+
+ if (data)
+ p += block_len;
+ remaining -= block_len;
+ } while (remaining > 0);
+
+ /* Write adler32 checksum */
+ put_be32(adler_buf, adler);
+ fwrite_or_die(f, adler_buf, sizeof(adler_buf));
+ algo->update_fn(pack_ctx, adler_buf, 4);
+}
+
+/*
+ * Write an uncompressed object to the pack file.
+ * If `data == NULL`, it is treated like a buffer to NUL bytes.
+ * Updates the pack checksum context.
+ */
+static void write_pack_object(FILE *f, struct git_hash_ctx *pack_ctx,
+ enum object_type type,
+ const void *data, size_t len,
+ struct object_id *oid,
+ const struct git_hash_algo *algo)
+{
+ unsigned char pack_header[MAX_PACK_OBJECT_HEADER];
+ char object_header[32];
+ int pack_header_len, object_header_len;
+ struct git_hash_ctx ctx;
+
+ /* Write pack object header */
+ pack_header_len = encode_in_pack_object_header(pack_header,
+ sizeof(pack_header),
+ type, len);
+ fwrite_or_die(f, pack_header, pack_header_len);
+ algo->update_fn(pack_ctx, pack_header, pack_header_len);
+
+ /* Write the data as uncompressed zlib */
+ write_uncompressed_zlib(f, pack_ctx, data, len, algo);
+
+ algo->init_fn(&ctx);
+ object_header_len = format_object_header(object_header,
+ sizeof(object_header),
+ type, len);
+ algo->update_fn(&ctx, object_header, object_header_len);
+ if (data)
+ algo->update_fn(&ctx, data, len);
+ else {
+ for (size_t i = len / BLOCK_SIZE; i; i--)
+ algo->update_fn(&ctx, zeros, BLOCK_SIZE);
+ algo->update_fn(&ctx, zeros, len % BLOCK_SIZE);
+ }
+ algo->final_oid_fn(oid, &ctx);
+}
+
+/*
+ * Generate a pack file with a single large (>4GB) reachable object.
+ *
+ * Creates:
+ * 1. A large blob (all NUL bytes)
+ * 2. A tree containing that blob as "file"
+ * 3. A commit using that tree
+ * 4. The empty tree
+ * 5. A child commit using the empty tree
+ *
+ * This is useful for testing that Git can handle objects larger than 4GB.
+ */
+static int generate_pack_with_large_object(const char *path, size_t blob_size,
+ const struct git_hash_algo *algo)
+{
+ FILE *f = xfopen(path, "wb");
+ struct git_hash_ctx pack_ctx;
+ unsigned char pack_hash[GIT_MAX_RAWSZ];
+ struct object_id blob_oid, tree_oid, commit_oid, empty_tree_oid, final_commit_oid;
+ struct strbuf buf = STRBUF_INIT;
+ const uint32_t object_count = 5;
+ struct pack_header pack_header = {
+ .hdr_signature = htonl(PACK_SIGNATURE),
+ .hdr_version = htonl(PACK_VERSION),
+ .hdr_entries = htonl(object_count),
+ };
+
+ algo->init_fn(&pack_ctx);
+
+ /* Write pack header */
+ fwrite_or_die(f, &pack_header, sizeof(pack_header));
+ algo->update_fn(&pack_ctx, &pack_header, sizeof(pack_header));
+
+ /* 1. Write the large blob */
+ write_pack_object(f, &pack_ctx, OBJ_BLOB, NULL, blob_size, &blob_oid, algo);
+
+ /* 2. Write tree containing the blob as "file" */
+ strbuf_addf(&buf, "100644 file%c", '\0');
+ strbuf_add(&buf, blob_oid.hash, algo->rawsz);
+ write_pack_object(f, &pack_ctx, OBJ_TREE, buf.buf, buf.len, &tree_oid, algo);
+
+ /* 3. Write commit using that tree */
+ strbuf_reset(&buf);
+ strbuf_addf(&buf,
+ "tree %s\n"
+ "author A U Thor <author@example.com> 1234567890 +0000\n"
+ "committer C O Mitter <committer@example.com> 1234567890 +0000\n"
+ "\n"
+ "Large blob commit\n",
+ oid_to_hex(&tree_oid));
+ write_pack_object(f, &pack_ctx, OBJ_COMMIT, buf.buf, buf.len, &commit_oid, algo);
+
+ /* 4. Write the empty tree */
+ write_pack_object(f, &pack_ctx, OBJ_TREE, "", 0, &empty_tree_oid, algo);
+
+ /* 5. Write final commit using empty tree, with previous commit as parent */
+ strbuf_reset(&buf);
+ strbuf_addf(&buf,
+ "tree %s\n"
+ "parent %s\n"
+ "author A U Thor <author@example.com> 1234567890 +0000\n"
+ "committer C O Mitter <committer@example.com> 1234567890 +0000\n"
+ "\n"
+ "Empty tree commit\n",
+ oid_to_hex(&empty_tree_oid),
+ oid_to_hex(&commit_oid));
+ write_pack_object(f, &pack_ctx, OBJ_COMMIT, buf.buf, buf.len, &final_commit_oid, algo);
+
+ /* Write pack trailer (checksum) */
+ algo->final_fn(pack_hash, &pack_ctx);
+ fwrite_or_die(f, pack_hash, algo->rawsz);
+ if (fclose(f))
+ die_errno(_("could not close '%s'"), path);
+
+ strbuf_release(&buf);
+
+ /* Print the final commit OID so caller can set up refs */
+ printf("%s\n", oid_to_hex(&final_commit_oid));
+
+ return 0;
+}
+
+static int cmd__synthesize__pack(int argc, const char **argv,
+ const char *prefix UNUSED,
+ struct repository *repo)
+{
+ int non_git;
+ int reachable_large = 0;
+ const struct git_hash_algo *algo;
+ size_t blob_size;
+ uintmax_t blob_size_u;
+ const char *path;
+ const char * const usage[] = {
+ "test-tool synthesize pack "
+ "--reachable-large <blob-size> <filename>",
+ NULL
+ };
+ struct option options[] = {
+ OPT_BOOL(0, "reachable-large", &reachable_large,
+ N_("write a pack with a single reachable large blob")),
+ OPT_END()
+ };
+
+ setup_git_directory_gently(&non_git);
+ repo = the_repository;
+ algo = repo->hash_algo;
+
+ argc = parse_options(argc, argv, NULL, options, usage,
+ PARSE_OPT_KEEP_ARGV0);
+ if (argc != 3 || !reachable_large)
+ usage_with_options(usage, options);
+
+ if (!git_parse_unsigned(argv[1], &blob_size_u,
+ maximum_unsigned_value_of_type(size_t)))
+ die(_("'%s' is not a valid blob size"), argv[1]);
+ blob_size = blob_size_u;
+ path = argv[2];
+
+ return !!generate_pack_with_large_object(path, blob_size, algo);
+}
+
+int cmd__synthesize(int argc, const char **argv)
+{
+ const char *prefix = NULL;
+ char const * const synthesize_usage[] = {
+ "test-tool synthesize pack <options>",
+ NULL,
+ };
+ parse_opt_subcommand_fn *fn = NULL;
+ struct option options[] = {
+ OPT_SUBCOMMAND("pack", &fn, cmd__synthesize__pack),
+ OPT_END()
+ };
+ argc = parse_options(argc, argv, prefix, options, synthesize_usage, 0);
+ return !!fn(argc, argv, prefix, NULL);
+}
diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
index a7abc618b3..b71a22b43b 100644
--- a/t/helper/test-tool.c
+++ b/t/helper/test-tool.c
@@ -82,6 +82,7 @@ static struct test_cmd cmds[] = {
{ "submodule-config", cmd__submodule_config },
{ "submodule-nested-repo-config", cmd__submodule_nested_repo_config },
{ "subprocess", cmd__subprocess },
+ { "synthesize", cmd__synthesize },
{ "trace2", cmd__trace2 },
{ "truncate", cmd__truncate },
{ "userdiff", cmd__userdiff },
diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h
index 7f150fa1eb..f2885b33d5 100644
--- a/t/helper/test-tool.h
+++ b/t/helper/test-tool.h
@@ -75,6 +75,7 @@ int cmd__submodule(int argc, const char **argv);
int cmd__submodule_config(int argc, const char **argv);
int cmd__submodule_nested_repo_config(int argc, const char **argv);
int cmd__subprocess(int argc, const char **argv);
+int cmd__synthesize(int argc, const char **argv);
int cmd__trace2(int argc, const char **argv);
int cmd__truncate(int argc, const char **argv);
int cmd__userdiff(int argc, const char **argv);
--
gitgitgadget
^ permalink raw reply related
* [PATCH 6/6] t5608: add regression test for >4GB object clone
From: Johannes Schindelin via GitGitGadget @ 2026-04-28 16:26 UTC (permalink / raw)
To: git; +Cc: Johannes Schindelin, Johannes Schindelin
In-Reply-To: <pull.2102.git.1777393580.gitgitgadget@gmail.com>
From: Johannes Schindelin <johannes.schindelin@gmx.de>
The shift overflow bug in index-pack and unpack-objects caused incorrect
object size calculation when the encoded size required more than 32 bits
of shift. This would result in corrupted or failed unpacking of objects
larger than 4GB.
Add a test that creates a pack file containing a 4GB+ blob using the
new 'test-tool synthesize pack --reachable-large' command, then clones
the repository to verify the fix works correctly.
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
t/t5608-clone-2gb.sh | 37 +++++++++++++++++++++++++++++++++++++
1 file changed, 37 insertions(+)
diff --git a/t/t5608-clone-2gb.sh b/t/t5608-clone-2gb.sh
index 87a8cd9f98..af93302dde 100755
--- a/t/t5608-clone-2gb.sh
+++ b/t/t5608-clone-2gb.sh
@@ -49,4 +49,41 @@ test_expect_success 'clone - with worktree, file:// protocol' '
'
+test_expect_success SIZE_T_IS_64BIT 'set up repo with >4GB object' '
+ large_blob_size=$((4*1024*1024*1024+1)) &&
+ git init --bare 4gb-repo &&
+ head_oid=$(test-tool synthesize pack \
+ --reachable-large "$large_blob_size" \
+ 4gb-repo/objects/pack/test.pack) &&
+ git -C 4gb-repo index-pack objects/pack/test.pack &&
+ git -C 4gb-repo update-ref refs/heads/main $head_oid &&
+ git -C 4gb-repo symbolic-ref HEAD refs/heads/main
+'
+
+test_expect_success SIZE_T_IS_64BIT 'clone >4GB object via unpack-objects' '
+ # The synthesized pack has five objects, so a large unpack limit keeps
+ # fetch-pack on the unpack-objects path.
+ git -c fetch.unpackLimit=100 clone --bare \
+ "file://$(pwd)/4gb-repo" 4gb-clone-unpack &&
+
+ # Verify the large blob survived the clone by comparing its OID
+ # between source and clone. We cannot use "cat-file -s" because
+ # object_info.sizep is still unsigned long, which truncates >4GB
+ # sizes on Windows. OID equality proves content integrity since
+ # the clone already verified checksums via index-pack/unpack-objects.
+ source_blob=$(git -C 4gb-repo rev-parse main^:file) &&
+ clone_blob=$(git -C 4gb-clone-unpack rev-parse main^:file) &&
+ test "$source_blob" = "$clone_blob"
+'
+
+test_expect_success SIZE_T_IS_64BIT 'clone with >4GB object via index-pack' '
+ # Force fetch-pack to hand the pack to index-pack instead.
+ git -c fetch.unpackLimit=1 clone --bare \
+ "file://$(pwd)/4gb-repo" 4gb-clone-index &&
+
+ source_blob=$(git -C 4gb-repo rev-parse main^:file) &&
+ clone_blob=$(git -C 4gb-clone-index rev-parse main^:file) &&
+ test "$source_blob" = "$clone_blob"
+'
+
test_done
--
gitgitgadget
^ permalink raw reply related
* Re: [PATCH] Reintegrate: send "Huh?" warnings to stderr, not stdout
From: Tian Yuchen @ 2026-04-28 18:05 UTC (permalink / raw)
To: git, gitster
In-Reply-To: <ae896PlyiYeqldFN@mbp>
On 4/27/26 18:47, Erik Cervin-Edin wrote:
> The "Huh?: $msg" warning in show_merge(), emitted when a first-parent
> merge subject does not match either "Merge branch '...'" or "Merge
> remote branch '...'", uses
>
> echo 2>&1 "Huh?: $msg"
Yes, this is clearly wrong.
> The "2>&1" redirect dupes stderr onto stdout's destination; it does
> not change where stdout itself points. Since echo writes to stdout,
> the "Huh?:" message lands on stdout regardless -- as would any
> command's normal output.
> The intent appears to have been ">&2", which dupes stdout onto stderr.
It would be better for me if this sentence (i.e., what was the code
originally *intended* to do before the patch?) were placed right at the
beginning. Something similar to:
In show_merge(), the warning "Huh?: $msg" is emitted to stdout because
it uses the erroneous redirect `echo 2>&1`. The intent was clearly to
use `>&2` to print to stderr...
Of course, it’s up to you ;)
>
> In the common Reintegrate invocation that captures stdout, e.g.
>
> Meta/Reintegrate next..seen >Meta/redo-seen.sh
>
> this means the warning is silently embedded in the generated heredoc
> body instead of being printed to the maintainer's terminal. The
> resulting redo-* script is corrupted with a "Huh?:..." line and the
> maintainer has no diagnostic that something went wrong.
>
> Every other diagnostic in this script already uses ">&2"; this line
> is the lone outlier.
>
> Use ">&2" so the warning reaches stderr as intended.
Overall, the reasoning is nice :-)
> Signed-off-by: Erik Cervin-Edin <erik@cervined.in>
> ---
> Reintegrate | 2 +-
> 1 file changed, 1 insertion(+), 1 deletion(-)
>
> diff --git a/Reintegrate b/Reintegrate
> index a1e67a0330..6fdc7c5f41 100755
> --- a/Reintegrate
> +++ b/Reintegrate
> @@ -327,7 +327,7 @@ show_merge () {
> merge_hier=
> ;;
> *)
> - echo 2>&1 "Huh?: $msg"
> + echo >&2 "Huh?: $msg"
> return
> ;;
> esac &&
Thanks, Yuchen
^ permalink raw reply
* [PATCH] checkout: add --autostash option for branch switching
From: Harald Nordgren @ 2026-04-28 18:08 UTC (permalink / raw)
To: phillip.wood123
Cc: chris.torek, git, gitgitgadget, haraldnordgren, peff,
phillip.wood
In-Reply-To: <88a89e06-5223-4a6f-8f9e-66e72b632ee2@gmail.com>
> This is looking good, there are just a few small issues. Hopefully the
> next iteration will be the last.
Thanks for the encouragement! 💪🏻
> s/would/will/
👍
> It is the changes in the files overlapping that causes the merge
> conflict, not the files overlapping
>
> When the `--merge` (`-m`) option is given and the local changes
> overlap with the changes in the branch we're switching to,
👍
> I'd drop this line and say instead "a message is printed"
👍
> This needs updating to match the new conflict advice.
👍
> If you've not done so already it would be well worth checking the
> generated git-checkout.html and the man page
Good catch, I generated it now and yes it didn't look correct. I dropped
that last section now.
> Don't we show the modified files as well now?
Good catch, very good idea to actually generate the man html file and
check.
> As this function only sets up the flags for unpack_trees() I think we
> could call this "quiet" or "show_errors"
Good point!
> We've added a function parameter for this option but then we ignore it
> unless "merge" and "old_commit" are true which is confusing. The reason
> we used to check those was to set "quiet" automatically but we can't do
> that now, so why not just use the value the call requested?
Good point! I attempted to change this, hopefully it doesn't break anything!
> This is an "out" parameter, so it would make sense to keep it at the end
> of the parameter list.
👍
> To create a multi-line file it is clearer to use
>
> cat >expect.messages <<-\EOF &&
> The following paths have local changes:
> M one
> EOF
👍
> I've realized since I suggested this that we should be checking the
> reflog message as well since that's what's shown by "git stash list" so
> we need to run
>
> git log -p -1 --format="%gs%n%B" -g --diff-merges=1 refs/stash >actual
>
> > + sed /^index/d actual >actual.trimmed &&
> > + cat >expect <<-EOF &&
>
> and add
>
>
> autostash while switching to ${SQ}side${SQ}
Make sense!
> Why the two calls to test_grep, rather than one? Anyway I've realized
> since I suggested this test that we also need to check the message only
> appears once to prevent a regression where merge_working_tree() calls
> unpack_trees() without setting "quiet" the first time it is called. We
> can do that by writing an expect file and calling test_cmp(), or by
> using "test_line_count = 1 err"
Excellent point. I went with test_cmp since it's multi-line output and
"test_line_count = 1" seemed to not work then.
Harald
^ permalink raw reply
* [PATCH v16 0/5] checkout: 'autostash' for branch switching
From: Harald Nordgren via GitGitGadget @ 2026-04-28 18:39 UTC (permalink / raw)
To: git; +Cc: Phillip Wood, Chris Torek, Jeff King, Harald Nordgren
In-Reply-To: <pull.2234.v15.git.git.1777065012.gitgitgadget@gmail.com>
* Updated the git checkout and git switch docs to show the actual output
when using -m to carry local changes across a branch switch, and changed
"would carry" to "will carry".
* Rewrote the merge-conflict example in both docs to match the new, more
concise message printed on autostash conflicts.
* Replaced the show_unpack_errors flag and old_commit parameter in
merge_working_tree()/init_topts() with a plain quiet boolean, so the
caller decides directly whether to suppress unpack errors.
* Tightened the 'checkout -m with dirty tree' test by replacing a printf
with a heredoc.
* Made the 'checkout -m creates a recoverable stash on conflict' test also
assert the reflog subject of the new stash entry.
* Replaced two test_grep calls in the 'checkout -m which would overwrite
untracked file' test with a single test_cmp, which also catches a
regression where the "would be overwritten" message could end up printed
twice.
Harald Nordgren (5):
stash: add --label-ours, --label-theirs, --label-base for apply
sequencer: allow create_autostash to run silently
sequencer: teach autostash apply to take optional conflict marker
labels
checkout: rollback lock on early returns in merge_working_tree
checkout -m: autostash when switching branches
Documentation/git-checkout.adoc | 55 +++++------
Documentation/git-stash.adoc | 11 ++-
Documentation/git-switch.adoc | 36 ++++---
builtin/checkout.c | 166 +++++++++++++++-----------------
builtin/commit.c | 3 +-
builtin/merge.c | 15 ++-
builtin/stash.c | 28 ++++--
sequencer.c | 69 +++++++++----
sequencer.h | 7 +-
t/t3420-rebase-autostash.sh | 16 +--
t/t3903-stash.sh | 24 +++++
t/t7201-co.sh | 71 +++++++++++++-
t/t7600-merge.sh | 3 +-
xdiff-interface.c | 12 +++
xdiff-interface.h | 1 +
xdiff/xmerge.c | 6 +-
16 files changed, 343 insertions(+), 180 deletions(-)
base-commit: 94f057755b7941b321fd11fec1b2e3ca5313a4e0
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2234%2FHaraldNordgren%2Fcheckout_autostash-v16
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2234/HaraldNordgren/checkout_autostash-v16
Pull-Request: https://github.com/git/git/pull/2234
Range-diff vs v15:
1: aba8e6a9dc = 1: aba8e6a9dc stash: add --label-ours, --label-theirs, --label-base for apply
2: 89e0bfa803 = 2: 89e0bfa803 sequencer: allow create_autostash to run silently
3: a428ce7328 = 3: a428ce7328 sequencer: teach autostash apply to take optional conflict marker labels
4: f358424085 = 4: f358424085 checkout: rollback lock on early returns in merge_working_tree
5: 96b14db827 ! 5: 07d25fda91 checkout -m: autostash when switching branches
@@ Documentation/git-checkout.adoc: $ git checkout mytopic
-You can give the `-m` flag to the command, which would try a
-three-way merge:
-+You can give the `-m` flag to the command, which would carry your local
++You can give the `-m` flag to the command, which will carry your local
+changes to the new branch:
------------
$ git checkout -m mytopic
-Auto-merging frotz
++Applied autostash.
+Switched to branch 'mytopic'
++The following paths have local changes:
++M frotz
------------
-After this three-way merge, the local modifications are _not_
@@ Documentation/git-checkout.adoc: $ git checkout mytopic
-When a merge conflict happens during switching branches with
-the `-m` option, you would see something like this:
-+When the `--merge` (`-m`) option is in effect and the locally
-+modified files overlap with files that need to be updated by the
-+branch switch, the changes are stashed and reapplied after the
-+switch. If this process results in conflicts, a stash entry is saved
-+and made available in `git stash list`:
++When the `--merge` (`-m`) option is given and the local changes
++overlap with the changes in the branch we're switching to, the
++changes are stashed and reapplied after the switch. If this
++process results in conflicts, the stash entry is saved and a
++message is printed:
------------
$ git checkout -m mytopic
@@ Documentation/git-checkout.adoc: $ git checkout mytopic
-ERROR: Merge conflict in frotz
-fatal: merge program failed
-------------
-+Your local changes are stashed, however, applying it to carry
-+forward your local changes resulted in conflicts:
-
+-
-At this point, `git diff` shows the changes cleanly merged as in
-the previous example, as well as the changes in the conflicted
-files. Edit and resolve the conflict and mark it resolved with
-`git add` as usual:
-+ - You can try resolving them now. If you resolved them
-+ successfully, discard the stash entry with "git stash drop".
-
-+ - Alternatively you can "git reset --hard" if you do not want
-+ to deal with them right now, and later "git stash pop" to
-+ recover your local changes.
- ------------
+-
+-------------
-$ edit frotz
-$ git add frotz
--------------
-+
-+You can try resolving the conflicts now. Edit the conflicting files
-+and mark them resolved with `git add` as usual, then run `git stash
-+drop` to discard the stash entry. Alternatively, you can clear the
-+working tree with `git reset --hard` and recover your local changes
-+later with `git stash pop`.
++Your local changes are stashed, however applying them
++resulted in conflicts. You can either resolve the conflicts
++and then discard the stash with "git stash drop", or, if you
++do not want to resolve them now, run "git reset --hard" and
++apply the local changes later by running "git stash pop".
+ ------------
CONFIGURATION
- -------------
## Documentation/git-switch.adoc ##
@@ Documentation/git-switch.adoc: variable.
@@ Documentation/git-switch.adoc: $ git switch mytopic
-You can give the `-m` flag to the command, which would try a three-way
-merge:
-+You can give the `-m` flag to the command, which would carry your local
++You can give the `-m` flag to the command, which will carry your local
+changes to the new branch:
------------
$ git switch -m mytopic
-Auto-merging frotz
++Applied autostash.
+Switched to branch 'mytopic'
++The following paths have local changes:
++M frotz
------------
-After this three-way merge, the local modifications are _not_
@@ builtin/checkout.c: struct checkout_opts {
char *name; /* The short name used */
char *path; /* The full name of a real branch */
@@ builtin/checkout.c: static void setup_branch_path(struct branch_info *branch)
+ branch->path = strbuf_detach(&buf, NULL);
+ }
- static void init_topts(struct unpack_trees_options *topts, int merge,
+-static void init_topts(struct unpack_trees_options *topts, int merge,
++static void init_topts(struct unpack_trees_options *topts,
int show_progress, int overwrite_ignore,
- struct commit *old_commit)
-+ struct commit *old_commit, bool show_unpack_errors)
++ bool quiet)
{
memset(topts, 0, sizeof(*topts));
topts->head_idx = -1;
@@ builtin/checkout.c: static void init_topts(struct unpack_trees_options *topts, i
topts->update = 1;
topts->merge = 1;
- topts->quiet = merge && old_commit;
-+ topts->quiet = merge && old_commit && !show_unpack_errors;
++ topts->quiet = quiet;
topts->verbose_update = show_progress;
topts->fn = twoway_merge;
topts->preserve_ignored = !overwrite_ignore;
@@ builtin/checkout.c: static void init_topts(struct unpack_trees_options *topts, i
static int merge_working_tree(const struct checkout_opts *opts,
struct branch_info *old_branch_info,
struct branch_info *new_branch_info,
-- int *writeout_error)
-+ int *writeout_error,
-+ bool show_unpack_errors)
++ bool quiet,
+ int *writeout_error)
{
int ret;
- struct lock_file lock_file = LOCK_INIT;
@@ builtin/checkout.c: static int merge_working_tree(const struct checkout_opts *opts,
+ }
/* 2-way merge to the new branch */
- init_topts(&topts, opts->merge, opts->show_progress,
+- init_topts(&topts, opts->merge, opts->show_progress,
- opts->overwrite_ignore, old_branch_info->commit);
-+ opts->overwrite_ignore, old_branch_info->commit,
-+ show_unpack_errors);
++ init_topts(&topts, opts->show_progress,
++ opts->overwrite_ignore, quiet);
init_checkout_metadata(&topts.meta, new_branch_info->refname,
new_branch_info->commit ?
&new_branch_info->commit->object.oid :
@@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts,
if (do_merge) {
- ret = merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error);
+ ret = merge_working_tree(opts, &old_branch_info, new_branch_info,
-+ &writeout_error, false);
++ opts->merge, &writeout_error);
+ if (ret == MERGE_WORKING_TREE_UNPACK_FAILED && opts->merge) {
+ strbuf_addf(&autostash_msg,
+ "autostash while switching to '%s'",
@@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts,
+ autostash_msg.buf, true);
+ created_autostash = 1;
+ ret = merge_working_tree(opts, &old_branch_info, new_branch_info,
-+ &writeout_error, true);
++ false, &writeout_error);
+ }
+ if (created_autostash) {
+ if (opts->conflict_style >= 0) {
@@ t/t7201-co.sh: test_expect_success 'checkout -m with dirty tree' '
test "$(git symbolic-ref HEAD)" = "refs/heads/side" &&
- printf "M\t%s\n" one >expect.messages &&
-+ printf "The following paths have local changes:\nM\t%s\n" one >expect.messages &&
++ cat >expect.messages <<-\EOF &&
++ The following paths have local changes:
++ M one
++ EOF
test_cmp expect.messages messages &&
fill "M one" "A three" "D two" >expect.main &&
@@ t/t7201-co.sh: test_expect_success 'checkout --merge --conflict=diff3 <branch>'
+ test_grep "git stash drop" actual &&
+ test_grep "git stash pop" actual &&
+ test_grep "The following paths have local changes" actual &&
-+ git show --format=%B --diff-merges=1 refs/stash >actual &&
++ git log -p -1 --format="%gs%n%B" -g --diff-merges=1 refs/stash >actual &&
+ sed /^index/d actual >actual.trimmed &&
+ cat >expect <<-EOF &&
++ autostash while switching to ${SQ}side${SQ}
+ On main: autostash while switching to ${SQ}side${SQ}
+
+ diff --git a/one b/one
@@ t/t7201-co.sh: test_expect_success 'checkout --merge --conflict=diff3 <branch>'
+ >another-file.t &&
+ fill 1 2 3 4 5 >one &&
+ test_must_fail git checkout -m @{-1} 2>err &&
-+ test_grep "would be overwritten by checkout" err &&
-+ test_grep "another-file.t" err
++ q_to_tab >expect <<-\EOF &&
++ error: The following untracked working tree files would be overwritten by checkout:
++ Qanother-file.t
++ Please move or remove them before you switch branches.
++ Aborting
++ Applied autostash.
++ EOF
++ test_cmp expect err
+'
+
test_expect_success 'switch to another branch while carrying a deletion' '
--
gitgitgadget
^ permalink raw reply
* [PATCH v16 1/5] stash: add --label-ours, --label-theirs, --label-base for apply
From: Harald Nordgren via GitGitGadget @ 2026-04-28 18:39 UTC (permalink / raw)
To: git; +Cc: Phillip Wood, Chris Torek, Jeff King, Harald Nordgren,
Harald Nordgren
In-Reply-To: <pull.2234.v16.git.git.1777401552.gitgitgadget@gmail.com>
From: Harald Nordgren <haraldnordgren@gmail.com>
Allow callers of "git stash apply" to pass custom labels for conflict
markers instead of the default "Updated upstream" and "Stashed changes".
Document the new options and add a test.
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
Documentation/git-stash.adoc | 11 ++++++++++-
builtin/stash.c | 28 ++++++++++++++++++++--------
t/t3903-stash.sh | 24 ++++++++++++++++++++++++
xdiff/xmerge.c | 6 +++---
4 files changed, 57 insertions(+), 12 deletions(-)
diff --git a/Documentation/git-stash.adoc b/Documentation/git-stash.adoc
index b05c990ecd..50bb89f483 100644
--- a/Documentation/git-stash.adoc
+++ b/Documentation/git-stash.adoc
@@ -12,7 +12,7 @@ git stash list [<log-options>]
git stash show [-u | --include-untracked | --only-untracked] [<diff-options>] [<stash>]
git stash drop [-q | --quiet] [<stash>]
git stash pop [--index] [-q | --quiet] [<stash>]
-git stash apply [--index] [-q | --quiet] [<stash>]
+git stash apply [--index] [-q | --quiet] [--label-ours=<label>] [--label-theirs=<label>] [--label-base=<label>] [<stash>]
git stash branch <branchname> [<stash>]
git stash [push] [-p | --patch] [-S | --staged] [-k | --[no-]keep-index] [-q | --quiet]
[-u | --include-untracked] [-a | --all] [(-m | --message) <message>]
@@ -195,6 +195,15 @@ the index's ones. However, this can fail, when you have conflicts
(which are stored in the index, where you therefore can no longer
apply the changes as they were originally).
+`--label-ours=<label>`::
+`--label-theirs=<label>`::
+`--label-base=<label>`::
+ These options are only valid for the `apply` command.
++
+Use the given labels in conflict markers instead of the default
+"Updated upstream", "Stashed changes", and "Stash base".
+`--label-base` only has an effect with merge.conflictStyle=diff3.
+
`-k`::
`--keep-index`::
`--no-keep-index`::
diff --git a/builtin/stash.c b/builtin/stash.c
index 0d27b2fb1f..32dbc97b47 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -44,7 +44,7 @@
#define BUILTIN_STASH_POP_USAGE \
N_("git stash pop [--index] [-q | --quiet] [<stash>]")
#define BUILTIN_STASH_APPLY_USAGE \
- N_("git stash apply [--index] [-q | --quiet] [<stash>]")
+ N_("git stash apply [--index] [-q | --quiet] [--label-ours=<label>] [--label-theirs=<label>] [--label-base=<label>] [<stash>]")
#define BUILTIN_STASH_BRANCH_USAGE \
N_("git stash branch <branchname> [<stash>]")
#define BUILTIN_STASH_STORE_USAGE \
@@ -591,7 +591,9 @@ static void unstage_changes_unless_new(struct object_id *orig_tree)
}
static int do_apply_stash(const char *prefix, struct stash_info *info,
- int index, int quiet)
+ int index, int quiet,
+ const char *label_ours, const char *label_theirs,
+ const char *label_base)
{
int clean, ret;
int has_index = index;
@@ -643,9 +645,9 @@ static int do_apply_stash(const char *prefix, struct stash_info *info,
init_ui_merge_options(&o, the_repository);
- o.branch1 = "Updated upstream";
- o.branch2 = "Stashed changes";
- o.ancestor = "Stash base";
+ o.branch1 = label_ours ? label_ours : "Updated upstream";
+ o.branch2 = label_theirs ? label_theirs : "Stashed changes";
+ o.ancestor = label_base ? label_base : "Stash base";
if (oideq(&info->b_tree, &c_tree))
o.branch1 = "Version stash was based on";
@@ -723,11 +725,18 @@ static int apply_stash(int argc, const char **argv, const char *prefix,
int ret = -1;
int quiet = 0;
int index = use_index;
+ const char *label_ours = NULL, *label_theirs = NULL, *label_base = NULL;
struct stash_info info = STASH_INFO_INIT;
struct option options[] = {
OPT__QUIET(&quiet, N_("be quiet, only report errors")),
OPT_BOOL(0, "index", &index,
N_("attempt to recreate the index")),
+ OPT_STRING(0, "label-ours", &label_ours, N_("label"),
+ N_("label for the upstream side in conflict markers")),
+ OPT_STRING(0, "label-theirs", &label_theirs, N_("label"),
+ N_("label for the stashed side in conflict markers")),
+ OPT_STRING(0, "label-base", &label_base, N_("label"),
+ N_("label for the base in diff3 conflict markers")),
OPT_END()
};
@@ -737,7 +746,8 @@ static int apply_stash(int argc, const char **argv, const char *prefix,
if (get_stash_info(&info, argc, argv))
goto cleanup;
- ret = do_apply_stash(prefix, &info, index, quiet);
+ ret = do_apply_stash(prefix, &info, index, quiet,
+ label_ours, label_theirs, label_base);
cleanup:
free_stash_info(&info);
return ret;
@@ -836,7 +846,8 @@ static int pop_stash(int argc, const char **argv, const char *prefix,
if (get_stash_info_assert(&info, argc, argv))
goto cleanup;
- if ((ret = do_apply_stash(prefix, &info, index, quiet)))
+ if ((ret = do_apply_stash(prefix, &info, index, quiet,
+ NULL, NULL, NULL)))
printf_ln(_("The stash entry is kept in case "
"you need it again."));
else
@@ -877,7 +888,8 @@ static int branch_stash(int argc, const char **argv, const char *prefix,
strvec_push(&cp.args, oid_to_hex(&info.b_commit));
ret = run_command(&cp);
if (!ret)
- ret = do_apply_stash(prefix, &info, 1, 0);
+ ret = do_apply_stash(prefix, &info, 1, 0,
+ NULL, NULL, NULL);
if (!ret && info.is_stash_ref)
ret = do_drop_stash(&info, 0);
diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
index 70879941c2..bdaad22e1f 100755
--- a/t/t3903-stash.sh
+++ b/t/t3903-stash.sh
@@ -56,6 +56,7 @@ setup_stash() {
git add other-file &&
test_tick &&
git commit -m initial &&
+ git tag initial &&
echo 2 >file &&
git add file &&
echo 3 >file &&
@@ -1790,4 +1791,27 @@ test_expect_success 'stash.index=false overridden by --index' '
test_cmp expect file
'
+test_expect_success 'apply with custom conflict labels' '
+ git reset --hard initial &&
+ test_commit label-base conflict-file base-content &&
+ echo stashed >conflict-file &&
+ git stash push -m "stashed" &&
+ test_commit label-upstream conflict-file upstream-content &&
+ test_must_fail git -c merge.conflictStyle=diff3 stash apply --label-ours=UP --label-theirs=STASH &&
+ test_grep "^<<<<<<< UP" conflict-file &&
+ test_grep "^||||||| Stash base" conflict-file &&
+ test_grep "^>>>>>>> STASH" conflict-file
+'
+
+test_expect_success 'apply with empty conflict labels' '
+ git reset --hard initial &&
+ test_commit empty-label-base conflict-file base-content &&
+ echo stashed >conflict-file &&
+ git stash push -m "stashed" &&
+ test_commit empty-label-upstream conflict-file upstream-content &&
+ test_must_fail git stash apply --label-ours= --label-theirs= &&
+ test_grep "^<<<<<<<$" conflict-file &&
+ test_grep "^>>>>>>>$" conflict-file
+'
+
test_done
diff --git a/xdiff/xmerge.c b/xdiff/xmerge.c
index 29dad98c49..659ad4ec97 100644
--- a/xdiff/xmerge.c
+++ b/xdiff/xmerge.c
@@ -199,9 +199,9 @@ static int fill_conflict_hunk(xdfenv_t *xe1, const char *name1,
int size, int i, int style,
xdmerge_t *m, char *dest, int marker_size)
{
- int marker1_size = (name1 ? strlen(name1) + 1 : 0);
- int marker2_size = (name2 ? strlen(name2) + 1 : 0);
- int marker3_size = (name3 ? strlen(name3) + 1 : 0);
+ int marker1_size = (name1 && *name1 ? strlen(name1) + 1 : 0);
+ int marker2_size = (name2 && *name2 ? strlen(name2) + 1 : 0);
+ int marker3_size = (name3 && *name3 ? strlen(name3) + 1 : 0);
int needs_cr = is_cr_needed(xe1, xe2, m);
if (marker_size <= 0)
--
gitgitgadget
^ permalink raw reply related
* [PATCH v16 2/5] sequencer: allow create_autostash to run silently
From: Harald Nordgren via GitGitGadget @ 2026-04-28 18:39 UTC (permalink / raw)
To: git; +Cc: Phillip Wood, Chris Torek, Jeff King, Harald Nordgren,
Harald Nordgren
In-Reply-To: <pull.2234.v16.git.git.1777401552.gitgitgadget@gmail.com>
From: Harald Nordgren <haraldnordgren@gmail.com>
Add a silent parameter to create_autostash_internal and introduce
create_autostash_ref_silent so that callers can create an autostash
without printing the "Created autostash" message.
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
builtin/merge.c | 6 ++++--
sequencer.c | 17 +++++++++++------
sequencer.h | 3 ++-
3 files changed, 17 insertions(+), 9 deletions(-)
diff --git a/builtin/merge.c b/builtin/merge.c
index 2cbce56f8d..3ebe190ef1 100644
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@ -1672,7 +1672,8 @@ int cmd_merge(int argc,
}
if (autostash)
- create_autostash_ref(the_repository, "MERGE_AUTOSTASH");
+ create_autostash_ref(the_repository, "MERGE_AUTOSTASH",
+ NULL, false);
if (checkout_fast_forward(the_repository,
&head_commit->object.oid,
&commit->object.oid,
@@ -1764,7 +1765,8 @@ int cmd_merge(int argc,
die_ff_impossible();
if (autostash)
- create_autostash_ref(the_repository, "MERGE_AUTOSTASH");
+ create_autostash_ref(the_repository, "MERGE_AUTOSTASH",
+ NULL, false);
/* We are going to make a new commit. */
git_committer_info(IDENT_STRICT);
diff --git a/sequencer.c b/sequencer.c
index b7d8dca47f..ff5258f481 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -4657,7 +4657,9 @@ static enum todo_command peek_command(struct todo_list *todo_list, int offset)
static void create_autostash_internal(struct repository *r,
const char *path,
- const char *refname)
+ const char *refname,
+ const char *message,
+ bool silent)
{
struct strbuf buf = STRBUF_INIT;
struct lock_file lock_file = LOCK_INIT;
@@ -4679,7 +4681,8 @@ static void create_autostash_internal(struct repository *r,
struct object_id oid;
strvec_pushl(&stash.args,
- "stash", "create", "autostash", NULL);
+ "stash", "create",
+ message ? message : "autostash", NULL);
stash.git_cmd = 1;
stash.no_stdin = 1;
strbuf_reset(&buf);
@@ -4702,7 +4705,8 @@ static void create_autostash_internal(struct repository *r,
&oid, null_oid(the_hash_algo), 0, UPDATE_REFS_DIE_ON_ERR);
}
- printf(_("Created autostash: %s\n"), buf.buf);
+ if (!silent)
+ printf(_("Created autostash: %s\n"), buf.buf);
if (reset_head(r, &ropts) < 0)
die(_("could not reset --hard"));
discard_index(r->index);
@@ -4714,12 +4718,13 @@ static void create_autostash_internal(struct repository *r,
void create_autostash(struct repository *r, const char *path)
{
- create_autostash_internal(r, path, NULL);
+ create_autostash_internal(r, path, NULL, NULL, false);
}
-void create_autostash_ref(struct repository *r, const char *refname)
+void create_autostash_ref(struct repository *r, const char *refname,
+ const char *message, bool silent)
{
- create_autostash_internal(r, NULL, refname);
+ create_autostash_internal(r, NULL, refname, message, silent);
}
static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply)
diff --git a/sequencer.h b/sequencer.h
index a6fa670c7c..02d2d9db06 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -229,7 +229,8 @@ void commit_post_rewrite(struct repository *r,
const struct object_id *new_head);
void create_autostash(struct repository *r, const char *path);
-void create_autostash_ref(struct repository *r, const char *refname);
+void create_autostash_ref(struct repository *r, const char *refname,
+ const char *message, bool silent);
int save_autostash(const char *path);
int save_autostash_ref(struct repository *r, const char *refname);
int apply_autostash(const char *path);
--
gitgitgadget
^ permalink raw reply related
* [PATCH v16 3/5] sequencer: teach autostash apply to take optional conflict marker labels
From: Harald Nordgren via GitGitGadget @ 2026-04-28 18:39 UTC (permalink / raw)
To: git; +Cc: Phillip Wood, Chris Torek, Jeff King, Harald Nordgren,
Harald Nordgren
In-Reply-To: <pull.2234.v16.git.git.1777401552.gitgitgadget@gmail.com>
From: Harald Nordgren <haraldnordgren@gmail.com>
Add label_ours, label_theirs, label_base, and stash_msg parameters to
apply_autostash_ref() and the autostash apply machinery so callers can
pass custom conflict marker labels through to
"git stash apply --label-ours/--label-theirs/--label-base", as well as
a custom stash message for "git stash store -m".
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
builtin/commit.c | 3 ++-
builtin/merge.c | 9 ++++++---
sequencer.c | 38 +++++++++++++++++++++++++++++---------
sequencer.h | 4 +++-
4 files changed, 40 insertions(+), 14 deletions(-)
diff --git a/builtin/commit.c b/builtin/commit.c
index a3e52ac9ca..28f6174503 100644
--- a/builtin/commit.c
+++ b/builtin/commit.c
@@ -1979,7 +1979,8 @@ int cmd_commit(int argc,
&oid, flags);
}
- apply_autostash_ref(the_repository, "MERGE_AUTOSTASH");
+ apply_autostash_ref(the_repository, "MERGE_AUTOSTASH",
+ NULL, NULL, NULL, NULL);
cleanup:
free_commit_extra_headers(extra);
diff --git a/builtin/merge.c b/builtin/merge.c
index 3ebe190ef1..aacf8c524e 100644
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@ -537,7 +537,8 @@ static void finish(struct commit *head_commit,
run_hooks_l(the_repository, "post-merge", squash ? "1" : "0", NULL);
if (new_head)
- apply_autostash_ref(the_repository, "MERGE_AUTOSTASH");
+ apply_autostash_ref(the_repository, "MERGE_AUTOSTASH",
+ NULL, NULL, NULL, NULL);
strbuf_release(&reflog_message);
}
@@ -1678,7 +1679,8 @@ int cmd_merge(int argc,
&head_commit->object.oid,
&commit->object.oid,
overwrite_ignore)) {
- apply_autostash_ref(the_repository, "MERGE_AUTOSTASH");
+ apply_autostash_ref(the_repository, "MERGE_AUTOSTASH",
+ NULL, NULL, NULL, NULL);
ret = 1;
goto done;
}
@@ -1851,7 +1853,8 @@ int cmd_merge(int argc,
else
fprintf(stderr, _("Merge with strategy %s failed.\n"),
use_strategies[0]->name);
- apply_autostash_ref(the_repository, "MERGE_AUTOSTASH");
+ apply_autostash_ref(the_repository, "MERGE_AUTOSTASH",
+ NULL, NULL, NULL, NULL);
ret = 2;
goto done;
} else if (best_strategy == wt_strategy)
diff --git a/sequencer.c b/sequencer.c
index ff5258f481..7c0376d9e4 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -4727,7 +4727,10 @@ void create_autostash_ref(struct repository *r, const char *refname,
create_autostash_internal(r, NULL, refname, message, silent);
}
-static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply)
+static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,
+ const char *label_ours, const char *label_theirs,
+ const char *label_base,
+ const char *stash_msg)
{
struct child_process child = CHILD_PROCESS_INIT;
int ret = 0;
@@ -4738,6 +4741,12 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply)
child.no_stderr = 1;
strvec_push(&child.args, "stash");
strvec_push(&child.args, "apply");
+ if (label_ours)
+ strvec_pushf(&child.args, "--label-ours=%s", label_ours);
+ if (label_theirs)
+ strvec_pushf(&child.args, "--label-theirs=%s", label_theirs);
+ if (label_base)
+ strvec_pushf(&child.args, "--label-base=%s", label_base);
strvec_push(&child.args, stash_oid);
ret = run_command(&child);
}
@@ -4751,7 +4760,7 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply)
strvec_push(&store.args, "stash");
strvec_push(&store.args, "store");
strvec_push(&store.args, "-m");
- strvec_push(&store.args, "autostash");
+ strvec_push(&store.args, stash_msg ? stash_msg : "autostash");
strvec_push(&store.args, "-q");
strvec_push(&store.args, stash_oid);
if (run_command(&store))
@@ -4782,7 +4791,8 @@ static int apply_save_autostash(const char *path, int attempt_apply)
}
strbuf_trim(&stash_oid);
- ret = apply_save_autostash_oid(stash_oid.buf, attempt_apply);
+ ret = apply_save_autostash_oid(stash_oid.buf, attempt_apply,
+ NULL, NULL, NULL, NULL);
unlink(path);
strbuf_release(&stash_oid);
@@ -4801,11 +4811,14 @@ int apply_autostash(const char *path)
int apply_autostash_oid(const char *stash_oid)
{
- return apply_save_autostash_oid(stash_oid, 1);
+ return apply_save_autostash_oid(stash_oid, 1, NULL, NULL, NULL, NULL);
}
static int apply_save_autostash_ref(struct repository *r, const char *refname,
- int attempt_apply)
+ int attempt_apply,
+ const char *label_ours, const char *label_theirs,
+ const char *label_base,
+ const char *stash_msg)
{
struct object_id stash_oid;
char stash_oid_hex[GIT_MAX_HEXSZ + 1];
@@ -4821,7 +4834,9 @@ static int apply_save_autostash_ref(struct repository *r, const char *refname,
return error(_("autostash reference is a symref"));
oid_to_hex_r(stash_oid_hex, &stash_oid);
- ret = apply_save_autostash_oid(stash_oid_hex, attempt_apply);
+ ret = apply_save_autostash_oid(stash_oid_hex, attempt_apply,
+ label_ours, label_theirs, label_base,
+ stash_msg);
refs_delete_ref(get_main_ref_store(r), "", refname,
&stash_oid, REF_NO_DEREF);
@@ -4831,12 +4846,17 @@ static int apply_save_autostash_ref(struct repository *r, const char *refname,
int save_autostash_ref(struct repository *r, const char *refname)
{
- return apply_save_autostash_ref(r, refname, 0);
+ return apply_save_autostash_ref(r, refname, 0,
+ NULL, NULL, NULL, NULL);
}
-int apply_autostash_ref(struct repository *r, const char *refname)
+int apply_autostash_ref(struct repository *r, const char *refname,
+ const char *label_ours, const char *label_theirs,
+ const char *label_base, const char *stash_msg)
{
- return apply_save_autostash_ref(r, refname, 1);
+ return apply_save_autostash_ref(r, refname, 1,
+ label_ours, label_theirs, label_base,
+ stash_msg);
}
static int checkout_onto(struct repository *r, struct replay_opts *opts,
diff --git a/sequencer.h b/sequencer.h
index 02d2d9db06..3164bd437d 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -235,7 +235,9 @@ int save_autostash(const char *path);
int save_autostash_ref(struct repository *r, const char *refname);
int apply_autostash(const char *path);
int apply_autostash_oid(const char *stash_oid);
-int apply_autostash_ref(struct repository *r, const char *refname);
+int apply_autostash_ref(struct repository *r, const char *refname,
+ const char *label_ours, const char *label_theirs,
+ const char *label_base, const char *stash_msg);
#define SUMMARY_INITIAL_COMMIT (1 << 0)
#define SUMMARY_SHOW_AUTHOR_DATE (1 << 1)
--
gitgitgadget
^ permalink raw reply related
* [PATCH v16 4/5] checkout: rollback lock on early returns in merge_working_tree
From: Harald Nordgren via GitGitGadget @ 2026-04-28 18:39 UTC (permalink / raw)
To: git; +Cc: Phillip Wood, Chris Torek, Jeff King, Harald Nordgren,
Harald Nordgren
In-Reply-To: <pull.2234.v16.git.git.1777401552.gitgitgadget@gmail.com>
From: Harald Nordgren <haraldnordgren@gmail.com>
merge_working_tree() acquires the index lock via
repo_hold_locked_index() but several early return paths exit
without calling rollback_lock_file(), leaving the lock held.
While this is currently harmless because the process exits soon
after, it becomes a problem if the function is ever called more
than once in the same process.
Add rollback_lock_file() calls to all early return paths.
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
builtin/checkout.c | 29 ++++++++++++++++++++++-------
1 file changed, 22 insertions(+), 7 deletions(-)
diff --git a/builtin/checkout.c b/builtin/checkout.c
index e031e61886..c80c62b37b 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -783,8 +783,10 @@ static int merge_working_tree(const struct checkout_opts *opts,
struct tree *new_tree;
repo_hold_locked_index(the_repository, &lock_file, LOCK_DIE_ON_ERROR);
- if (repo_read_index_preload(the_repository, NULL, 0) < 0)
+ if (repo_read_index_preload(the_repository, NULL, 0) < 0) {
+ rollback_lock_file(&lock_file);
return error(_("index file corrupt"));
+ }
resolve_undo_clear_index(the_repository->index);
if (opts->new_orphan_branch && opts->orphan_from_empty_tree) {
@@ -797,14 +799,18 @@ static int merge_working_tree(const struct checkout_opts *opts,
} else {
new_tree = repo_get_commit_tree(the_repository,
new_branch_info->commit);
- if (!new_tree)
+ if (!new_tree) {
+ rollback_lock_file(&lock_file);
return error(_("unable to read tree (%s)"),
oid_to_hex(&new_branch_info->commit->object.oid));
+ }
}
if (opts->discard_changes) {
ret = reset_tree(new_tree, opts, 1, writeout_error, new_branch_info);
- if (ret)
+ if (ret) {
+ rollback_lock_file(&lock_file);
return ret;
+ }
} else {
struct tree_desc trees[2];
struct tree *tree;
@@ -814,6 +820,7 @@ static int merge_working_tree(const struct checkout_opts *opts,
refresh_index(the_repository->index, REFRESH_QUIET, NULL, NULL, NULL);
if (unmerged_index(the_repository->index)) {
+ rollback_lock_file(&lock_file);
error(_("you need to resolve your current index first"));
return 1;
}
@@ -857,15 +864,19 @@ static int merge_working_tree(const struct checkout_opts *opts,
struct strbuf sb = STRBUF_INIT;
struct strbuf old_commit_shortname = STRBUF_INIT;
- if (!opts->merge)
+ if (!opts->merge) {
+ rollback_lock_file(&lock_file);
return 1;
+ }
/*
* Without old_branch_info->commit, the below is the same as
* the two-tree unpack we already tried and failed.
*/
- if (!old_branch_info->commit)
+ if (!old_branch_info->commit) {
+ rollback_lock_file(&lock_file);
return 1;
+ }
old_tree = repo_get_commit_tree(the_repository,
old_branch_info->commit);
@@ -897,8 +908,10 @@ static int merge_working_tree(const struct checkout_opts *opts,
ret = reset_tree(new_tree,
opts, 1,
writeout_error, new_branch_info);
- if (ret)
+ if (ret) {
+ rollback_lock_file(&lock_file);
return ret;
+ }
o.ancestor = old_branch_info->name;
if (!old_branch_info->name) {
strbuf_add_unique_abbrev(&old_commit_shortname,
@@ -920,8 +933,10 @@ static int merge_working_tree(const struct checkout_opts *opts,
writeout_error, new_branch_info);
strbuf_release(&o.obuf);
strbuf_release(&old_commit_shortname);
- if (ret)
+ if (ret) {
+ rollback_lock_file(&lock_file);
return ret;
+ }
}
}
--
gitgitgadget
^ permalink raw reply related
* [PATCH v16 5/5] checkout -m: autostash when switching branches
From: Harald Nordgren via GitGitGadget @ 2026-04-28 18:39 UTC (permalink / raw)
To: git; +Cc: Phillip Wood, Chris Torek, Jeff King, Harald Nordgren,
Harald Nordgren
In-Reply-To: <pull.2234.v16.git.git.1777401552.gitgitgadget@gmail.com>
From: Harald Nordgren <haraldnordgren@gmail.com>
When switching branches with "git checkout -m", the attempted merge
of local modifications may cause conflicts with the changes made on
the other branch, which the user may not want to (or may not be able
to) resolve right now. Because there is no easy way to recover from
this situation, we discouraged users from using "checkout -m" unless
they are certain their changes are trivial and within their ability
to resolve conflicts.
Teach the -m flow to create a temporary stash before switching and
reapply it after. On success, the stash is silently applied and
the list of locally modified paths is shown, same as a successful
"git checkout" without "-m".
If reapplying causes conflicts, the stash is kept and the user is
told they can resolve and run "git stash drop", or run "git reset
--hard" and later "git stash pop" to recover their changes.
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
Documentation/git-checkout.adoc | 55 ++++++-----
Documentation/git-switch.adoc | 36 +++----
builtin/checkout.c | 161 ++++++++++++++------------------
sequencer.c | 14 ++-
t/t3420-rebase-autostash.sh | 16 ++--
t/t7201-co.sh | 71 +++++++++++++-
t/t7600-merge.sh | 3 +-
xdiff-interface.c | 12 +++
xdiff-interface.h | 1 +
9 files changed, 219 insertions(+), 150 deletions(-)
diff --git a/Documentation/git-checkout.adoc b/Documentation/git-checkout.adoc
index 43ccf47cf6..a8b3b8c2e2 100644
--- a/Documentation/git-checkout.adoc
+++ b/Documentation/git-checkout.adoc
@@ -251,20 +251,19 @@ working tree, by copying them from elsewhere, extracting a tarball, etc.
are different between the current branch and the branch to
which you are switching, the command refuses to switch
branches in order to preserve your modifications in context.
- However, with this option, a three-way merge between the current
- branch, your working tree contents, and the new branch
- is done, and you will be on the new branch.
-+
-When a merge conflict happens, the index entries for conflicting
-paths are left unmerged, and you need to resolve the conflicts
-and mark the resolved paths with `git add` (or `git rm` if the merge
-should result in deletion of the path).
+ With this option, the conflicting local changes are
+ automatically stashed before the switch and reapplied
+ afterwards. If the local changes do not overlap with the
+ differences between branches, the switch proceeds without
+ stashing. If reapplying the stash results in conflicts, the
+ entry is saved to the stash list. Resolve the conflicts
+ and run `git stash drop` when done, or clear the working
+ tree (e.g. with `git reset --hard`) before running `git stash
+ pop` later to re-apply your changes.
+
When checking out paths from the index, this option lets you recreate
the conflicted merge in the specified paths. This option cannot be
used when checking out paths from a tree-ish.
-+
-When switching branches with `--merge`, staged changes may be lost.
`--conflict=<style>`::
The same as `--merge` option above, but changes the way the
@@ -578,38 +577,36 @@ $ git checkout mytopic
error: You have local changes to 'frotz'; not switching branches.
------------
-You can give the `-m` flag to the command, which would try a
-three-way merge:
+You can give the `-m` flag to the command, which will carry your local
+changes to the new branch:
------------
$ git checkout -m mytopic
-Auto-merging frotz
+Applied autostash.
+Switched to branch 'mytopic'
+The following paths have local changes:
+M frotz
------------
-After this three-way merge, the local modifications are _not_
+After the switch, the local modifications are reapplied and are _not_
registered in your index file, so `git diff` would show you what
changes you made since the tip of the new branch.
=== 3. Merge conflict
-When a merge conflict happens during switching branches with
-the `-m` option, you would see something like this:
+When the `--merge` (`-m`) option is given and the local changes
+overlap with the changes in the branch we're switching to, the
+changes are stashed and reapplied after the switch. If this
+process results in conflicts, the stash entry is saved and a
+message is printed:
------------
$ git checkout -m mytopic
-Auto-merging frotz
-ERROR: Merge conflict in frotz
-fatal: merge program failed
-------------
-
-At this point, `git diff` shows the changes cleanly merged as in
-the previous example, as well as the changes in the conflicted
-files. Edit and resolve the conflict and mark it resolved with
-`git add` as usual:
-
-------------
-$ edit frotz
-$ git add frotz
+Your local changes are stashed, however applying them
+resulted in conflicts. You can either resolve the conflicts
+and then discard the stash with "git stash drop", or, if you
+do not want to resolve them now, run "git reset --hard" and
+apply the local changes later by running "git stash pop".
------------
CONFIGURATION
diff --git a/Documentation/git-switch.adoc b/Documentation/git-switch.adoc
index 87707e9265..d6c4f229a5 100644
--- a/Documentation/git-switch.adoc
+++ b/Documentation/git-switch.adoc
@@ -123,18 +123,19 @@ variable.
`-m`::
`--merge`::
- If you have local modifications to one or more files that are
- different between the current branch and the branch to which
- you are switching, the command refuses to switch branches in
- order to preserve your modifications in context. However,
- with this option, a three-way merge between the current
- branch, your working tree contents, and the new branch is
- done, and you will be on the new branch.
-+
-When a merge conflict happens, the index entries for conflicting
-paths are left unmerged, and you need to resolve the conflicts
-and mark the resolved paths with `git add` (or `git rm` if the merge
-should result in deletion of the path).
+ If you have local modifications to one or more files that
+ are different between the current branch and the branch to
+ which you are switching, the command normally refuses to
+ switch branches in order to preserve your modifications in
+ context. However, with this option, the conflicting local
+ changes are automatically stashed before the switch and
+ reapplied afterwards. If the local changes do not overlap
+ with the differences between branches, the switch proceeds
+ without stashing. If reapplying the stash results in
+ conflicts, the entry is saved to the stash list. Resolve
+ the conflicts and run `git stash drop` when done, or clear
+ the working tree (e.g. with `git reset --hard`) before
+ running `git stash pop` later to re-apply your changes.
`--conflict=<style>`::
The same as `--merge` option above, but changes the way the
@@ -217,15 +218,18 @@ $ git switch mytopic
error: You have local changes to 'frotz'; not switching branches.
------------
-You can give the `-m` flag to the command, which would try a three-way
-merge:
+You can give the `-m` flag to the command, which will carry your local
+changes to the new branch:
------------
$ git switch -m mytopic
-Auto-merging frotz
+Applied autostash.
+Switched to branch 'mytopic'
+The following paths have local changes:
+M frotz
------------
-After this three-way merge, the local modifications are _not_
+After the switch, the local modifications are reapplied and are _not_
registered in your index file, so `git diff` would show you what
changes you made since the tip of the new branch.
diff --git a/builtin/checkout.c b/builtin/checkout.c
index c80c62b37b..3e9c456fad 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -17,7 +17,6 @@
#include "merge-ll.h"
#include "lockfile.h"
#include "mem-pool.h"
-#include "merge-ort-wrappers.h"
#include "object-file.h"
#include "object-name.h"
#include "odb.h"
@@ -30,6 +29,7 @@
#include "repo-settings.h"
#include "resolve-undo.h"
#include "revision.h"
+#include "sequencer.h"
#include "setup.h"
#include "submodule.h"
#include "symlinks.h"
@@ -99,6 +99,8 @@ struct checkout_opts {
.auto_advance = 1, \
}
+#define MERGE_WORKING_TREE_UNPACK_FAILED (-2)
+
struct branch_info {
char *name; /* The short name used */
char *path; /* The full name of a real branch */
@@ -753,9 +755,9 @@ static void setup_branch_path(struct branch_info *branch)
branch->path = strbuf_detach(&buf, NULL);
}
-static void init_topts(struct unpack_trees_options *topts, int merge,
+static void init_topts(struct unpack_trees_options *topts,
int show_progress, int overwrite_ignore,
- struct commit *old_commit)
+ bool quiet)
{
memset(topts, 0, sizeof(*topts));
topts->head_idx = -1;
@@ -767,7 +769,7 @@ static void init_topts(struct unpack_trees_options *topts, int merge,
topts->initial_checkout = is_index_unborn(the_repository->index);
topts->update = 1;
topts->merge = 1;
- topts->quiet = merge && old_commit;
+ topts->quiet = quiet;
topts->verbose_update = show_progress;
topts->fn = twoway_merge;
topts->preserve_ignored = !overwrite_ignore;
@@ -776,6 +778,7 @@ static void init_topts(struct unpack_trees_options *topts, int merge,
static int merge_working_tree(const struct checkout_opts *opts,
struct branch_info *old_branch_info,
struct branch_info *new_branch_info,
+ bool quiet,
int *writeout_error)
{
int ret;
@@ -826,8 +829,8 @@ static int merge_working_tree(const struct checkout_opts *opts,
}
/* 2-way merge to the new branch */
- init_topts(&topts, opts->merge, opts->show_progress,
- opts->overwrite_ignore, old_branch_info->commit);
+ init_topts(&topts, opts->show_progress,
+ opts->overwrite_ignore, quiet);
init_checkout_metadata(&topts.meta, new_branch_info->refname,
new_branch_info->commit ?
&new_branch_info->commit->object.oid :
@@ -853,90 +856,8 @@ static int merge_working_tree(const struct checkout_opts *opts,
ret = unpack_trees(2, trees, &topts);
clear_unpack_trees_porcelain(&topts);
if (ret == -1) {
- /*
- * Unpack couldn't do a trivial merge; either
- * give up or do a real merge, depending on
- * whether the merge flag was used.
- */
- struct tree *work;
- struct tree *old_tree;
- struct merge_options o;
- struct strbuf sb = STRBUF_INIT;
- struct strbuf old_commit_shortname = STRBUF_INIT;
-
- if (!opts->merge) {
- rollback_lock_file(&lock_file);
- return 1;
- }
-
- /*
- * Without old_branch_info->commit, the below is the same as
- * the two-tree unpack we already tried and failed.
- */
- if (!old_branch_info->commit) {
- rollback_lock_file(&lock_file);
- return 1;
- }
- old_tree = repo_get_commit_tree(the_repository,
- old_branch_info->commit);
-
- if (repo_index_has_changes(the_repository, old_tree, &sb))
- die(_("cannot continue with staged changes in "
- "the following files:\n%s"), sb.buf);
- strbuf_release(&sb);
-
- /* Do more real merge */
-
- /*
- * We update the index fully, then write the
- * tree from the index, then merge the new
- * branch with the current tree, with the old
- * branch as the base. Then we reset the index
- * (but not the working tree) to the new
- * branch, leaving the working tree as the
- * merged version, but skipping unmerged
- * entries in the index.
- */
-
- add_files_to_cache(the_repository, NULL, NULL, NULL, 0,
- 0, 0);
- init_ui_merge_options(&o, the_repository);
- o.verbosity = 0;
- work = write_in_core_index_as_tree(the_repository,
- the_repository->index);
-
- ret = reset_tree(new_tree,
- opts, 1,
- writeout_error, new_branch_info);
- if (ret) {
- rollback_lock_file(&lock_file);
- return ret;
- }
- o.ancestor = old_branch_info->name;
- if (!old_branch_info->name) {
- strbuf_add_unique_abbrev(&old_commit_shortname,
- &old_branch_info->commit->object.oid,
- DEFAULT_ABBREV);
- o.ancestor = old_commit_shortname.buf;
- }
- o.branch1 = new_branch_info->name;
- o.branch2 = "local";
- o.conflict_style = opts->conflict_style;
- ret = merge_ort_nonrecursive(&o,
- new_tree,
- work,
- old_tree);
- if (ret < 0)
- die(NULL);
- ret = reset_tree(new_tree,
- opts, 0,
- writeout_error, new_branch_info);
- strbuf_release(&o.obuf);
- strbuf_release(&old_commit_shortname);
- if (ret) {
- rollback_lock_file(&lock_file);
- return ret;
- }
+ rollback_lock_file(&lock_file);
+ return MERGE_WORKING_TREE_UNPACK_FAILED;
}
}
@@ -1181,6 +1102,10 @@ static int switch_branches(const struct checkout_opts *opts,
struct object_id rev;
int flag, writeout_error = 0;
int do_merge = 1;
+ int created_autostash = 0;
+ struct strbuf old_commit_shortname = STRBUF_INIT;
+ struct strbuf autostash_msg = STRBUF_INIT;
+ const char *stash_label_base = NULL;
trace2_cmd_mode("branch");
@@ -1218,11 +1143,49 @@ static int switch_branches(const struct checkout_opts *opts,
do_merge = 0;
}
+ if (old_branch_info.name) {
+ stash_label_base = old_branch_info.name;
+ } else if (old_branch_info.commit) {
+ strbuf_add_unique_abbrev(&old_commit_shortname,
+ &old_branch_info.commit->object.oid,
+ DEFAULT_ABBREV);
+ stash_label_base = old_commit_shortname.buf;
+ }
+
if (do_merge) {
- ret = merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error);
+ ret = merge_working_tree(opts, &old_branch_info, new_branch_info,
+ opts->merge, &writeout_error);
+ if (ret == MERGE_WORKING_TREE_UNPACK_FAILED && opts->merge) {
+ strbuf_addf(&autostash_msg,
+ "autostash while switching to '%s'",
+ new_branch_info->name);
+ create_autostash_ref(the_repository,
+ "CHECKOUT_AUTOSTASH_HEAD",
+ autostash_msg.buf, true);
+ created_autostash = 1;
+ ret = merge_working_tree(opts, &old_branch_info, new_branch_info,
+ false, &writeout_error);
+ }
+ if (created_autostash) {
+ if (opts->conflict_style >= 0) {
+ struct strbuf cfg = STRBUF_INIT;
+ strbuf_addf(&cfg, "merge.conflictStyle=%s",
+ conflict_style_name(opts->conflict_style));
+ git_config_push_parameter(cfg.buf);
+ strbuf_release(&cfg);
+ }
+ apply_autostash_ref(the_repository,
+ "CHECKOUT_AUTOSTASH_HEAD",
+ new_branch_info->name,
+ "local",
+ stash_label_base,
+ autostash_msg.buf);
+ }
if (ret) {
branch_info_release(&old_branch_info);
- return ret;
+ strbuf_release(&old_commit_shortname);
+ strbuf_release(&autostash_msg);
+ return ret < 0 ? 1 : ret;
}
}
@@ -1231,8 +1194,22 @@ static int switch_branches(const struct checkout_opts *opts,
update_refs_for_switch(opts, &old_branch_info, new_branch_info);
+ if (created_autostash) {
+ discard_index(the_repository->index);
+ if (repo_read_index(the_repository) < 0)
+ die(_("index file corrupt"));
+
+ if (!opts->quiet && new_branch_info->commit) {
+ printf(_("The following paths have local changes:\n"));
+ show_local_changes(&new_branch_info->commit->object,
+ &opts->diff_options);
+ }
+ }
+
ret = post_checkout_hook(old_branch_info.commit, new_branch_info->commit, 1);
branch_info_release(&old_branch_info);
+ strbuf_release(&old_commit_shortname);
+ strbuf_release(&autostash_msg);
return ret || writeout_error;
}
diff --git a/sequencer.c b/sequencer.c
index 7c0376d9e4..746f85a442 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -4765,15 +4765,19 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,
strvec_push(&store.args, stash_oid);
if (run_command(&store))
ret = error(_("cannot store %s"), stash_oid);
+ else if (attempt_apply)
+ fprintf(stderr,
+ _("Your local changes are stashed, however applying them\n"
+ "resulted in conflicts. You can either resolve the conflicts\n"
+ "and then discard the stash with \"git stash drop\", or, if you\n"
+ "do not want to resolve them now, run \"git reset --hard\" and\n"
+ "apply the local changes later by running \"git stash pop\".\n"));
else
fprintf(stderr,
- _("%s\n"
+ _("Autostash exists; creating a new stash entry.\n"
"Your changes are safe in the stash.\n"
"You can run \"git stash pop\" or"
- " \"git stash drop\" at any time.\n"),
- attempt_apply ?
- _("Applying autostash resulted in conflicts.") :
- _("Autostash exists; creating a new stash entry."));
+ " \"git stash drop\" at any time.\n"));
}
return ret;
diff --git a/t/t3420-rebase-autostash.sh b/t/t3420-rebase-autostash.sh
index ad3ba6a984..f0bbc476ff 100755
--- a/t/t3420-rebase-autostash.sh
+++ b/t/t3420-rebase-autostash.sh
@@ -61,18 +61,22 @@ create_expected_failure_apply () {
First, rewinding head to replay your work on top of it...
Applying: second commit
Applying: third commit
- Applying autostash resulted in conflicts.
- Your changes are safe in the stash.
- You can run "git stash pop" or "git stash drop" at any time.
+ Your local changes are stashed, however applying them
+ resulted in conflicts. You can either resolve the conflicts
+ and then discard the stash with "git stash drop", or, if you
+ do not want to resolve them now, run "git reset --hard" and
+ apply the local changes later by running "git stash pop".
EOF
}
create_expected_failure_merge () {
cat >expected <<-EOF
$(grep "^Created autostash: [0-9a-f][0-9a-f]*\$" actual)
- Applying autostash resulted in conflicts.
- Your changes are safe in the stash.
- You can run "git stash pop" or "git stash drop" at any time.
+ Your local changes are stashed, however applying them
+ resulted in conflicts. You can either resolve the conflicts
+ and then discard the stash with "git stash drop", or, if you
+ do not want to resolve them now, run "git reset --hard" and
+ apply the local changes later by running "git stash pop".
Successfully rebased and updated refs/heads/rebased-feature-branch.
EOF
}
diff --git a/t/t7201-co.sh b/t/t7201-co.sh
index 9bcf7c0b40..7613b1d2a4 100755
--- a/t/t7201-co.sh
+++ b/t/t7201-co.sh
@@ -102,7 +102,10 @@ test_expect_success 'checkout -m with dirty tree' '
test "$(git symbolic-ref HEAD)" = "refs/heads/side" &&
- printf "M\t%s\n" one >expect.messages &&
+ cat >expect.messages <<-\EOF &&
+ The following paths have local changes:
+ M one
+ EOF
test_cmp expect.messages messages &&
fill "M one" "A three" "D two" >expect.main &&
@@ -210,6 +213,72 @@ test_expect_success 'checkout --merge --conflict=diff3 <branch>' '
test_cmp expect two
'
+test_expect_success 'checkout -m with mixed staged and unstaged changes' '
+ git checkout -f main &&
+ git clean -f &&
+
+ fill 0 x y z >same &&
+ git add same &&
+ fill 1 2 3 4 5 6 7 >one &&
+ git checkout -m side >actual 2>&1 &&
+ test_grep "Applied autostash" actual &&
+ fill 0 x y z >expect &&
+ test_cmp expect same &&
+ fill 1 2 3 4 5 6 7 >expect &&
+ test_cmp expect one
+'
+
+test_expect_success 'checkout -m creates a recoverable stash on conflict' '
+ git checkout -f main &&
+ git clean -f &&
+
+ fill 1 2 3 4 5 >one &&
+ test_must_fail git checkout side 2>stderr &&
+ test_grep "Your local changes" stderr &&
+ git checkout -m side >actual 2>&1 &&
+ test_grep "resulted in conflicts" actual &&
+ test_grep "git stash drop" actual &&
+ test_grep "git stash pop" actual &&
+ test_grep "The following paths have local changes" actual &&
+ git log -p -1 --format="%gs%n%B" -g --diff-merges=1 refs/stash >actual &&
+ sed /^index/d actual >actual.trimmed &&
+ cat >expect <<-EOF &&
+ autostash while switching to ${SQ}side${SQ}
+ On main: autostash while switching to ${SQ}side${SQ}
+
+ diff --git a/one b/one
+ --- a/one
+ +++ b/one
+ @@ -3,6 +3,3 @@
+ 3
+ 4
+ 5
+ -6
+ -7
+ -8
+ EOF
+ test_cmp expect actual.trimmed &&
+ git stash drop &&
+ git reset --hard
+'
+
+test_expect_success 'checkout -m which would overwrite untracked file' '
+ git checkout -f --detach main &&
+ test_commit another-file &&
+ git checkout HEAD^ &&
+ >another-file.t &&
+ fill 1 2 3 4 5 >one &&
+ test_must_fail git checkout -m @{-1} 2>err &&
+ q_to_tab >expect <<-\EOF &&
+ error: The following untracked working tree files would be overwritten by checkout:
+ Qanother-file.t
+ Please move or remove them before you switch branches.
+ Aborting
+ Applied autostash.
+ EOF
+ test_cmp expect err
+'
+
test_expect_success 'switch to another branch while carrying a deletion' '
git checkout -f main &&
git reset --hard &&
diff --git a/t/t7600-merge.sh b/t/t7600-merge.sh
index 9838094b66..f877d9a433 100755
--- a/t/t7600-merge.sh
+++ b/t/t7600-merge.sh
@@ -914,7 +914,8 @@ test_expect_success 'merge with conflicted --autostash changes' '
git diff >expect &&
test_when_finished "test_might_fail git stash drop" &&
git merge --autostash c3 2>err &&
- test_grep "Applying autostash resulted in conflicts." err &&
+ test_grep "applying them" err &&
+ test_grep "resulted in conflicts" err &&
git show HEAD:file >merge-result &&
test_cmp result.1-9 merge-result &&
git stash show -p >actual &&
diff --git a/xdiff-interface.c b/xdiff-interface.c
index f043330f2a..5ee2b96d0a 100644
--- a/xdiff-interface.c
+++ b/xdiff-interface.c
@@ -325,6 +325,18 @@ int parse_conflict_style_name(const char *value)
return -1;
}
+const char *conflict_style_name(int style)
+{
+ switch (style) {
+ case XDL_MERGE_DIFF3:
+ return "diff3";
+ case XDL_MERGE_ZEALOUS_DIFF3:
+ return "zdiff3";
+ default:
+ return "merge";
+ }
+}
+
int git_xmerge_style = -1;
int git_xmerge_config(const char *var, const char *value,
diff --git a/xdiff-interface.h b/xdiff-interface.h
index fbc4ceec40..ce54e1c0e0 100644
--- a/xdiff-interface.h
+++ b/xdiff-interface.h
@@ -55,6 +55,7 @@ void xdiff_set_find_func(xdemitconf_t *xecfg, const char *line, int cflags);
void xdiff_clear_find_func(xdemitconf_t *xecfg);
struct config_context;
int parse_conflict_style_name(const char *value);
+const char *conflict_style_name(int style);
int git_xmerge_config(const char *var, const char *value,
const struct config_context *ctx, void *cb);
extern int git_xmerge_style;
--
gitgitgadget
^ permalink raw reply related
* git commit silently fails
From: Yuri @ 2026-04-28 20:39 UTC (permalink / raw)
To: Git Mailing List
Hi,
I was committing this way forever.
But today git failed:
[yuri@yv /usr/ports/devel/catch2]$ /usr/local/bin/git commit --verbose .
-m "devel/catch2: update 3.13.0 → 3.14.0"
[yuri@yv /usr/ports/devel/catch2]$ echo $?
1
[yuri@yv /usr/ports/devel/catch2]$ /usr/local/bin/git --version
git version 2.54.0
No error messages, no verbose messages, just failure ...
What might be wrong?
OS: FreeBSD 15 STABLE amd64
Thanks,
Yuri
^ permalink raw reply
* Re: Git generated tarballs and Debian
From: brian m. carlson @ 2026-04-28 21:20 UTC (permalink / raw)
To: Theodore Tso; +Cc: Simon Richter, git, Ian Jackson
In-Reply-To: <20260428115017.GA71700@macsyma-wired.lan>
[-- Attachment #1: Type: text/plain, Size: 3791 bytes --]
On 2026-04-28 at 11:50:17, Theodore Tso wrote:
> On Tue, Apr 28, 2026 at 10:25:24AM +0000, brian m. carlson wrote:
> >
> > I'll just note that we don't make any guarantees that `git archive`
> > produces identical output across versions. Incorrectly making that
> > assumption broke kernel.org when we changed the format in the past.
> >
> > Also, if you use `export-subst`, then it's possible to emit short object
> > IDs, which can differ in length depending on how many objects are in the
> > repository. It's also possible to use zlib or pigz instead of gzip to
> > produce tarballs, in which case the compressed data will also differ.
>
> This is what I've been using to try get reproducible tarballs for
> e2fprogs:
>
> git archive --prefix=e2fsprogs-${ver}/ ${commit} | gzip -9n > $fn
>
> ,,, where $commit is a signed git tag.
>
> I know that in the past, using --format=tgz has broken based on
> different compression parameters used by git (and whether it used an
> external or internal compressor). I also know that if $commit is a
> tree-id, this can result in the timestamps being not reproduible. I
> also don't use export-subst.
>
> There is also the difference in the prefix used by github and gitlab,
> but that's arguably not git's fault.
>
> What other gotchas are there? How is this likely to be inconsistent
> in the future? How much work is there to provide that guarantee in
> the future?
We could in theory provide reproducible tar output, but again, nobody
has committed to doing that yet. If we did that, we would add a special
option that produces, say, reproducible format v1, and if we needed to
make a change, then we would provide reproducible format v2, and so on.
That would also necessarily disable `export-subst`, since that
introduces non-reproducibility.
Of course, if you're using filters, then those can also be a source of
issues. Git LFS doesn't have that problem because it identifies objects
by SHA-256 hash, but there are many people who _do_ have unreproducible
filters (for instance, inserting the current date and time), so we might
need to disable those as well.
My approach would be to document a format and then implement it and
thoroughly test it. I was hoping, in fact, to define a format that
other tarball-generating implementations could _also_ implement, since
reproducible tarballs are also an issue for other tools like Cargo. I
have some code somewhere in some branch to do part of that, but it ran
into complexities due to handling `--add-file` and `--add-virtual-file`,
which are always appended, when we'd actually want them inserted in
sorted order. This will almost certainly be easier to write in Rust
because of better data structures and easier unit testing, so I may pick
it up at some point.
We cannot guarantee providing reproducible tar.gz output because we
don't control the compressor. If gzip tomorrow decides to release a
version that produces a different bitstream for some output, we're not
going to ship our own gzip. Same goes for zlib, especially since
different distros use different libraries to implement that interface.
Our zip files also have the undesirable attribute that they contain both
a local and a UTC timestamp, so the timezone is a problem. This bit us
at $DAYJOB when we started generating archives inside a container, since
the local timezone changed to UTC[0] and thus the archives were no
longer bit-for-bit identical. Of course, zip files also have
compression, which adds additional potential for reproducibility
problems.
[0] Yes, I know all servers should be using UTC and I agree, but that
decision got made well before my time.
--
brian m. carlson (they/them)
Toronto, Ontario, CA
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 325 bytes --]
^ permalink raw reply
* [PATCH v3 0/5] format-rev: introduce builtin for on-demand pretty formatting
From: kristofferhaugsbakk @ 2026-04-28 22:25 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk, ben.knoble
In-Reply-To: <V2_CV_name-rev_--format.51b@msgid.xyz>
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
(Previous subject: name-rev: learn --format=<pretty>)
Topic name (applied): kh/name-rev-custom-format
Topic summary: Introduce a new builtin for pretty formatting one revision
expression per line or commit object names found in running text.
See the last patch for the motivation. In short there isn’t anything that I
have found that lets you format however many commits you want through one
process (so looping over `git show` is excepted).
The other patches prepare for this change.
§ Changes in v3
Rewrite from a git-name-rev(1) option add-on to a new builtin. I make room
in `builtin/name-rev.c` for it.
This was based on this part from the previous cover letter:
git(1) already has a lot of commands. This doesn’t expand the footprint. On
the other hand, I might be naive about what a proper formatter needs in
terms of command options. Right now there is the helper `--notes`. But is
more needed?
• --abbrev ?
• --date ?
I don’t know. Maybe at some point this would be too much, too crowded, for
git-name-rev(1). Then it might make sense to split it out to a separate
builtin?
I did not want to expand this git-name-rev(1) command with more options
specific to pretty formatting.
§ Why experimental command?
This command is marked Experimental three times (like git-replay(1)).[1]
Not so much because the UI design seems difficult. It’s more so that it can
be thrown out if it doesn’t end up being worth having around.
(*Experimental* also implies wholesale trashing. Right? That’s one possible
change in behavior.)
Or the behavior here could be moved somewhere else.
† 1: I vaguely recall reading a good argument for emphasizing this on the
mailing list. I wasn’t able to find back to it right now.
§ Outstanding work
I could look more into how to do the translation strings for “X is
require” (arg), I’m probably missing something that already exists. I could
probably also link to other commands on the git-format-rev(1) doc. But
right now I wanted to get this out there. There might not be any appetite
for builtin number 148 for this particular niche anyway.
§ Link to v2
https://lore.kernel.org/git/V2_CV_name-rev_--format.51b@msgid.xyz/
[1/5] name-rev: wrap both blocks in braces
[2/5] name-rev: run clang-format before factoring code
[3/5] name-rev: factor code for sharing with a new command
[4/5] name-rev: make dedicated --annotate-stdin --name-only test
[5/5] format-rev: introduce builtin for on-demand pretty formatting
Documentation/git-format-rev.adoc | 148 +++++++++++++++++
Makefile | 1 +
builtin.h | 1 +
builtin/name-rev.c | 264 +++++++++++++++++++++++++++---
command-list.txt | 1 +
git.c | 1 +
t/t1517-outside-repo.sh | 3 +-
t/t6120-describe.sh | 118 +++++++++++++
8 files changed, 511 insertions(+), 26 deletions(-)
create mode 100644 Documentation/git-format-rev.adoc
Interdiff against v2:
diff --git a/Documentation/git-format-rev.adoc b/Documentation/git-format-rev.adoc
new file mode 100644
index 00000000000..d960001d750
--- /dev/null
+++ b/Documentation/git-format-rev.adoc
@@ -0,0 +1,148 @@
+git-format-rev(1)
+=================
+
+NAME
+----
+git-format-rev - EXPERIMENTAL: Pretty format revisions on demand
+
+
+SYNOPSIS
+--------
+[synopsis]
+(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]
+
+DESCRIPTION
+-----------
+
+Pretty format revisions from standard input.
+
+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
+
+OPTIONS
+-------
+
+`--stdin-mode=<mode>`::
+ How to interpret standard input data:
++
+--
+`revs`:: Each line is interpreted as a commit. Any kind of revision
+ expression can be used (see linkgit:gitrevisions[7]). Annotated
+ tags are peeled (see linkgit:gitglossary[7]).
++
+The argument `rev` is also accepted.
+`text`:: Formats all commit object names found in freeform text. These
+ must the full object names, i.e. abbreviated hexidecimal object
+ names will not be interpreted.
+--
+
+`--format=<pretty>`::
+ Pretty format string.
+
+`--notes=<ref>`::
+`--no-notes`::
+ Custom notes ref. Notes are displayed when using the `%N`
+ atom. See linkgit:git-notes[1].
+
+EXAMPLES
+--------
+
+The command linkgit:git-last-modified[1] shows the commit that each file
+was last modified in.
+
+----
+$ git last-modified -- README.md Makefile
+7798034171030be0909c56377a4e0e10e6d2df93 Makefile
+c50fbb2dd225e7e82abba4380423ae105089f4d7 README.md
+----
+
+We can pipe the result to this command in order to replace the object
+name with the commit author.
+
+----
+$ git last-modified -- README.md Makefile |
+ git format-rev --stdin-mode=text --format=%an
+Junio C Hamano Makefile
+Todd Zullinger README.md
+----
+
+Another example is _formatting commits in commit messages_. Given this commit message:
+
+----
+Fix off-by-one error
+
+Fix off-by-one error introduced in
+e83c5163316f89bfbde7d9ab23ca2e25604af290.
+
+We thought we fixed this in 5569bf9bbedd63a00780fc5c110e0cfab3aa97b9 but
+that only covered 1/3 of the faulty cases.
+----
+
+We can format the commits and use par(1) to reflow the text, say in a
+`commit-msg` hook:
+
+----
+$ git config set hook.reference-commits.event commit-msg
+$ git config set hook.reference-commits.command reference-commits
+$ cat $(which reference-commits)
+#/bin/sh
+
+msg="$1"
+rewritten=$(mktemp)
+git format-rev --stdin-mode=text --format=reference <"$msg" |
+ par >"$rewritten"
+mv "$rewritten" "$msg"
+----
+
+Which will produce something like this:
+
+----
+Fix off-by-one error
+
+Fix off-by-one error introduced in e83c5163316 (Implement better memory
+allocator, 2005-04-07).
+
+We thought we fixed this in 5569bf9bbed (Fix memory allocator,
+2005-06-22) but that only covered 1/3 of the faulty cases.
+----
+
+DISCUSSION
+----------
+
+This command lets you format any number of revisions in any order
+through one command invocation. Consider the
+linkgit:git-last-modified[1] case from the "EXAMPLES" section above:
+
+1. There might be hundreds of files
+2. Commits can be repeated, i.e. two or more files were last modified in
+ the same commit
+
+Two widely-used commands which pretty formats commits are
+linkgit:git-log[1] and linkgit:git-show[1]. It turns out that they are
+not a good fit for the above use case.
+
+- The output of linkgit:git-last-modified[1] would have to be processed
+ in stages since you need to transform the first column separately and
+ then link the author to the filename. But this is surmountable.
+- You can feed each commit to `git show` or `git show --no-walk -1`. But
+ that means that you need to create a process for each line.
+- Let’s say that you want to use one process, not one per line. So you
+ want to feed all the commits to the command. Now you face the problem
+ that you have to feed all the commits to the commands before you get
+ any output (this is also the case for the `--stdin` modes). In other
+ words, you cannot loop through each line, get the author for the
+ commit, and output the author and the filename. You need to feed all
+ the commits, get back all the output, and match the output with the
+ filename.
+- But the next problem is that commands will deduplicate the input and
+ only output one commit one single time only. Thus you cannot make the
+ output order match the input order, since a commit could have been
+ repeated in the original input.
+
+In short, it is straightforward to use these two commands if you use one
+process per line. It is much more work if you just want to use one
+process, but still doable. In contrast, this problem is just another
+shell pipeline with this command.
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Documentation/git-name-rev.adoc b/Documentation/git-name-rev.adoc
index 65348690c8c..d4f1c4d5945 100644
--- a/Documentation/git-name-rev.adoc
+++ b/Documentation/git-name-rev.adoc
@@ -9,7 +9,7 @@ git-name-rev - Find symbolic names for given revs
SYNOPSIS
--------
[verse]
-'git name-rev' [--tags] [--refs=<pattern>] [--format=<pretty>]
+'git name-rev' [--tags] [--refs=<pattern>]
( --all | --annotate-stdin | <commit-ish>... )
DESCRIPTION
@@ -21,14 +21,6 @@ format parsable by 'git rev-parse'.
OPTIONS
-------
---format=<pretty>::
---no-format::
- Format revisions instead of outputting symbolic names. The
- default is `--no-format`.
-+
-Implies `--name-only`. The negation `--no-format` implies
-`--no-name-only` (the default for the command).
-
--tags::
Do not use branch names, but only tags to name the commits
diff --git a/Makefile b/Makefile
index 15b1ded1a0b..cbaf91fd846 100644
--- a/Makefile
+++ b/Makefile
@@ -895,6 +895,7 @@ BUILT_INS += $(patsubst builtin/%.o,git-%$X,$(BUILTIN_OBJS))
BUILT_INS += git-cherry$X
BUILT_INS += git-cherry-pick$X
BUILT_INS += git-format-patch$X
+BUILT_INS += git-format-rev$X
BUILT_INS += git-fsck-objects$X
BUILT_INS += git-init$X
BUILT_INS += git-maintenance$X
diff --git a/builtin.h b/builtin.h
index 235c51f30e5..63813c90125 100644
--- a/builtin.h
+++ b/builtin.h
@@ -189,6 +189,7 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix, struct re
int cmd_for_each_ref(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_for_each_repo(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_format_patch(int argc, const char **argv, const char *prefix, struct repository *repo);
+int cmd_format_rev(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_fsck(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_gc(int argc, const char **argv, const char *prefix, struct repository *repo);
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 9a008d8b7a8..b60cc766279 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -36,16 +36,6 @@ struct rev_name {
int from_tag;
};
-struct pretty_format {
- struct pretty_print_context ctx;
- struct userformat_want want;
-};
-
-struct format_cb_data {
- const char *format;
- int *name_only;
-};
-
define_commit_slab(commit_rev_name, struct rev_name);
static timestamp_t generation_cutoff = GENERATION_NUMBER_INFINITY;
@@ -285,6 +275,43 @@ struct name_ref_data {
struct string_list exclude_filters;
};
+struct pretty_format {
+ struct pretty_print_context ctx;
+ struct userformat_want want;
+};
+
+enum command_type {
+ NAME_REV = 1,
+ FORMAT_REV = 2,
+};
+
+enum stdin_mode {
+ TEXT = 1,
+ REVS = 2,
+};
+
+struct command {
+ enum command_type type;
+ union {
+ int name_only;
+ struct pretty_format *pretty_format;
+ } u;
+};
+
+static void init_name_rev_command(struct command *cmd,
+ int name_only)
+{
+ cmd->type = NAME_REV;
+ cmd->u.name_only = name_only;
+}
+
+static void init_format_rev_command(struct command *cmd,
+ struct pretty_format *pretty_format)
+{
+ cmd->type = FORMAT_REV;
+ cmd->u.pretty_format = pretty_format;
+}
+
static struct tip_table {
struct tip_table_entry {
struct object_id oid;
@@ -467,9 +494,7 @@ static const char *get_exact_ref_match(const struct object *o)
}
/* may return a constant string or use "buf" as scratch space */
-static const char *get_rev_name(const struct object *o,
- struct pretty_format *format_ctx,
- struct strbuf *buf)
+static const char *get_rev_name(const struct object *o, struct strbuf *buf)
{
struct rev_name *n;
const struct commit *c;
@@ -477,25 +502,6 @@ static const char *get_rev_name(const struct object *o,
if (o->type != OBJ_COMMIT)
return get_exact_ref_match(o);
c = (const struct commit *) o;
-
- if (format_ctx) {
- strbuf_reset(buf);
-
- if (format_ctx->want.notes) {
- struct strbuf notebuf = STRBUF_INIT;
-
- format_display_notes(&c->object.oid, ¬ebuf,
- get_log_output_encoding(),
- format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);
- format_ctx->ctx.notes_message = strbuf_detach(¬ebuf, NULL);
- }
-
- pretty_print_commit(&format_ctx->ctx, c, buf);
- FREE_AND_NULL(format_ctx->ctx.notes_message);
-
- return buf->buf;
- }
-
n = get_commit_rev_name(c);
if (!n)
return NULL;
@@ -511,9 +517,29 @@ static const char *get_rev_name(const struct object *o,
}
}
+static const char *get_format_rev(const struct commit *c,
+ struct pretty_format *format_ctx,
+ struct strbuf *buf)
+{
+ strbuf_reset(buf);
+
+ if (format_ctx->want.notes) {
+ struct strbuf notebuf = STRBUF_INIT;
+
+ format_display_notes(&c->object.oid, ¬ebuf,
+ get_log_output_encoding(),
+ format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);
+ format_ctx->ctx.notes_message = strbuf_detach(¬ebuf, NULL);
+ }
+
+ pretty_print_commit(&format_ctx->ctx, c, buf);
+ FREE_AND_NULL(format_ctx->ctx.notes_message);
+
+ return buf->buf;
+}
+
static void show_name(const struct object *obj,
const char *caller_name,
- struct pretty_format *format_ctx,
int always, int allow_undefined, int name_only)
{
const char *name;
@@ -522,7 +548,7 @@ static void show_name(const struct object *obj,
if (!name_only)
printf("%s ", caller_name ? caller_name : oid_to_hex(oid));
- name = get_rev_name(obj, format_ctx, &buf);
+ name = get_rev_name(obj, &buf);
if (name)
printf("%s\n", name);
else if (allow_undefined)
@@ -542,9 +568,7 @@ static char const * const name_rev_usage[] = {
NULL
};
-static void name_rev_line(char *p,
- struct name_ref_data *data,
- struct pretty_format *format_ctx)
+static void name_rev_line(char *p, struct command *cmd)
{
struct strbuf buf = STRBUF_INIT;
int counter = 0;
@@ -553,33 +577,54 @@ static void name_rev_line(char *p,
for (p_start = p; *p; p++) {
#define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))
+ start:
if (!ishex(*p)) {
counter = 0;
} else if (++counter == hexsz &&
- !ishex(*(p+1))) {
+ !ishex(*(p + 1))) {
struct object_id oid;
const char *name = NULL;
- char c = *(p+1);
+ char c = *(p + 1);
int p_len = p - p_start + 1;
+ struct object *o = NULL;
+ int oid_ret = 1;
counter = 0;
- *(p+1) = 0;
- if (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {
- struct object *o =
- lookup_object(the_repository, &oid);
+ *(p + 1) = 0;
+ oid_ret = repo_get_oid(the_repository, p - (hexsz - 1), &oid);
+
+ switch (cmd->type) {
+ case NAME_REV:
+ if (!oid_ret)
+ o = lookup_object(the_repository, &oid);
if (o)
- name = get_rev_name(o, format_ctx, &buf);
+ name = get_rev_name(o, &buf);
+ *(p + 1) = c;
+ if (!name)
+ goto start;
+ if (cmd->u.name_only)
+ printf("%.*s%s", p_len - hexsz, p_start, name);
+ else
+ printf("%.*s (%s)", p_len, p_start, name);
+ break;
+ case FORMAT_REV:
+ if (!oid_ret)
+ o = parse_object(the_repository, &oid);
+ if (o && o->type == OBJ_COMMIT)
+ name = get_format_rev((const struct commit *)o,
+ cmd->u.pretty_format,
+ &buf);
+ *(p + 1) = c;
+ if (name)
+ printf("%.*s%s", p_len - hexsz, p_start, name);
+ else
+ printf("%.*s", p_len, p_start);
+ break;
+ default:
+ BUG("uncovered case: %d", cmd->type);
}
- *(p+1) = c;
- if (!name)
- continue;
-
- if (data->name_only)
- printf("%.*s%s", p_len - hexsz, p_start, name);
- else
- printf("%.*s (%s)", p_len, p_start, name);
p_start = p + 1;
}
}
@@ -591,16 +636,6 @@ static void name_rev_line(char *p,
strbuf_release(&buf);
}
-static int format_cb(const struct option *option,
- const char *arg,
- int unset)
-{
- struct format_cb_data *data = option->value;
- data->format = arg;
- *data->name_only = !unset;
- return 0;
-}
-
int cmd_name_rev(int argc,
const char **argv,
const char *prefix,
@@ -614,19 +649,14 @@ int cmd_name_rev(int argc,
#endif
int all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;
struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };
- static struct format_cb_data format_cb_data = { 0 };
- struct display_notes_opt format_notes_opt;
- struct rev_info format_rev = REV_INFO_INIT;
- struct pretty_format *format_ctx = NULL;
- struct pretty_format format_pp = { 0 };
- struct string_list notes = STRING_LIST_INIT_NODUP;
+ struct command cmd;
struct option opts[] = {
OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")),
OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
OPT_STRING_LIST(0, "refs", &data.ref_filters, N_("pattern"),
- N_("only use refs matching <pattern>")),
+ N_("only use refs matching <pattern>")),
OPT_STRING_LIST(0, "exclude", &data.exclude_filters, N_("pattern"),
- N_("ignore refs matching <pattern>")),
+ N_("ignore refs matching <pattern>")),
OPT_GROUP(""),
OPT_BOOL(0, "all", &all, N_("list all commits reachable from all refs")),
#ifndef WITH_BREAKING_CHANGES
@@ -637,24 +667,19 @@ int cmd_name_rev(int argc,
PARSE_OPT_HIDDEN),
#endif /* WITH_BREAKING_CHANGES */
OPT_BOOL(0, "annotate-stdin", &annotate_stdin, N_("annotate text from stdin")),
- OPT_CALLBACK(0, "format", &format_cb_data, N_("format"),
- N_("pretty-print output instead"), format_cb),
- OPT_STRING_LIST(0, "notes", ¬es, N_("notes"),
- N_("display notes for --format")),
OPT_BOOL(0, "undefined", &allow_undefined, N_("allow to print `undefined` names (default)")),
- OPT_BOOL(0, "always", &always,
- N_("show abbreviated commit object as fallback")),
+ OPT_BOOL(0, "always", &always,
+ N_("show abbreviated commit object as fallback")),
OPT_HIDDEN_BOOL(0, "peel-tag", &peel_tag,
- N_("dereference tags in the input (internal use)")),
+ N_("dereference tags in the input (internal use)")),
OPT_END(),
};
- init_display_notes(&format_notes_opt);
- format_cb_data.name_only = &data.name_only;
mem_pool_init(&string_pool, 0);
init_commit_rev_name(&rev_names);
repo_config(the_repository, git_default_config, NULL);
argc = parse_options(argc, argv, prefix, opts, name_rev_usage, 0);
+ init_name_rev_command(&cmd, data.name_only);
#ifndef WITH_BREAKING_CHANGES
if (transform_stdin) {
@@ -665,31 +690,6 @@ int cmd_name_rev(int argc,
}
#endif
- if (format_cb_data.format) {
- get_commit_format(format_cb_data.format, &format_rev);
- format_pp.ctx.rev = &format_rev;
- format_pp.ctx.fmt = format_rev.commit_format;
- format_pp.ctx.abbrev = format_rev.abbrev;
- format_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;
- format_pp.ctx.date_mode = format_rev.date_mode;
- format_pp.ctx.color = GIT_COLOR_AUTO;
-
- userformat_find_requirements(format_cb_data.format,
- &format_pp.want);
- if (format_pp.want.notes) {
- int ignore_show_notes = 0;
- struct string_list_item *n;
-
- for_each_string_list_item(n, ¬es)
- enable_ref_display_notes(&format_notes_opt,
- &ignore_show_notes,
- n->string);
- load_display_notes(&format_notes_opt);
- }
-
- format_ctx = &format_pp;
- }
-
if (all + annotate_stdin + !!argc > 1) {
error("Specify either a list, or --all, not both!");
usage_with_options(name_rev_usage, opts);
@@ -747,7 +747,7 @@ int cmd_name_rev(int argc,
while (strbuf_getline(&sb, stdin) != EOF) {
strbuf_addch(&sb, '\n');
- name_rev_line(sb.buf, &data, format_ctx);
+ name_rev_line(sb.buf, &cmd);
}
strbuf_release(&sb);
} else if (all) {
@@ -758,21 +758,149 @@ int cmd_name_rev(int argc,
struct object *obj = get_indexed_object(the_repository, i);
if (!obj || obj->type != OBJ_COMMIT)
continue;
- show_name(obj, NULL, format_ctx,
+ show_name(obj, NULL,
always, allow_undefined, data.name_only);
}
} else {
int i;
for (i = 0; i < revs.nr; i++)
- show_name(revs.objects[i].item, revs.objects[i].name, format_ctx,
+ show_name(revs.objects[i].item, revs.objects[i].name,
always, allow_undefined, data.name_only);
}
string_list_clear(&data.ref_filters, 0);
string_list_clear(&data.exclude_filters, 0);
- string_list_clear(¬es, 0);
- release_display_notes(&format_notes_opt);
mem_pool_discard(&string_pool, 0);
object_array_clear(&revs);
return 0;
}
+
+static enum stdin_mode parse_stdin_mode(const char *stdin_mode)
+{
+ if (!strcmp(stdin_mode, "text"))
+ return TEXT;
+ else if (!strcmp(stdin_mode, "revs") ||
+ !strcmp(stdin_mode, "rev"))
+ return REVS;
+ else
+ die(_("'%s' needs to be either text, revs, or rev"),
+ "--stdin-mode");
+}
+
+static char const *const format_rev_usage[] = {
+ N_("(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]"),
+ NULL
+};
+
+int cmd_format_rev(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo UNUSED)
+{
+ const char *format = NULL;
+ enum stdin_mode stdin_mode;
+ const char *stdin_mode_arg = NULL;
+ struct display_notes_opt format_notes_opt;
+ struct rev_info format_rev = REV_INFO_INIT;
+ struct pretty_format format_pp = { 0 };
+ struct string_list notes = STRING_LIST_INIT_NODUP;
+ struct strbuf scratch_buf = STRBUF_INIT;
+ struct command cmd;
+ struct option opts[] = {
+ OPT_STRING(0, "format", &format, N_("format"),
+ N_("pretty format to use")),
+ OPT_STRING(0, "stdin-mode", &stdin_mode_arg, N_("stdin-mode"),
+ N_("how revs are processed")),
+ OPT_STRING_LIST(0, "notes", ¬es, N_("notes"),
+ N_("display notes for pretty format")),
+ OPT_END(),
+ };
+
+ argc = parse_options(argc, argv, prefix, opts, format_rev_usage, 0);
+
+ if (argc > 0) {
+ error(_("too many arguments"));
+ usage_with_options(format_rev_usage, opts);
+ }
+
+ if (!format)
+ die(_("'%s' is required"), "--format");
+ if (!stdin_mode_arg)
+ die(_("'%s' is required"), "--stdin-mode");
+
+ init_display_notes(&format_notes_opt);
+ stdin_mode = parse_stdin_mode(stdin_mode_arg);
+
+ get_commit_format(format, &format_rev);
+ format_pp.ctx.rev = &format_rev;
+ format_pp.ctx.fmt = format_rev.commit_format;
+ format_pp.ctx.abbrev = format_rev.abbrev;
+ format_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;
+ format_pp.ctx.date_mode = format_rev.date_mode;
+ format_pp.ctx.color = GIT_COLOR_AUTO;
+
+ userformat_find_requirements(format,
+ &format_pp.want);
+ if (format_pp.want.notes) {
+ int ignore_show_notes = 0;
+ struct string_list_item *n;
+
+ for_each_string_list_item(n, ¬es)
+ enable_ref_display_notes(&format_notes_opt,
+ &ignore_show_notes,
+ n->string);
+ load_display_notes(&format_notes_opt);
+ }
+
+ init_format_rev_command(&cmd, &format_pp);
+
+ switch (stdin_mode) {
+ case TEXT:
+ while (strbuf_getline(&scratch_buf, stdin) != EOF) {
+ strbuf_addch(&scratch_buf, '\n');
+ name_rev_line(scratch_buf.buf, &cmd);
+ }
+ break;
+ case REVS:
+ while (strbuf_getline(&scratch_buf, stdin) != EOF) {
+ struct object_id oid;
+ struct object *object;
+ struct object *peeled;
+ struct commit *commit;
+
+ if (repo_get_oid(the_repository, scratch_buf.buf, &oid)) {
+ fprintf(stderr, "Could not get sha1 for %s. Skipping.\n",
+ scratch_buf.buf);
+ continue;
+ }
+
+ object = parse_object(the_repository, &oid);
+ if (!object) {
+ fprintf(stderr, "Could not get object for %s. Skipping.\n",
+ scratch_buf.buf);
+ continue;
+ }
+
+ peeled = deref_tag(the_repository, object, scratch_buf.buf, 0);
+ if (peeled && peeled->type == OBJ_COMMIT)
+ commit = (struct commit *)peeled;
+ if (!commit) {
+ fprintf(stderr, "Could not get commit for %s. Skipping.\n",
+ *argv);
+ continue;
+ }
+
+ get_format_rev(commit, &format_pp, &scratch_buf);
+ printf("%s\n", scratch_buf.buf);
+ strbuf_release(&scratch_buf);
+ }
+ break;
+ default:
+ BUG("uncovered case: %d", stdin_mode);
+ }
+
+ strbuf_release(&scratch_buf);
+ string_list_clear(¬es, 0);
+ release_display_notes(&format_notes_opt);
+ return 0;
+}
diff --git a/command-list.txt b/command-list.txt
index f9005cf4597..df729872dca 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -108,6 +108,7 @@ git-fmt-merge-msg purehelpers
git-for-each-ref plumbinginterrogators
git-for-each-repo plumbinginterrogators
git-format-patch mainporcelain
+git-format-rev plumbinginterrogators
git-fsck ancillaryinterrogators complete
git-gc mainporcelain
git-get-tar-commit-id plumbinginterrogators
diff --git a/git.c b/git.c
index 2b212e6675d..af5b0422b00 100644
--- a/git.c
+++ b/git.c
@@ -578,6 +578,7 @@ static struct cmd_struct commands[] = {
{ "for-each-ref", cmd_for_each_ref, RUN_SETUP },
{ "for-each-repo", cmd_for_each_repo, RUN_SETUP_GENTLY },
{ "format-patch", cmd_format_patch, RUN_SETUP },
+ { "format-rev", cmd_format_rev, RUN_SETUP },
{ "fsck", cmd_fsck, RUN_SETUP },
{ "fsck-objects", cmd_fsck, RUN_SETUP },
{ "fsmonitor--daemon", cmd_fsmonitor__daemon, RUN_SETUP },
diff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh
index c824c1a25cf..360a9323343 100755
--- a/t/t1517-outside-repo.sh
+++ b/t/t1517-outside-repo.sh
@@ -114,7 +114,8 @@ do
archimport | citool | credential-netrc | credential-libsecret | \
credential-osxkeychain | cvsexportcommit | cvsimport | cvsserver | \
daemon | \
- difftool--helper | filter-branch | fsck-objects | get-tar-commit-id | \
+ difftool--helper | filter-branch | format-rev | fsck-objects | \
+ get-tar-commit-id | \
gui | gui--askpass | \
http-backend | http-fetch | http-push | init-db | \
merge-octopus | merge-one-file | merge-resolve | mergetool | \
diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
index 0b7e9fe396d..725f7d81b6b 100755
--- a/t/t6120-describe.sh
+++ b/t/t6120-describe.sh
@@ -298,6 +298,20 @@ test_expect_success 'name-rev --annotate-stdin' '
test_cmp expect actual
'
+test_expect_success 'name-rev --annotate-stdin --name-only' '
+ >expect.unsorted &&
+ for rev in $(git rev-list --all)
+ do
+ name=$(git name-rev --name-only $rev) &&
+ echo "$name" >>expect.unsorted || return 1
+ done &&
+ sort <expect.unsorted >expect &&
+ git name-rev --annotate-stdin --name-only \
+ <list >actual.unsorted &&
+ sort <actual.unsorted >actual &&
+ test_cmp expect actual
+'
+
test_expect_success 'name-rev --stdin deprecated' '
git rev-list --all >list &&
if ! test_have_prereq WITH_BREAKING_CHANGES
@@ -658,102 +672,6 @@ test_expect_success 'name-rev --annotate-stdin works with commitGraph' '
)
'
-test_expect_success 'name-rev --format setup' '
- mkdir repo-format &&
- git -C repo-format init &&
- test_commit -C repo-format first &&
- test_commit -C repo-format second &&
- test_commit -C repo-format third &&
- test_commit -C repo-format fourth &&
- test_commit -C repo-format fifth &&
- test_commit -C repo-format sixth &&
- test_commit -C repo-format seventh &&
- test_commit -C repo-format eighth
-'
-
-test_expect_success 'name-rev --format --no-name-only' '
- cat >expect <<-\EOF &&
- HEAD~3 [fifth]
- HEAD [eighth]
- HEAD~5 [third]
- EOF
- git -C repo-format name-rev --format="[%s]" \
- --no-name-only HEAD~3 HEAD HEAD~5 >actual &&
- test_cmp expect actual
-'
-
-test_expect_success 'name-rev --format --no-format is the same as regular name-rev' '
- git -C repo-format name-rev HEAD~2 HEAD~3 >expect &&
- test_file_not_empty expect &&
- git -C repo-format name-rev --format="huh?" \
- --no-format HEAD~2 HEAD~3 >actual &&
- test_cmp expect actual
-'
-
-test_expect_success 'name-rev --format=%s for argument revs' '
- cat >expect <<-\EOF &&
- eighth
- seventh
- fifth
- EOF
- git -C repo-format name-rev --format=%s \
- HEAD HEAD~ HEAD~3 >actual &&
- test_cmp expect actual
-'
-
-test_expect_success '--name-rev --format=reference --annotate-stdin from rev-list same as log' '
- git -C repo-format log --format=reference >expect &&
- test_file_not_empty expect &&
- git -C repo-format rev-list HEAD >list &&
- git -C repo-format name-rev --format=reference \
- --annotate-stdin <list >actual &&
- test_cmp expect actual
-'
-
-test_expect_success '--name-rev --format=<pretty> --annotate-stdin with running text and tree oid' '
- cmit_oid=$(git -C repo-format rev-parse :/fifth) &&
- reference=$(git -C repo-format log -n1 --format=reference :/fifth) &&
- tree=$(git -C repo-format rev-parse HEAD^{tree}) &&
- cat >expect <<-EOF &&
- We thought we fixed this in ${reference}.
- But look at this tree: ${tree}.
- EOF
- git -C repo-format name-rev --format=reference --annotate-stdin \
- >actual <<-EOF &&
- We thought we fixed this in ${cmit_oid}.
- But look at this tree: ${tree}.
- EOF
- test_cmp expect actual
-'
-
-test_expect_success 'name-rev --format=<pretty> with %N (note)' '
- test_when_finished "git -C repo-format notes remove" &&
- git -C repo-format notes add -m"Make a note" &&
- printf "Make a note\n\n\n" >expect &&
- git -C repo-format name-rev --format="tformat:%N" \
- HEAD HEAD~ >actual &&
- test_cmp expect actual
-'
-
-test_expect_success 'name-rev --format=<pretty> --notes<ref>' '
- # One custom notes ref
- test_when_finished "git -C repo-format notes remove" &&
- test_when_finished "git -C repo-format notes --ref=word remove" &&
- git -C repo-format notes add -m"default" &&
- git -C repo-format notes --ref=word add -m"custom" &&
- printf "custom\n\n" >expect &&
- git -C repo-format name-rev --format="tformat:%N" \
- --notes=word \
- HEAD >actual &&
- test_cmp expect actual &&
- # Glob all
- printf "default\ncustom\n\n" >expect &&
- git -C repo-format name-rev --format="tformat:%N" \
- --notes=* \
- HEAD >actual &&
- test_cmp expect actual
-'
-
# B
# o
# H \
@@ -883,4 +801,108 @@ test_expect_success 'do not be fooled by invalid describe format ' '
test_must_fail git cat-file -t "refs/tags/super-invalid/./../...../ ~^:/?*[////\\\\\\&}/busted.lock-42-g"$(cat out)
'
+test_expect_success 'name-rev --format setup' '
+ mkdir repo-format &&
+ git -C repo-format init &&
+ test_commit -C repo-format first &&
+ test_commit -C repo-format second &&
+ test_commit -C repo-format third &&
+ test_commit -C repo-format fourth &&
+ test_commit -C repo-format fifth &&
+ test_commit -C repo-format sixth &&
+ test_commit -C repo-format seventh &&
+ test_commit -C repo-format eighth
+'
+
+test_expect_success 'format-rev --stdin-mode=revs' '
+ cat >expect <<-\EOF &&
+ eighth
+ seventh
+ fifth
+ EOF
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format=%s >actual <<-\EOF &&
+ HEAD
+ HEAD~
+ HEAD~3
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=text from rev-list same as log' '
+ git -C repo-format log --format=reference >expect &&
+ test_file_not_empty expect &&
+ git -C repo-format rev-list HEAD >list &&
+ git -C repo-format format-rev --stdin-mode=text \
+ --format=reference <list >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=text with running text and tree oid' '
+ cmit_oid=$(git -C repo-format rev-parse :/fifth) &&
+ reference=$(git -C repo-format log -n1 --format=reference :/fifth) &&
+ tree=$(git -C repo-format rev-parse HEAD^{tree}) &&
+ cat >expect <<-EOF &&
+ We thought we fixed this in ${reference}.
+ But look at this tree: ${tree}.
+ EOF
+ git -C repo-format format-rev --stdin-mode=text --format=reference \
+ >actual <<-EOF &&
+ We thought we fixed this in ${cmit_oid}.
+ But look at this tree: ${tree}.
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'format-rev with %N (note)' '
+ test_when_finished "git -C repo-format notes remove" &&
+ git -C repo-format notes add -m"Make a note" &&
+ printf "Make a note\n\n\n" >expect &&
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format="tformat:%N" \
+ >actual <<-\EOF &&
+ HEAD
+ HEAD~
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'format-rev --notes<ref> (custom notes ref)' '
+ # One custom notes ref
+ test_when_finished "git -C repo-format notes remove" &&
+ test_when_finished "git -C repo-format notes --ref=word remove" &&
+ git -C repo-format notes add -m"default" &&
+ git -C repo-format notes --ref=word add -m"custom" &&
+ printf "custom\n\n" >expect &&
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format="tformat:%N" \
+ --notes=word \
+ >actual <<-\EOF &&
+ HEAD
+ EOF
+ test_cmp expect actual &&
+ # Glob all
+ printf "default\ncustom\n\n" >expect &&
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format="tformat:%N" \
+ --notes=* >actual <<-\EOF &&
+ HEAD
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=revs on annotated tag peels to commit' '
+ test_when_finished "git -C repo-format tag -d version" &&
+ git -C repo-format tag -a -m"new version" version &&
+ cat >expect <<-\EOF &&
+ eighth
+ EOF
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format=%s \
+ --notes=* >actual <<-\EOF &&
+ version
+ EOF
+ test_cmp expect actual
+'
+
test_done
Range-diff against v2:
1: 9cb5cfd1ec3 = 1: 9cb5cfd1ec3 name-rev: wrap both blocks in braces
-: ----------- > 2: 14900271321 name-rev: run clang-format before factoring code
-: ----------- > 3: 1f17f0b0090 name-rev: factor code for sharing with a new command
-: ----------- > 4: 40989b3672a name-rev: make dedicated --annotate-stdin --name-only test
2: 52a52060776 ! 5: bb54e8f753e name-rev: learn --format=<pretty>
@@ Metadata
Author: Kristoffer Haugsbakk <code@khaugsbakk.name>
## Commit message ##
- name-rev: learn --format=<pretty>
+ format-rev: introduce builtin for on-demand pretty formatting
- Teach git-name-rev(1) to format the given revisions instead of creating
- symbolic names.
+ Introduce a new builtin for pretty formatting one revision expression
+ per line or commit object names found in running text.
- Sometimes you want to format commits. Most of the time you’re walking
- the graph, e.g. getting a range of commits like `master..topic`. That’s
- a job for git-log(1).
+ Sometimes you want to format commits. Most of the time you’re
+ walking the graph, e.g. getting a range of commits like
+ `master..topic`. That’s a job for git-log(1).
- But sometimes you might want to format commits that you encounter
+ But there are times when you want to format commits that you encounter
on demand:
• Full hashes in running text that you might want to pretty-print
- • git-last-modified(1) outputs full hashes that you can do the same with
+ • git-last-modified(1) outputs full hashes that you can do the same
+ with
• git-cherry(1) has `-v` for commit subject, but maybe you want
something else?
But now you can’t use git-log(1), git-show(1), or git-rev-list(1):
- • You can’t feed commits piecemeal to these commands, one input for one
- output; they block until standard in is closed
+ • You can’t feed commits piecemeal to these commands, one input
+ for one output; they block until standard in is closed
• You can’t feed a list of possibly duplicate commits, like the output
of git-last-modified(1); they effectively deduplicate the output
Beyond these two points there’s also the input massage problem: you
cannot feed mixed input (revisions mixed with arbitrary text).
- One might hope that git-cat-file(1) can save us. But it doesn’t support
- pretty formats.
+ One might hope that git-cat-file(1) can save us. But it doesn’t
+ support pretty formats.
But there is one command that already both handles revisions as
- arguments, revisions on standard input, and even revisions mixed
- in with arbitrary text. Namely git-name-rev(1).
+ arguments, revisions on standard input, and even revisions mixed in
+ with arbitrary text. Namely git-name-rev(1): the command for outputting
+ symbolic names for commits.
- Teach it to work in a format mode where the output for each revision is
- the pretty output (implies `--name-only`). This can be used to format
- any revision expression when given as arguments, and all full commit
- hashes in running text on stdin.
+ We made some room in `builtin/name-rev.c` two commits ago. Let’s
+ now add this new git-format-rev(1) command. Taking inspiration from
+ git-name-rev(1), there are two modes:
- Just bring the hashes (to the pipeline). We will pretty print them.
+ • revs: like git-name-rev(1) in argv mode, but one revision per line
+ on standard in
+ • text: like git-name-rev(1) with `--annotate-stdin`
+
+ ***
+
+ We need to add this command to the exception list in
+ `t/t1517-outside-repo.sh` because it uses “EXPERIMENTAL!”
+ in the usage line.
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
- ## Documentation/git-name-rev.adoc ##
-@@ Documentation/git-name-rev.adoc: git-name-rev - Find symbolic names for given revs
- SYNOPSIS
- --------
- [verse]
--'git name-rev' [--tags] [--refs=<pattern>]
-+'git name-rev' [--tags] [--refs=<pattern>] [--format=<pretty>]
- ( --all | --annotate-stdin | <commit-ish>... )
-
- DESCRIPTION
-@@ Documentation/git-name-rev.adoc: format parsable by 'git rev-parse'.
- OPTIONS
- -------
-
-+--format=<pretty>::
-+--no-format::
-+ Format revisions instead of outputting symbolic names. The
-+ default is `--no-format`.
+ ## Documentation/git-format-rev.adoc (new) ##
+@@
++git-format-rev(1)
++=================
++
++NAME
++----
++git-format-rev - EXPERIMENTAL: Pretty format revisions on demand
++
++
++SYNOPSIS
++--------
++[synopsis]
++(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]
++
++DESCRIPTION
++-----------
++
++Pretty format revisions from standard input.
++
++THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
++
++OPTIONS
++-------
++
++`--stdin-mode=<mode>`::
++ How to interpret standard input data:
+++
++--
++`revs`:: Each line is interpreted as a commit. Any kind of revision
++ expression can be used (see linkgit:gitrevisions[7]). Annotated
++ tags are peeled (see linkgit:gitglossary[7]).
++
-+Implies `--name-only`. The negation `--no-format` implies
-+`--no-name-only` (the default for the command).
++The argument `rev` is also accepted.
++`text`:: Formats all commit object names found in freeform text. These
++ must the full object names, i.e. abbreviated hexidecimal object
++ names will not be interpreted.
++--
+
- --tags::
- Do not use branch names, but only tags to name the commits
-
++`--format=<pretty>`::
++ Pretty format string.
++
++`--notes=<ref>`::
++`--no-notes`::
++ Custom notes ref. Notes are displayed when using the `%N`
++ atom. See linkgit:git-notes[1].
++
++EXAMPLES
++--------
++
++The command linkgit:git-last-modified[1] shows the commit that each file
++was last modified in.
++
++----
++$ git last-modified -- README.md Makefile
++7798034171030be0909c56377a4e0e10e6d2df93 Makefile
++c50fbb2dd225e7e82abba4380423ae105089f4d7 README.md
++----
++
++We can pipe the result to this command in order to replace the object
++name with the commit author.
++
++----
++$ git last-modified -- README.md Makefile |
++ git format-rev --stdin-mode=text --format=%an
++Junio C Hamano Makefile
++Todd Zullinger README.md
++----
++
++Another example is _formatting commits in commit messages_. Given this commit message:
++
++----
++Fix off-by-one error
++
++Fix off-by-one error introduced in
++e83c5163316f89bfbde7d9ab23ca2e25604af290.
++
++We thought we fixed this in 5569bf9bbedd63a00780fc5c110e0cfab3aa97b9 but
++that only covered 1/3 of the faulty cases.
++----
++
++We can format the commits and use par(1) to reflow the text, say in a
++`commit-msg` hook:
++
++----
++$ git config set hook.reference-commits.event commit-msg
++$ git config set hook.reference-commits.command reference-commits
++$ cat $(which reference-commits)
++#/bin/sh
++
++msg="$1"
++rewritten=$(mktemp)
++git format-rev --stdin-mode=text --format=reference <"$msg" |
++ par >"$rewritten"
++mv "$rewritten" "$msg"
++----
++
++Which will produce something like this:
++
++----
++Fix off-by-one error
++
++Fix off-by-one error introduced in e83c5163316 (Implement better memory
++allocator, 2005-04-07).
++
++We thought we fixed this in 5569bf9bbed (Fix memory allocator,
++2005-06-22) but that only covered 1/3 of the faulty cases.
++----
++
++DISCUSSION
++----------
++
++This command lets you format any number of revisions in any order
++through one command invocation. Consider the
++linkgit:git-last-modified[1] case from the "EXAMPLES" section above:
++
++1. There might be hundreds of files
++2. Commits can be repeated, i.e. two or more files were last modified in
++ the same commit
++
++Two widely-used commands which pretty formats commits are
++linkgit:git-log[1] and linkgit:git-show[1]. It turns out that they are
++not a good fit for the above use case.
++
++- The output of linkgit:git-last-modified[1] would have to be processed
++ in stages since you need to transform the first column separately and
++ then link the author to the filename. But this is surmountable.
++- You can feed each commit to `git show` or `git show --no-walk -1`. But
++ that means that you need to create a process for each line.
++- Let’s say that you want to use one process, not one per line. So you
++ want to feed all the commits to the command. Now you face the problem
++ that you have to feed all the commits to the commands before you get
++ any output (this is also the case for the `--stdin` modes). In other
++ words, you cannot loop through each line, get the author for the
++ commit, and output the author and the filename. You need to feed all
++ the commits, get back all the output, and match the output with the
++ filename.
++- But the next problem is that commands will deduplicate the input and
++ only output one commit one single time only. Thus you cannot make the
++ output order match the input order, since a commit could have been
++ repeated in the original input.
++
++In short, it is straightforward to use these two commands if you use one
++process per line. It is much more work if you just want to use one
++process, but still doable. In contrast, this problem is just another
++shell pipeline with this command.
++
++GIT
++---
++Part of the linkgit:git[1] suite
+
+ ## Makefile ##
+@@ Makefile: BUILT_INS += $(patsubst builtin/%.o,git-%$X,$(BUILTIN_OBJS))
+ BUILT_INS += git-cherry$X
+ BUILT_INS += git-cherry-pick$X
+ BUILT_INS += git-format-patch$X
++BUILT_INS += git-format-rev$X
+ BUILT_INS += git-fsck-objects$X
+ BUILT_INS += git-init$X
+ BUILT_INS += git-maintenance$X
+
+ ## builtin.h ##
+@@ builtin.h: int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix, struct re
+ int cmd_for_each_ref(int argc, const char **argv, const char *prefix, struct repository *repo);
+ int cmd_for_each_repo(int argc, const char **argv, const char *prefix, struct repository *repo);
+ int cmd_format_patch(int argc, const char **argv, const char *prefix, struct repository *repo);
++int cmd_format_rev(int argc, const char **argv, const char *prefix, struct repository *repo);
+ int cmd_fsck(int argc, const char **argv, const char *prefix, struct repository *repo);
+ int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix, struct repository *repo);
+ int cmd_gc(int argc, const char **argv, const char *prefix, struct repository *repo);
## builtin/name-rev.c ##
@@
@@ builtin/name-rev.c
/*
* One day. See the 'name a rev shortly after epoch' test in t6120 when
-@@ builtin/name-rev.c: struct rev_name {
- int from_tag;
+@@ builtin/name-rev.c: struct name_ref_data {
+ struct string_list exclude_filters;
};
+struct pretty_format {
@@ builtin/name-rev.c: struct rev_name {
+ struct userformat_want want;
+};
+
-+struct format_cb_data {
-+ const char *format;
-+ int *name_only;
+ enum command_type {
+ NAME_REV = 1,
++ FORMAT_REV = 2,
+};
+
- define_commit_slab(commit_rev_name, struct rev_name);
++enum stdin_mode {
++ TEXT = 1,
++ REVS = 2,
+ };
+
+ struct command {
+ enum command_type type;
+ union {
+ int name_only;
++ struct pretty_format *pretty_format;
+ } u;
+ };
- static timestamp_t generation_cutoff = GENERATION_NUMBER_INFINITY;
-@@ builtin/name-rev.c: static const char *get_exact_ref_match(const struct object *o)
+@@ builtin/name-rev.c: static void init_name_rev_command(struct command *cmd,
+ cmd->u.name_only = name_only;
}
- /* may return a constant string or use "buf" as scratch space */
--static const char *get_rev_name(const struct object *o, struct strbuf *buf)
-+static const char *get_rev_name(const struct object *o,
-+ struct pretty_format *format_ctx,
-+ struct strbuf *buf)
- {
- struct rev_name *n;
- const struct commit *c;
-@@ builtin/name-rev.c: static const char *get_rev_name(const struct object *o, struct strbuf *buf)
- if (o->type != OBJ_COMMIT)
- return get_exact_ref_match(o);
- c = (const struct commit *) o;
++static void init_format_rev_command(struct command *cmd,
++ struct pretty_format *pretty_format)
++{
++ cmd->type = FORMAT_REV;
++ cmd->u.pretty_format = pretty_format;
++}
+
-+ if (format_ctx) {
-+ strbuf_reset(buf);
+ static struct tip_table {
+ struct tip_table_entry {
+ struct object_id oid;
+@@ builtin/name-rev.c: static const char *get_rev_name(const struct object *o, struct strbuf *buf)
+ }
+ }
+
++static const char *get_format_rev(const struct commit *c,
++ struct pretty_format *format_ctx,
++ struct strbuf *buf)
++{
++ strbuf_reset(buf);
+
-+ if (format_ctx->want.notes) {
-+ struct strbuf notebuf = STRBUF_INIT;
++ if (format_ctx->want.notes) {
++ struct strbuf notebuf = STRBUF_INIT;
+
-+ format_display_notes(&c->object.oid, ¬ebuf,
-+ get_log_output_encoding(),
-+ format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);
-+ format_ctx->ctx.notes_message = strbuf_detach(¬ebuf, NULL);
-+ }
++ format_display_notes(&c->object.oid, ¬ebuf,
++ get_log_output_encoding(),
++ format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);
++ format_ctx->ctx.notes_message = strbuf_detach(¬ebuf, NULL);
++ }
+
-+ pretty_print_commit(&format_ctx->ctx, c, buf);
-+ FREE_AND_NULL(format_ctx->ctx.notes_message);
++ pretty_print_commit(&format_ctx->ctx, c, buf);
++ FREE_AND_NULL(format_ctx->ctx.notes_message);
+
-+ return buf->buf;
-+ }
++ return buf->buf;
++}
+
- n = get_commit_rev_name(c);
- if (!n)
- return NULL;
-@@ builtin/name-rev.c: static const char *get_rev_name(const struct object *o, struct strbuf *buf)
-
static void show_name(const struct object *obj,
const char *caller_name,
-+ struct pretty_format *format_ctx,
int always, int allow_undefined, int name_only)
- {
- const char *name;
-@@ builtin/name-rev.c: static void show_name(const struct object *obj,
-
- if (!name_only)
- printf("%s ", caller_name ? caller_name : oid_to_hex(oid));
-- name = get_rev_name(obj, &buf);
-+ name = get_rev_name(obj, format_ctx, &buf);
- if (name)
- printf("%s\n", name);
- else if (allow_undefined)
-@@ builtin/name-rev.c: static char const * const name_rev_usage[] = {
- NULL
- };
-
--static void name_rev_line(char *p, struct name_ref_data *data)
-+static void name_rev_line(char *p,
-+ struct name_ref_data *data,
-+ struct pretty_format *format_ctx)
- {
- struct strbuf buf = STRBUF_INIT;
- int counter = 0;
-@@ builtin/name-rev.c: static void name_rev_line(char *p, struct name_ref_data *data)
- struct object *o =
- lookup_object(the_repository, &oid);
- if (o)
-- name = get_rev_name(o, &buf);
-+ name = get_rev_name(o, format_ctx, &buf);
+@@ builtin/name-rev.c: static void name_rev_line(char *p, struct command *cmd)
+ else
+ printf("%.*s (%s)", p_len, p_start, name);
+ break;
++ case FORMAT_REV:
++ if (!oid_ret)
++ o = parse_object(the_repository, &oid);
++ if (o && o->type == OBJ_COMMIT)
++ name = get_format_rev((const struct commit *)o,
++ cmd->u.pretty_format,
++ &buf);
++ *(p + 1) = c;
++ if (name)
++ printf("%.*s%s", p_len - hexsz, p_start, name);
++ else
++ printf("%.*s", p_len, p_start);
++ break;
+ default:
+ BUG("uncovered case: %d", cmd->type);
}
- *(p+1) = c;
-
-@@ builtin/name-rev.c: static void name_rev_line(char *p, struct name_ref_data *data)
- strbuf_release(&buf);
+@@ builtin/name-rev.c: int cmd_name_rev(int argc,
+ object_array_clear(&revs);
+ return 0;
}
-
-+static int format_cb(const struct option *option,
-+ const char *arg,
-+ int unset)
++
++static enum stdin_mode parse_stdin_mode(const char *stdin_mode)
+{
-+ struct format_cb_data *data = option->value;
-+ data->format = arg;
-+ *data->name_only = !unset;
-+ return 0;
++ if (!strcmp(stdin_mode, "text"))
++ return TEXT;
++ else if (!strcmp(stdin_mode, "revs") ||
++ !strcmp(stdin_mode, "rev"))
++ return REVS;
++ else
++ die(_("'%s' needs to be either text, revs, or rev"),
++ "--stdin-mode");
+}
+
- int cmd_name_rev(int argc,
- const char **argv,
- const char *prefix,
-@@ builtin/name-rev.c: int cmd_name_rev(int argc,
- #endif
- int all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;
- struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };
-+ static struct format_cb_data format_cb_data = { 0 };
++static char const *const format_rev_usage[] = {
++ N_("(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]"),
++ NULL
++};
++
++int cmd_format_rev(int argc,
++ const char **argv,
++ const char *prefix,
++ struct repository *repo UNUSED)
++{
++ const char *format = NULL;
++ enum stdin_mode stdin_mode;
++ const char *stdin_mode_arg = NULL;
+ struct display_notes_opt format_notes_opt;
+ struct rev_info format_rev = REV_INFO_INIT;
-+ struct pretty_format *format_ctx = NULL;
+ struct pretty_format format_pp = { 0 };
+ struct string_list notes = STRING_LIST_INIT_NODUP;
- struct option opts[] = {
- OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")),
- OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
-@@ builtin/name-rev.c: int cmd_name_rev(int argc,
- PARSE_OPT_HIDDEN),
- #endif /* WITH_BREAKING_CHANGES */
- OPT_BOOL(0, "annotate-stdin", &annotate_stdin, N_("annotate text from stdin")),
-+ OPT_CALLBACK(0, "format", &format_cb_data, N_("format"),
-+ N_("pretty-print output instead"), format_cb),
++ struct strbuf scratch_buf = STRBUF_INIT;
++ struct command cmd;
++ struct option opts[] = {
++ OPT_STRING(0, "format", &format, N_("format"),
++ N_("pretty format to use")),
++ OPT_STRING(0, "stdin-mode", &stdin_mode_arg, N_("stdin-mode"),
++ N_("how revs are processed")),
+ OPT_STRING_LIST(0, "notes", ¬es, N_("notes"),
-+ N_("display notes for --format")),
- OPT_BOOL(0, "undefined", &allow_undefined, N_("allow to print `undefined` names (default)")),
- OPT_BOOL(0, "always", &always,
- N_("show abbreviated commit object as fallback")),
-@@ builtin/name-rev.c: int cmd_name_rev(int argc,
- OPT_END(),
- };
-
++ N_("display notes for pretty format")),
++ OPT_END(),
++ };
++
++ argc = parse_options(argc, argv, prefix, opts, format_rev_usage, 0);
++
++ if (argc > 0) {
++ error(_("too many arguments"));
++ usage_with_options(format_rev_usage, opts);
++ }
++
++ if (!format)
++ die(_("'%s' is required"), "--format");
++ if (!stdin_mode_arg)
++ die(_("'%s' is required"), "--stdin-mode");
++
+ init_display_notes(&format_notes_opt);
-+ format_cb_data.name_only = &data.name_only;
- mem_pool_init(&string_pool, 0);
- init_commit_rev_name(&rev_names);
- repo_config(the_repository, git_default_config, NULL);
-@@ builtin/name-rev.c: int cmd_name_rev(int argc,
- }
- #endif
-
-+ if (format_cb_data.format) {
-+ get_commit_format(format_cb_data.format, &format_rev);
-+ format_pp.ctx.rev = &format_rev;
-+ format_pp.ctx.fmt = format_rev.commit_format;
-+ format_pp.ctx.abbrev = format_rev.abbrev;
-+ format_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;
-+ format_pp.ctx.date_mode = format_rev.date_mode;
-+ format_pp.ctx.color = GIT_COLOR_AUTO;
-+
-+ userformat_find_requirements(format_cb_data.format,
-+ &format_pp.want);
-+ if (format_pp.want.notes) {
-+ int ignore_show_notes = 0;
-+ struct string_list_item *n;
-+
-+ for_each_string_list_item(n, ¬es)
-+ enable_ref_display_notes(&format_notes_opt,
-+ &ignore_show_notes,
-+ n->string);
-+ load_display_notes(&format_notes_opt);
++ stdin_mode = parse_stdin_mode(stdin_mode_arg);
++
++ get_commit_format(format, &format_rev);
++ format_pp.ctx.rev = &format_rev;
++ format_pp.ctx.fmt = format_rev.commit_format;
++ format_pp.ctx.abbrev = format_rev.abbrev;
++ format_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;
++ format_pp.ctx.date_mode = format_rev.date_mode;
++ format_pp.ctx.color = GIT_COLOR_AUTO;
++
++ userformat_find_requirements(format,
++ &format_pp.want);
++ if (format_pp.want.notes) {
++ int ignore_show_notes = 0;
++ struct string_list_item *n;
++
++ for_each_string_list_item(n, ¬es)
++ enable_ref_display_notes(&format_notes_opt,
++ &ignore_show_notes,
++ n->string);
++ load_display_notes(&format_notes_opt);
++ }
++
++ init_format_rev_command(&cmd, &format_pp);
++
++ switch (stdin_mode) {
++ case TEXT:
++ while (strbuf_getline(&scratch_buf, stdin) != EOF) {
++ strbuf_addch(&scratch_buf, '\n');
++ name_rev_line(scratch_buf.buf, &cmd);
+ }
++ break;
++ case REVS:
++ while (strbuf_getline(&scratch_buf, stdin) != EOF) {
++ struct object_id oid;
++ struct object *object;
++ struct object *peeled;
++ struct commit *commit;
++
++ if (repo_get_oid(the_repository, scratch_buf.buf, &oid)) {
++ fprintf(stderr, "Could not get sha1 for %s. Skipping.\n",
++ scratch_buf.buf);
++ continue;
++ }
++
++ object = parse_object(the_repository, &oid);
++ if (!object) {
++ fprintf(stderr, "Could not get object for %s. Skipping.\n",
++ scratch_buf.buf);
++ continue;
++ }
+
-+ format_ctx = &format_pp;
++ peeled = deref_tag(the_repository, object, scratch_buf.buf, 0);
++ if (peeled && peeled->type == OBJ_COMMIT)
++ commit = (struct commit *)peeled;
++ if (!commit) {
++ fprintf(stderr, "Could not get commit for %s. Skipping.\n",
++ *argv);
++ continue;
++ }
++
++ get_format_rev(commit, &format_pp, &scratch_buf);
++ printf("%s\n", scratch_buf.buf);
++ strbuf_release(&scratch_buf);
++ }
++ break;
++ default:
++ BUG("uncovered case: %d", stdin_mode);
+ }
+
- if (all + annotate_stdin + !!argc > 1) {
- error("Specify either a list, or --all, not both!");
- usage_with_options(name_rev_usage, opts);
-@@ builtin/name-rev.c: int cmd_name_rev(int argc,
-
- while (strbuf_getline(&sb, stdin) != EOF) {
- strbuf_addch(&sb, '\n');
-- name_rev_line(sb.buf, &data);
-+ name_rev_line(sb.buf, &data, format_ctx);
- }
- strbuf_release(&sb);
- } else if (all) {
-@@ builtin/name-rev.c: int cmd_name_rev(int argc,
- struct object *obj = get_indexed_object(the_repository, i);
- if (!obj || obj->type != OBJ_COMMIT)
- continue;
-- show_name(obj, NULL,
-+ show_name(obj, NULL, format_ctx,
- always, allow_undefined, data.name_only);
- }
- } else {
- int i;
- for (i = 0; i < revs.nr; i++)
-- show_name(revs.objects[i].item, revs.objects[i].name,
-+ show_name(revs.objects[i].item, revs.objects[i].name, format_ctx,
- always, allow_undefined, data.name_only);
- }
-
- string_list_clear(&data.ref_filters, 0);
- string_list_clear(&data.exclude_filters, 0);
++ strbuf_release(&scratch_buf);
+ string_list_clear(¬es, 0);
+ release_display_notes(&format_notes_opt);
- mem_pool_discard(&string_pool, 0);
- object_array_clear(&revs);
- return 0;
++ return 0;
++}
+
+ ## command-list.txt ##
+@@ command-list.txt: git-fmt-merge-msg purehelpers
+ git-for-each-ref plumbinginterrogators
+ git-for-each-repo plumbinginterrogators
+ git-format-patch mainporcelain
++git-format-rev plumbinginterrogators
+ git-fsck ancillaryinterrogators complete
+ git-gc mainporcelain
+ git-get-tar-commit-id plumbinginterrogators
+
+ ## git.c ##
+@@ git.c: static struct cmd_struct commands[] = {
+ { "for-each-ref", cmd_for_each_ref, RUN_SETUP },
+ { "for-each-repo", cmd_for_each_repo, RUN_SETUP_GENTLY },
+ { "format-patch", cmd_format_patch, RUN_SETUP },
++ { "format-rev", cmd_format_rev, RUN_SETUP },
+ { "fsck", cmd_fsck, RUN_SETUP },
+ { "fsck-objects", cmd_fsck, RUN_SETUP },
+ { "fsmonitor--daemon", cmd_fsmonitor__daemon, RUN_SETUP },
+
+ ## t/t1517-outside-repo.sh ##
+@@ t/t1517-outside-repo.sh: do
+ archimport | citool | credential-netrc | credential-libsecret | \
+ credential-osxkeychain | cvsexportcommit | cvsimport | cvsserver | \
+ daemon | \
+- difftool--helper | filter-branch | fsck-objects | get-tar-commit-id | \
++ difftool--helper | filter-branch | format-rev | fsck-objects | \
++ get-tar-commit-id | \
+ gui | gui--askpass | \
+ http-backend | http-fetch | http-push | init-db | \
+ merge-octopus | merge-one-file | merge-resolve | mergetool | \
## t/t6120-describe.sh ##
-@@ t/t6120-describe.sh: test_expect_success 'name-rev --annotate-stdin works with commitGraph' '
- )
+@@ t/t6120-describe.sh: test_expect_success 'do not be fooled by invalid describe format ' '
+ test_must_fail git cat-file -t "refs/tags/super-invalid/./../...../ ~^:/?*[////\\\\\\&}/busted.lock-42-g"$(cat out)
'
+test_expect_success 'name-rev --format setup' '
@@ t/t6120-describe.sh: test_expect_success 'name-rev --annotate-stdin works with c
+ test_commit -C repo-format eighth
+'
+
-+test_expect_success 'name-rev --format --no-name-only' '
-+ cat >expect <<-\EOF &&
-+ HEAD~3 [fifth]
-+ HEAD [eighth]
-+ HEAD~5 [third]
-+ EOF
-+ git -C repo-format name-rev --format="[%s]" \
-+ --no-name-only HEAD~3 HEAD HEAD~5 >actual &&
-+ test_cmp expect actual
-+'
-+
-+test_expect_success 'name-rev --format --no-format is the same as regular name-rev' '
-+ git -C repo-format name-rev HEAD~2 HEAD~3 >expect &&
-+ test_file_not_empty expect &&
-+ git -C repo-format name-rev --format="huh?" \
-+ --no-format HEAD~2 HEAD~3 >actual &&
-+ test_cmp expect actual
-+'
-+
-+test_expect_success 'name-rev --format=%s for argument revs' '
++test_expect_success 'format-rev --stdin-mode=revs' '
+ cat >expect <<-\EOF &&
+ eighth
+ seventh
+ fifth
+ EOF
-+ git -C repo-format name-rev --format=%s \
-+ HEAD HEAD~ HEAD~3 >actual &&
++ git -C repo-format format-rev --stdin-mode=revs \
++ --format=%s >actual <<-\EOF &&
++ HEAD
++ HEAD~
++ HEAD~3
++ EOF
+ test_cmp expect actual
+'
+
-+test_expect_success '--name-rev --format=reference --annotate-stdin from rev-list same as log' '
++test_expect_success 'format-rev --stdin-mode=text from rev-list same as log' '
+ git -C repo-format log --format=reference >expect &&
+ test_file_not_empty expect &&
+ git -C repo-format rev-list HEAD >list &&
-+ git -C repo-format name-rev --format=reference \
-+ --annotate-stdin <list >actual &&
++ git -C repo-format format-rev --stdin-mode=text \
++ --format=reference <list >actual &&
+ test_cmp expect actual
+'
+
-+test_expect_success '--name-rev --format=<pretty> --annotate-stdin with running text and tree oid' '
++test_expect_success 'format-rev --stdin-mode=text with running text and tree oid' '
+ cmit_oid=$(git -C repo-format rev-parse :/fifth) &&
+ reference=$(git -C repo-format log -n1 --format=reference :/fifth) &&
+ tree=$(git -C repo-format rev-parse HEAD^{tree}) &&
@@ t/t6120-describe.sh: test_expect_success 'name-rev --annotate-stdin works with c
+ We thought we fixed this in ${reference}.
+ But look at this tree: ${tree}.
+ EOF
-+ git -C repo-format name-rev --format=reference --annotate-stdin \
++ git -C repo-format format-rev --stdin-mode=text --format=reference \
+ >actual <<-EOF &&
+ We thought we fixed this in ${cmit_oid}.
+ But look at this tree: ${tree}.
@@ t/t6120-describe.sh: test_expect_success 'name-rev --annotate-stdin works with c
+ test_cmp expect actual
+'
+
-+test_expect_success 'name-rev --format=<pretty> with %N (note)' '
++test_expect_success 'format-rev with %N (note)' '
+ test_when_finished "git -C repo-format notes remove" &&
+ git -C repo-format notes add -m"Make a note" &&
+ printf "Make a note\n\n\n" >expect &&
-+ git -C repo-format name-rev --format="tformat:%N" \
-+ HEAD HEAD~ >actual &&
++ git -C repo-format format-rev --stdin-mode=revs \
++ --format="tformat:%N" \
++ >actual <<-\EOF &&
++ HEAD
++ HEAD~
++ EOF
+ test_cmp expect actual
+'
+
-+test_expect_success 'name-rev --format=<pretty> --notes<ref>' '
++test_expect_success 'format-rev --notes<ref> (custom notes ref)' '
+ # One custom notes ref
+ test_when_finished "git -C repo-format notes remove" &&
+ test_when_finished "git -C repo-format notes --ref=word remove" &&
+ git -C repo-format notes add -m"default" &&
+ git -C repo-format notes --ref=word add -m"custom" &&
+ printf "custom\n\n" >expect &&
-+ git -C repo-format name-rev --format="tformat:%N" \
++ git -C repo-format format-rev --stdin-mode=revs \
++ --format="tformat:%N" \
+ --notes=word \
-+ HEAD >actual &&
++ >actual <<-\EOF &&
++ HEAD
++ EOF
+ test_cmp expect actual &&
+ # Glob all
+ printf "default\ncustom\n\n" >expect &&
-+ git -C repo-format name-rev --format="tformat:%N" \
-+ --notes=* \
-+ HEAD >actual &&
++ git -C repo-format format-rev --stdin-mode=revs \
++ --format="tformat:%N" \
++ --notes=* >actual <<-\EOF &&
++ HEAD
++ EOF
++ test_cmp expect actual
++'
++
++test_expect_success 'format-rev --stdin-mode=revs on annotated tag peels to commit' '
++ test_when_finished "git -C repo-format tag -d version" &&
++ git -C repo-format tag -a -m"new version" version &&
++ cat >expect <<-\EOF &&
++ eighth
++ EOF
++ git -C repo-format format-rev --stdin-mode=revs \
++ --format=%s \
++ --notes=* >actual <<-\EOF &&
++ version
++ EOF
+ test_cmp expect actual
+'
+
- # B
- # o
- # H \
+ test_done
base-commit: 67006b9db8b772423ad0706029286096307d2567
--
2.54.0.13.g9c7419e39f8
^ permalink raw reply related
* [PATCH v3 1/5] name-rev: wrap both blocks in braces
From: kristofferhaugsbakk @ 2026-04-28 22:25 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk, ben.knoble
In-Reply-To: <V3_CV_format-rev.66a@msgid.xyz>
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
See `CodingGuidelines`:
- When there are multiple arms to a conditional and some of them
require braces, enclose even a single line block in braces for
consistency. [...]
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
Notes (series):
v2:
Fix stray formatting of `(p+1)`
builtin/name-rev.c | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 6188cf98ce0..171e7bd0e98 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -466,9 +466,9 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
if (!n)
return NULL;
- if (!n->generation)
+ if (!n->generation) {
return n->tip_name;
- else {
+ } else {
strbuf_reset(buf);
strbuf_addstr(buf, n->tip_name);
strbuf_strip_suffix(buf, "^0");
@@ -516,9 +516,9 @@ static void name_rev_line(char *p, struct name_ref_data *data)
for (p_start = p; *p; p++) {
#define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))
- if (!ishex(*p))
+ if (!ishex(*p)) {
counter = 0;
- else if (++counter == hexsz &&
+ } else if (++counter == hexsz &&
!ishex(*(p+1))) {
struct object_id oid;
const char *name = NULL;
--
2.54.0.13.g9c7419e39f8
^ permalink raw reply related
* [PATCH v3 2/5] name-rev: run clang-format before factoring code
From: kristofferhaugsbakk @ 2026-04-28 22:25 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk, ben.knoble
In-Reply-To: <V3_CV_format-rev.66a@msgid.xyz>
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
We are about to move code around to prepare for adding a new
command. Let’s deal with clang-format changes first in the affected
areas.
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
builtin/name-rev.c | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 171e7bd0e98..6357eaa76d0 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -519,22 +519,22 @@ static void name_rev_line(char *p, struct name_ref_data *data)
if (!ishex(*p)) {
counter = 0;
} else if (++counter == hexsz &&
- !ishex(*(p+1))) {
+ !ishex(*(p + 1))) {
struct object_id oid;
const char *name = NULL;
- char c = *(p+1);
+ char c = *(p + 1);
int p_len = p - p_start + 1;
counter = 0;
- *(p+1) = 0;
+ *(p + 1) = 0;
if (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {
struct object *o =
lookup_object(the_repository, &oid);
if (o)
name = get_rev_name(o, &buf);
}
- *(p+1) = c;
+ *(p + 1) = c;
if (!name)
continue;
@@ -571,9 +571,9 @@ int cmd_name_rev(int argc,
OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")),
OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
OPT_STRING_LIST(0, "refs", &data.ref_filters, N_("pattern"),
- N_("only use refs matching <pattern>")),
+ N_("only use refs matching <pattern>")),
OPT_STRING_LIST(0, "exclude", &data.exclude_filters, N_("pattern"),
- N_("ignore refs matching <pattern>")),
+ N_("ignore refs matching <pattern>")),
OPT_GROUP(""),
OPT_BOOL(0, "all", &all, N_("list all commits reachable from all refs")),
#ifndef WITH_BREAKING_CHANGES
@@ -585,10 +585,10 @@ int cmd_name_rev(int argc,
#endif /* WITH_BREAKING_CHANGES */
OPT_BOOL(0, "annotate-stdin", &annotate_stdin, N_("annotate text from stdin")),
OPT_BOOL(0, "undefined", &allow_undefined, N_("allow to print `undefined` names (default)")),
- OPT_BOOL(0, "always", &always,
- N_("show abbreviated commit object as fallback")),
+ OPT_BOOL(0, "always", &always,
+ N_("show abbreviated commit object as fallback")),
OPT_HIDDEN_BOOL(0, "peel-tag", &peel_tag,
- N_("dereference tags in the input (internal use)")),
+ N_("dereference tags in the input (internal use)")),
OPT_END(),
};
--
2.54.0.13.g9c7419e39f8
^ permalink raw reply related
* [PATCH v3 3/5] name-rev: factor code for sharing with a new command
From: kristofferhaugsbakk @ 2026-04-28 22:25 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk, ben.knoble
In-Reply-To: <V3_CV_format-rev.66a@msgid.xyz>
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
We are about to introduce a new command git-format-rev(1) to this
file. Let’s factor some code so that we can share it with the new
command.
We want to be able to format commits found in freeform text, and
git-name-rev(1) already has a function for that but for symbolic
names. Let’s use a tagged union for the command-specific payload.
No functional changes.
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
builtin/name-rev.c | 54 +++++++++++++++++++++++++++++++++++-----------
1 file changed, 41 insertions(+), 13 deletions(-)
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 6357eaa76d0..dc4136f4de3 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -272,6 +272,24 @@ struct name_ref_data {
struct string_list exclude_filters;
};
+enum command_type {
+ NAME_REV = 1,
+};
+
+struct command {
+ enum command_type type;
+ union {
+ int name_only;
+ } u;
+};
+
+static void init_name_rev_command(struct command *cmd,
+ int name_only)
+{
+ cmd->type = NAME_REV;
+ cmd->u.name_only = name_only;
+}
+
static struct tip_table {
struct tip_table_entry {
struct object_id oid;
@@ -507,7 +525,7 @@ static char const * const name_rev_usage[] = {
NULL
};
-static void name_rev_line(char *p, struct name_ref_data *data)
+static void name_rev_line(char *p, struct command *cmd)
{
struct strbuf buf = STRBUF_INIT;
int counter = 0;
@@ -516,6 +534,7 @@ static void name_rev_line(char *p, struct name_ref_data *data)
for (p_start = p; *p; p++) {
#define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))
+ start:
if (!ishex(*p)) {
counter = 0;
} else if (++counter == hexsz &&
@@ -524,25 +543,32 @@ static void name_rev_line(char *p, struct name_ref_data *data)
const char *name = NULL;
char c = *(p + 1);
int p_len = p - p_start + 1;
+ struct object *o = NULL;
+ int oid_ret = 1;
counter = 0;
*(p + 1) = 0;
- if (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {
- struct object *o =
- lookup_object(the_repository, &oid);
+ oid_ret = repo_get_oid(the_repository, p - (hexsz - 1), &oid);
+
+ switch (cmd->type) {
+ case NAME_REV:
+ if (!oid_ret)
+ o = lookup_object(the_repository, &oid);
if (o)
name = get_rev_name(o, &buf);
+ *(p + 1) = c;
+ if (!name)
+ goto start;
+ if (cmd->u.name_only)
+ printf("%.*s%s", p_len - hexsz, p_start, name);
+ else
+ printf("%.*s (%s)", p_len, p_start, name);
+ break;
+ default:
+ BUG("uncovered case: %d", cmd->type);
}
- *(p + 1) = c;
-
- if (!name)
- continue;
- if (data->name_only)
- printf("%.*s%s", p_len - hexsz, p_start, name);
- else
- printf("%.*s (%s)", p_len, p_start, name);
p_start = p + 1;
}
}
@@ -567,6 +593,7 @@ int cmd_name_rev(int argc,
#endif
int all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;
struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };
+ struct command cmd;
struct option opts[] = {
OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")),
OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
@@ -596,6 +623,7 @@ int cmd_name_rev(int argc,
init_commit_rev_name(&rev_names);
repo_config(the_repository, git_default_config, NULL);
argc = parse_options(argc, argv, prefix, opts, name_rev_usage, 0);
+ init_name_rev_command(&cmd, data.name_only);
#ifndef WITH_BREAKING_CHANGES
if (transform_stdin) {
@@ -663,7 +691,7 @@ int cmd_name_rev(int argc,
while (strbuf_getline(&sb, stdin) != EOF) {
strbuf_addch(&sb, '\n');
- name_rev_line(sb.buf, &data);
+ name_rev_line(sb.buf, &cmd);
}
strbuf_release(&sb);
} else if (all) {
--
2.54.0.13.g9c7419e39f8
^ permalink raw reply related
* [PATCH v3 4/5] name-rev: make dedicated --annotate-stdin --name-only test
From: kristofferhaugsbakk @ 2026-04-28 22:25 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk, ben.knoble
In-Reply-To: <V3_CV_format-rev.66a@msgid.xyz>
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
The previous commit split the `--name-only` handling:
1. `--annotate-stdin`: uses the new `struct command`
2. The rest: uses `struct name_ref_data`
But there is no dedicated test for the option combination in (1). That
means that the following tests will fail if you neglect to set
`command.u.name_only` properly:
name-rev --annotate-stdin works with commitGraph
name-rev --annotate-stdin works with non-monotonic timestamps
even though it has nothing to do with what these tests are supposed
to test.
Let’s add another regression test now that it is relevant.
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
t/t6120-describe.sh | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
index 2c70cc561ad..62789f76381 100755
--- a/t/t6120-describe.sh
+++ b/t/t6120-describe.sh
@@ -298,6 +298,20 @@ test_expect_success 'name-rev --annotate-stdin' '
test_cmp expect actual
'
+test_expect_success 'name-rev --annotate-stdin --name-only' '
+ >expect.unsorted &&
+ for rev in $(git rev-list --all)
+ do
+ name=$(git name-rev --name-only $rev) &&
+ echo "$name" >>expect.unsorted || return 1
+ done &&
+ sort <expect.unsorted >expect &&
+ git name-rev --annotate-stdin --name-only \
+ <list >actual.unsorted &&
+ sort <actual.unsorted >actual &&
+ test_cmp expect actual
+'
+
test_expect_success 'name-rev --stdin deprecated' '
git rev-list --all >list &&
if ! test_have_prereq WITH_BREAKING_CHANGES
--
2.54.0.13.g9c7419e39f8
^ permalink raw reply related
* [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting
From: kristofferhaugsbakk @ 2026-04-28 22:25 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk, ben.knoble
In-Reply-To: <V3_CV_format-rev.66a@msgid.xyz>
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
Introduce a new builtin for pretty formatting one revision expression
per line or commit object names found in running text.
Sometimes you want to format commits. Most of the time you’re
walking the graph, e.g. getting a range of commits like
`master..topic`. That’s a job for git-log(1).
But there are times when you want to format commits that you encounter
on demand:
• Full hashes in running text that you might want to pretty-print
• git-last-modified(1) outputs full hashes that you can do the same
with
• git-cherry(1) has `-v` for commit subject, but maybe you want
something else?
But now you can’t use git-log(1), git-show(1), or git-rev-list(1):
• You can’t feed commits piecemeal to these commands, one input
for one output; they block until standard in is closed
• You can’t feed a list of possibly duplicate commits, like the output
of git-last-modified(1); they effectively deduplicate the output
Beyond these two points there’s also the input massage problem: you
cannot feed mixed input (revisions mixed with arbitrary text).
One might hope that git-cat-file(1) can save us. But it doesn’t
support pretty formats.
But there is one command that already both handles revisions as
arguments, revisions on standard input, and even revisions mixed in
with arbitrary text. Namely git-name-rev(1): the command for outputting
symbolic names for commits.
We made some room in `builtin/name-rev.c` two commits ago. Let’s
now add this new git-format-rev(1) command. Taking inspiration from
git-name-rev(1), there are two modes:
• revs: like git-name-rev(1) in argv mode, but one revision per line
on standard in
• text: like git-name-rev(1) with `--annotate-stdin`
***
We need to add this command to the exception list in
`t/t1517-outside-repo.sh` because it uses “EXPERIMENTAL!”
in the usage line.
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
Notes (series):
v3:
• And don’t forget to document --notes this time
https://lore.kernel.org/git/CALnO6CB5WOTp_e7Kv3CrEbQ+3XE-gDxNVHf7qATBEbyKWfxpLg@mail.gmail.com/
Documentation/git-format-rev.adoc | 148 ++++++++++++++++++++++++
Makefile | 1 +
builtin.h | 1 +
builtin/name-rev.c | 186 ++++++++++++++++++++++++++++++
command-list.txt | 1 +
git.c | 1 +
t/t1517-outside-repo.sh | 3 +-
t/t6120-describe.sh | 104 +++++++++++++++++
8 files changed, 444 insertions(+), 1 deletion(-)
create mode 100644 Documentation/git-format-rev.adoc
diff --git a/Documentation/git-format-rev.adoc b/Documentation/git-format-rev.adoc
new file mode 100644
index 00000000000..d960001d750
--- /dev/null
+++ b/Documentation/git-format-rev.adoc
@@ -0,0 +1,148 @@
+git-format-rev(1)
+=================
+
+NAME
+----
+git-format-rev - EXPERIMENTAL: Pretty format revisions on demand
+
+
+SYNOPSIS
+--------
+[synopsis]
+(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]
+
+DESCRIPTION
+-----------
+
+Pretty format revisions from standard input.
+
+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
+
+OPTIONS
+-------
+
+`--stdin-mode=<mode>`::
+ How to interpret standard input data:
++
+--
+`revs`:: Each line is interpreted as a commit. Any kind of revision
+ expression can be used (see linkgit:gitrevisions[7]). Annotated
+ tags are peeled (see linkgit:gitglossary[7]).
++
+The argument `rev` is also accepted.
+`text`:: Formats all commit object names found in freeform text. These
+ must the full object names, i.e. abbreviated hexidecimal object
+ names will not be interpreted.
+--
+
+`--format=<pretty>`::
+ Pretty format string.
+
+`--notes=<ref>`::
+`--no-notes`::
+ Custom notes ref. Notes are displayed when using the `%N`
+ atom. See linkgit:git-notes[1].
+
+EXAMPLES
+--------
+
+The command linkgit:git-last-modified[1] shows the commit that each file
+was last modified in.
+
+----
+$ git last-modified -- README.md Makefile
+7798034171030be0909c56377a4e0e10e6d2df93 Makefile
+c50fbb2dd225e7e82abba4380423ae105089f4d7 README.md
+----
+
+We can pipe the result to this command in order to replace the object
+name with the commit author.
+
+----
+$ git last-modified -- README.md Makefile |
+ git format-rev --stdin-mode=text --format=%an
+Junio C Hamano Makefile
+Todd Zullinger README.md
+----
+
+Another example is _formatting commits in commit messages_. Given this commit message:
+
+----
+Fix off-by-one error
+
+Fix off-by-one error introduced in
+e83c5163316f89bfbde7d9ab23ca2e25604af290.
+
+We thought we fixed this in 5569bf9bbedd63a00780fc5c110e0cfab3aa97b9 but
+that only covered 1/3 of the faulty cases.
+----
+
+We can format the commits and use par(1) to reflow the text, say in a
+`commit-msg` hook:
+
+----
+$ git config set hook.reference-commits.event commit-msg
+$ git config set hook.reference-commits.command reference-commits
+$ cat $(which reference-commits)
+#/bin/sh
+
+msg="$1"
+rewritten=$(mktemp)
+git format-rev --stdin-mode=text --format=reference <"$msg" |
+ par >"$rewritten"
+mv "$rewritten" "$msg"
+----
+
+Which will produce something like this:
+
+----
+Fix off-by-one error
+
+Fix off-by-one error introduced in e83c5163316 (Implement better memory
+allocator, 2005-04-07).
+
+We thought we fixed this in 5569bf9bbed (Fix memory allocator,
+2005-06-22) but that only covered 1/3 of the faulty cases.
+----
+
+DISCUSSION
+----------
+
+This command lets you format any number of revisions in any order
+through one command invocation. Consider the
+linkgit:git-last-modified[1] case from the "EXAMPLES" section above:
+
+1. There might be hundreds of files
+2. Commits can be repeated, i.e. two or more files were last modified in
+ the same commit
+
+Two widely-used commands which pretty formats commits are
+linkgit:git-log[1] and linkgit:git-show[1]. It turns out that they are
+not a good fit for the above use case.
+
+- The output of linkgit:git-last-modified[1] would have to be processed
+ in stages since you need to transform the first column separately and
+ then link the author to the filename. But this is surmountable.
+- You can feed each commit to `git show` or `git show --no-walk -1`. But
+ that means that you need to create a process for each line.
+- Let’s say that you want to use one process, not one per line. So you
+ want to feed all the commits to the command. Now you face the problem
+ that you have to feed all the commits to the commands before you get
+ any output (this is also the case for the `--stdin` modes). In other
+ words, you cannot loop through each line, get the author for the
+ commit, and output the author and the filename. You need to feed all
+ the commits, get back all the output, and match the output with the
+ filename.
+- But the next problem is that commands will deduplicate the input and
+ only output one commit one single time only. Thus you cannot make the
+ output order match the input order, since a commit could have been
+ repeated in the original input.
+
+In short, it is straightforward to use these two commands if you use one
+process per line. It is much more work if you just want to use one
+process, but still doable. In contrast, this problem is just another
+shell pipeline with this command.
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Makefile b/Makefile
index 15b1ded1a0b..cbaf91fd846 100644
--- a/Makefile
+++ b/Makefile
@@ -895,6 +895,7 @@ BUILT_INS += $(patsubst builtin/%.o,git-%$X,$(BUILTIN_OBJS))
BUILT_INS += git-cherry$X
BUILT_INS += git-cherry-pick$X
BUILT_INS += git-format-patch$X
+BUILT_INS += git-format-rev$X
BUILT_INS += git-fsck-objects$X
BUILT_INS += git-init$X
BUILT_INS += git-maintenance$X
diff --git a/builtin.h b/builtin.h
index 235c51f30e5..63813c90125 100644
--- a/builtin.h
+++ b/builtin.h
@@ -189,6 +189,7 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix, struct re
int cmd_for_each_ref(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_for_each_repo(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_format_patch(int argc, const char **argv, const char *prefix, struct repository *repo);
+int cmd_format_rev(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_fsck(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_gc(int argc, const char **argv, const char *prefix, struct repository *repo);
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index dc4136f4de3..b60cc766279 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -18,6 +18,9 @@
#include "commit-graph.h"
#include "wildmatch.h"
#include "mem-pool.h"
+#include "pretty.h"
+#include "revision.h"
+#include "notes.h"
/*
* One day. See the 'name a rev shortly after epoch' test in t6120 when
@@ -272,14 +275,26 @@ struct name_ref_data {
struct string_list exclude_filters;
};
+struct pretty_format {
+ struct pretty_print_context ctx;
+ struct userformat_want want;
+};
+
enum command_type {
NAME_REV = 1,
+ FORMAT_REV = 2,
+};
+
+enum stdin_mode {
+ TEXT = 1,
+ REVS = 2,
};
struct command {
enum command_type type;
union {
int name_only;
+ struct pretty_format *pretty_format;
} u;
};
@@ -290,6 +305,13 @@ static void init_name_rev_command(struct command *cmd,
cmd->u.name_only = name_only;
}
+static void init_format_rev_command(struct command *cmd,
+ struct pretty_format *pretty_format)
+{
+ cmd->type = FORMAT_REV;
+ cmd->u.pretty_format = pretty_format;
+}
+
static struct tip_table {
struct tip_table_entry {
struct object_id oid;
@@ -495,6 +517,27 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
}
}
+static const char *get_format_rev(const struct commit *c,
+ struct pretty_format *format_ctx,
+ struct strbuf *buf)
+{
+ strbuf_reset(buf);
+
+ if (format_ctx->want.notes) {
+ struct strbuf notebuf = STRBUF_INIT;
+
+ format_display_notes(&c->object.oid, ¬ebuf,
+ get_log_output_encoding(),
+ format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);
+ format_ctx->ctx.notes_message = strbuf_detach(¬ebuf, NULL);
+ }
+
+ pretty_print_commit(&format_ctx->ctx, c, buf);
+ FREE_AND_NULL(format_ctx->ctx.notes_message);
+
+ return buf->buf;
+}
+
static void show_name(const struct object *obj,
const char *caller_name,
int always, int allow_undefined, int name_only)
@@ -565,6 +608,19 @@ static void name_rev_line(char *p, struct command *cmd)
else
printf("%.*s (%s)", p_len, p_start, name);
break;
+ case FORMAT_REV:
+ if (!oid_ret)
+ o = parse_object(the_repository, &oid);
+ if (o && o->type == OBJ_COMMIT)
+ name = get_format_rev((const struct commit *)o,
+ cmd->u.pretty_format,
+ &buf);
+ *(p + 1) = c;
+ if (name)
+ printf("%.*s%s", p_len - hexsz, p_start, name);
+ else
+ printf("%.*s", p_len, p_start);
+ break;
default:
BUG("uncovered case: %d", cmd->type);
}
@@ -718,3 +774,133 @@ int cmd_name_rev(int argc,
object_array_clear(&revs);
return 0;
}
+
+static enum stdin_mode parse_stdin_mode(const char *stdin_mode)
+{
+ if (!strcmp(stdin_mode, "text"))
+ return TEXT;
+ else if (!strcmp(stdin_mode, "revs") ||
+ !strcmp(stdin_mode, "rev"))
+ return REVS;
+ else
+ die(_("'%s' needs to be either text, revs, or rev"),
+ "--stdin-mode");
+}
+
+static char const *const format_rev_usage[] = {
+ N_("(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]"),
+ NULL
+};
+
+int cmd_format_rev(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo UNUSED)
+{
+ const char *format = NULL;
+ enum stdin_mode stdin_mode;
+ const char *stdin_mode_arg = NULL;
+ struct display_notes_opt format_notes_opt;
+ struct rev_info format_rev = REV_INFO_INIT;
+ struct pretty_format format_pp = { 0 };
+ struct string_list notes = STRING_LIST_INIT_NODUP;
+ struct strbuf scratch_buf = STRBUF_INIT;
+ struct command cmd;
+ struct option opts[] = {
+ OPT_STRING(0, "format", &format, N_("format"),
+ N_("pretty format to use")),
+ OPT_STRING(0, "stdin-mode", &stdin_mode_arg, N_("stdin-mode"),
+ N_("how revs are processed")),
+ OPT_STRING_LIST(0, "notes", ¬es, N_("notes"),
+ N_("display notes for pretty format")),
+ OPT_END(),
+ };
+
+ argc = parse_options(argc, argv, prefix, opts, format_rev_usage, 0);
+
+ if (argc > 0) {
+ error(_("too many arguments"));
+ usage_with_options(format_rev_usage, opts);
+ }
+
+ if (!format)
+ die(_("'%s' is required"), "--format");
+ if (!stdin_mode_arg)
+ die(_("'%s' is required"), "--stdin-mode");
+
+ init_display_notes(&format_notes_opt);
+ stdin_mode = parse_stdin_mode(stdin_mode_arg);
+
+ get_commit_format(format, &format_rev);
+ format_pp.ctx.rev = &format_rev;
+ format_pp.ctx.fmt = format_rev.commit_format;
+ format_pp.ctx.abbrev = format_rev.abbrev;
+ format_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;
+ format_pp.ctx.date_mode = format_rev.date_mode;
+ format_pp.ctx.color = GIT_COLOR_AUTO;
+
+ userformat_find_requirements(format,
+ &format_pp.want);
+ if (format_pp.want.notes) {
+ int ignore_show_notes = 0;
+ struct string_list_item *n;
+
+ for_each_string_list_item(n, ¬es)
+ enable_ref_display_notes(&format_notes_opt,
+ &ignore_show_notes,
+ n->string);
+ load_display_notes(&format_notes_opt);
+ }
+
+ init_format_rev_command(&cmd, &format_pp);
+
+ switch (stdin_mode) {
+ case TEXT:
+ while (strbuf_getline(&scratch_buf, stdin) != EOF) {
+ strbuf_addch(&scratch_buf, '\n');
+ name_rev_line(scratch_buf.buf, &cmd);
+ }
+ break;
+ case REVS:
+ while (strbuf_getline(&scratch_buf, stdin) != EOF) {
+ struct object_id oid;
+ struct object *object;
+ struct object *peeled;
+ struct commit *commit;
+
+ if (repo_get_oid(the_repository, scratch_buf.buf, &oid)) {
+ fprintf(stderr, "Could not get sha1 for %s. Skipping.\n",
+ scratch_buf.buf);
+ continue;
+ }
+
+ object = parse_object(the_repository, &oid);
+ if (!object) {
+ fprintf(stderr, "Could not get object for %s. Skipping.\n",
+ scratch_buf.buf);
+ continue;
+ }
+
+ peeled = deref_tag(the_repository, object, scratch_buf.buf, 0);
+ if (peeled && peeled->type == OBJ_COMMIT)
+ commit = (struct commit *)peeled;
+ if (!commit) {
+ fprintf(stderr, "Could not get commit for %s. Skipping.\n",
+ *argv);
+ continue;
+ }
+
+ get_format_rev(commit, &format_pp, &scratch_buf);
+ printf("%s\n", scratch_buf.buf);
+ strbuf_release(&scratch_buf);
+ }
+ break;
+ default:
+ BUG("uncovered case: %d", stdin_mode);
+ }
+
+ strbuf_release(&scratch_buf);
+ string_list_clear(¬es, 0);
+ release_display_notes(&format_notes_opt);
+ return 0;
+}
diff --git a/command-list.txt b/command-list.txt
index f9005cf4597..df729872dca 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -108,6 +108,7 @@ git-fmt-merge-msg purehelpers
git-for-each-ref plumbinginterrogators
git-for-each-repo plumbinginterrogators
git-format-patch mainporcelain
+git-format-rev plumbinginterrogators
git-fsck ancillaryinterrogators complete
git-gc mainporcelain
git-get-tar-commit-id plumbinginterrogators
diff --git a/git.c b/git.c
index 2b212e6675d..af5b0422b00 100644
--- a/git.c
+++ b/git.c
@@ -578,6 +578,7 @@ static struct cmd_struct commands[] = {
{ "for-each-ref", cmd_for_each_ref, RUN_SETUP },
{ "for-each-repo", cmd_for_each_repo, RUN_SETUP_GENTLY },
{ "format-patch", cmd_format_patch, RUN_SETUP },
+ { "format-rev", cmd_format_rev, RUN_SETUP },
{ "fsck", cmd_fsck, RUN_SETUP },
{ "fsck-objects", cmd_fsck, RUN_SETUP },
{ "fsmonitor--daemon", cmd_fsmonitor__daemon, RUN_SETUP },
diff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh
index c824c1a25cf..360a9323343 100755
--- a/t/t1517-outside-repo.sh
+++ b/t/t1517-outside-repo.sh
@@ -114,7 +114,8 @@ do
archimport | citool | credential-netrc | credential-libsecret | \
credential-osxkeychain | cvsexportcommit | cvsimport | cvsserver | \
daemon | \
- difftool--helper | filter-branch | fsck-objects | get-tar-commit-id | \
+ difftool--helper | filter-branch | format-rev | fsck-objects | \
+ get-tar-commit-id | \
gui | gui--askpass | \
http-backend | http-fetch | http-push | init-db | \
merge-octopus | merge-one-file | merge-resolve | mergetool | \
diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
index 62789f76381..725f7d81b6b 100755
--- a/t/t6120-describe.sh
+++ b/t/t6120-describe.sh
@@ -801,4 +801,108 @@ test_expect_success 'do not be fooled by invalid describe format ' '
test_must_fail git cat-file -t "refs/tags/super-invalid/./../...../ ~^:/?*[////\\\\\\&}/busted.lock-42-g"$(cat out)
'
+test_expect_success 'name-rev --format setup' '
+ mkdir repo-format &&
+ git -C repo-format init &&
+ test_commit -C repo-format first &&
+ test_commit -C repo-format second &&
+ test_commit -C repo-format third &&
+ test_commit -C repo-format fourth &&
+ test_commit -C repo-format fifth &&
+ test_commit -C repo-format sixth &&
+ test_commit -C repo-format seventh &&
+ test_commit -C repo-format eighth
+'
+
+test_expect_success 'format-rev --stdin-mode=revs' '
+ cat >expect <<-\EOF &&
+ eighth
+ seventh
+ fifth
+ EOF
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format=%s >actual <<-\EOF &&
+ HEAD
+ HEAD~
+ HEAD~3
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=text from rev-list same as log' '
+ git -C repo-format log --format=reference >expect &&
+ test_file_not_empty expect &&
+ git -C repo-format rev-list HEAD >list &&
+ git -C repo-format format-rev --stdin-mode=text \
+ --format=reference <list >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=text with running text and tree oid' '
+ cmit_oid=$(git -C repo-format rev-parse :/fifth) &&
+ reference=$(git -C repo-format log -n1 --format=reference :/fifth) &&
+ tree=$(git -C repo-format rev-parse HEAD^{tree}) &&
+ cat >expect <<-EOF &&
+ We thought we fixed this in ${reference}.
+ But look at this tree: ${tree}.
+ EOF
+ git -C repo-format format-rev --stdin-mode=text --format=reference \
+ >actual <<-EOF &&
+ We thought we fixed this in ${cmit_oid}.
+ But look at this tree: ${tree}.
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'format-rev with %N (note)' '
+ test_when_finished "git -C repo-format notes remove" &&
+ git -C repo-format notes add -m"Make a note" &&
+ printf "Make a note\n\n\n" >expect &&
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format="tformat:%N" \
+ >actual <<-\EOF &&
+ HEAD
+ HEAD~
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'format-rev --notes<ref> (custom notes ref)' '
+ # One custom notes ref
+ test_when_finished "git -C repo-format notes remove" &&
+ test_when_finished "git -C repo-format notes --ref=word remove" &&
+ git -C repo-format notes add -m"default" &&
+ git -C repo-format notes --ref=word add -m"custom" &&
+ printf "custom\n\n" >expect &&
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format="tformat:%N" \
+ --notes=word \
+ >actual <<-\EOF &&
+ HEAD
+ EOF
+ test_cmp expect actual &&
+ # Glob all
+ printf "default\ncustom\n\n" >expect &&
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format="tformat:%N" \
+ --notes=* >actual <<-\EOF &&
+ HEAD
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=revs on annotated tag peels to commit' '
+ test_when_finished "git -C repo-format tag -d version" &&
+ git -C repo-format tag -a -m"new version" version &&
+ cat >expect <<-\EOF &&
+ eighth
+ EOF
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format=%s \
+ --notes=* >actual <<-\EOF &&
+ version
+ EOF
+ test_cmp expect actual
+'
+
test_done
--
2.54.0.13.g9c7419e39f8
^ permalink raw reply related
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox