* [GSoC Patch v4 2/4] rev-parse: use append_formatted_path() for path formatting
From: K Jayatheerth @ 2026-06-15 4:51 UTC (permalink / raw)
To: git
Cc: a3205153416, gitster, jltobler, kumarayushjha123,
lucasseikioshiro, phillip.wood, sandals, kristofferhaugsbakk,
K Jayatheerth
In-Reply-To: <20260615045112.50686-1-jayatheerthkulkarni2005@gmail.com>
Now that path formatting logic lives in a shared helper, keeping a
duplicate implementation in rev-parse is unnecessary and risks the
two diverging over time.
Replace the local format_type and default_type enums and the
hand-rolled formatting logic with a call to append_formatted_path().
Introduce PATH_FORMAT_DEFAULT as the initial value of arg_path_format
so that per-path fallback behavior is resolved in print_path() rather
than leaked into the shared helper.
Mentored-by: Justin Tobler <jltobler@gmail.com>
Mentored-by: Lucas Seiki Oshiro <lucasseikioshiro@gmail.com>
Signed-off-by: K Jayatheerth <jayatheerthkulkarni2005@gmail.com>
---
builtin/rev-parse.c | 103 ++++++++++----------------------------------
1 file changed, 23 insertions(+), 80 deletions(-)
diff --git a/builtin/rev-parse.c b/builtin/rev-parse.c
index 218b5f34d6..2dd35361f3 100644
--- a/builtin/rev-parse.c
+++ b/builtin/rev-parse.c
@@ -632,73 +632,16 @@ static void handle_ref_opt(const char *pattern, const char *prefix)
clear_ref_exclusions(&ref_excludes);
}
-enum format_type {
- /* We would like a relative path. */
- FORMAT_RELATIVE,
- /* We would like a canonical absolute path. */
- FORMAT_CANONICAL,
- /* We would like the default behavior. */
- FORMAT_DEFAULT,
-};
-
-enum default_type {
- /* Our default is a relative path. */
- DEFAULT_RELATIVE,
- /* Our default is a relative path if there's a shared root. */
- DEFAULT_RELATIVE_IF_SHARED,
- /* Our default is a canonical absolute path. */
- DEFAULT_CANONICAL,
- /* Our default is not to modify the item. */
- DEFAULT_UNMODIFIED,
-};
-
-static void print_path(const char *path, const char *prefix, enum format_type format, enum default_type def)
+static void print_path(const char *path, const char *prefix,
+ enum path_format arg_path_format, enum path_format def_format)
{
- char *cwd = NULL;
- /*
- * We don't ever produce a relative path if prefix is NULL, so set the
- * prefix to the current directory so that we can produce a relative
- * path whenever possible. If we're using RELATIVE_IF_SHARED mode, then
- * we want an absolute path unless the two share a common prefix, so don't
- * set it in that case, since doing so causes a relative path to always
- * be produced if possible.
- */
- if (!prefix && (format != FORMAT_DEFAULT || def != DEFAULT_RELATIVE_IF_SHARED))
- prefix = cwd = xgetcwd();
- if (format == FORMAT_DEFAULT && def == DEFAULT_UNMODIFIED) {
- puts(path);
- } else if (format == FORMAT_RELATIVE ||
- (format == FORMAT_DEFAULT && def == DEFAULT_RELATIVE)) {
- /*
- * In order for relative_path to work as expected, we need to
- * make sure that both paths are absolute paths. If we don't,
- * we can end up with an unexpected absolute path that the user
- * didn't want.
- */
- struct strbuf buf = STRBUF_INIT, realbuf = STRBUF_INIT, prefixbuf = STRBUF_INIT;
- if (!is_absolute_path(path)) {
- strbuf_realpath_forgiving(&realbuf, path, 1);
- path = realbuf.buf;
- }
- if (!is_absolute_path(prefix)) {
- strbuf_realpath_forgiving(&prefixbuf, prefix, 1);
- prefix = prefixbuf.buf;
- }
- puts(relative_path(path, prefix, &buf));
- strbuf_release(&buf);
- strbuf_release(&realbuf);
- strbuf_release(&prefixbuf);
- } else if (format == FORMAT_DEFAULT && def == DEFAULT_RELATIVE_IF_SHARED) {
- struct strbuf buf = STRBUF_INIT;
- puts(relative_path(path, prefix, &buf));
- strbuf_release(&buf);
- } else {
- struct strbuf buf = STRBUF_INIT;
- strbuf_realpath_forgiving(&buf, path, 1);
- puts(buf.buf);
- strbuf_release(&buf);
- }
- free(cwd);
+ struct strbuf sb = STRBUF_INIT;
+ enum path_format fmt = (arg_path_format != PATH_FORMAT_DEFAULT) ? arg_path_format : def_format;
+
+ append_formatted_path(&sb, path, prefix, fmt);
+ puts(sb.buf);
+
+ strbuf_release(&sb);
}
int cmd_rev_parse(int argc,
@@ -717,7 +660,7 @@ int cmd_rev_parse(int argc,
const char *name = NULL;
struct strbuf buf = STRBUF_INIT;
int seen_end_of_options = 0;
- enum format_type format = FORMAT_DEFAULT;
+ enum path_format arg_path_format = PATH_FORMAT_DEFAULT;
show_usage_if_asked(argc, argv, builtin_rev_parse_usage);
@@ -797,8 +740,8 @@ int cmd_rev_parse(int argc,
die(_("--git-path requires an argument"));
print_path(repo_git_path_replace(the_repository, &buf,
"%s", argv[i + 1]), prefix,
- format,
- DEFAULT_RELATIVE_IF_SHARED);
+ arg_path_format,
+ PATH_FORMAT_RELATIVE_IF_SHARED);
i++;
continue;
}
@@ -820,9 +763,9 @@ int cmd_rev_parse(int argc,
if (!arg)
die(_("--path-format requires an argument"));
if (!strcmp(arg, "absolute")) {
- format = FORMAT_CANONICAL;
+ arg_path_format = PATH_FORMAT_CANONICAL;
} else if (!strcmp(arg, "relative")) {
- format = FORMAT_RELATIVE;
+ arg_path_format = PATH_FORMAT_RELATIVE;
} else {
die(_("unknown argument to --path-format: %s"), arg);
}
@@ -985,7 +928,7 @@ int cmd_rev_parse(int argc,
if (!strcmp(arg, "--show-toplevel")) {
const char *work_tree = repo_get_work_tree(the_repository);
if (work_tree)
- print_path(work_tree, prefix, format, DEFAULT_UNMODIFIED);
+ print_path(work_tree, prefix, arg_path_format, PATH_FORMAT_UNMODIFIED);
else
die(_("this operation must be run in a work tree"));
continue;
@@ -993,7 +936,7 @@ int cmd_rev_parse(int argc,
if (!strcmp(arg, "--show-superproject-working-tree")) {
struct strbuf superproject = STRBUF_INIT;
if (get_superproject_working_tree(&superproject))
- print_path(superproject.buf, prefix, format, DEFAULT_UNMODIFIED);
+ print_path(superproject.buf, prefix, arg_path_format, PATH_FORMAT_UNMODIFIED);
strbuf_release(&superproject);
continue;
}
@@ -1028,18 +971,18 @@ int cmd_rev_parse(int argc,
const char *gitdir = getenv(GIT_DIR_ENVIRONMENT);
char *cwd;
int len;
- enum format_type wanted = format;
+ enum path_format wanted = arg_path_format;
if (arg[2] == 'g') { /* --git-dir */
if (gitdir) {
- print_path(gitdir, prefix, format, DEFAULT_UNMODIFIED);
+ print_path(gitdir, prefix, arg_path_format, PATH_FORMAT_UNMODIFIED);
continue;
}
if (!prefix) {
- print_path(".git", prefix, format, DEFAULT_UNMODIFIED);
+ print_path(".git", prefix, arg_path_format, PATH_FORMAT_UNMODIFIED);
continue;
}
} else { /* --absolute-git-dir */
- wanted = FORMAT_CANONICAL;
+ wanted = PATH_FORMAT_CANONICAL;
if (!gitdir && !prefix)
gitdir = ".git";
if (gitdir) {
@@ -1055,11 +998,11 @@ int cmd_rev_parse(int argc,
strbuf_reset(&buf);
strbuf_addf(&buf, "%s%s.git", cwd, len && cwd[len-1] != '/' ? "/" : "");
free(cwd);
- print_path(buf.buf, prefix, wanted, DEFAULT_CANONICAL);
+ print_path(buf.buf, prefix, wanted, PATH_FORMAT_CANONICAL);
continue;
}
if (!strcmp(arg, "--git-common-dir")) {
- print_path(repo_get_common_dir(the_repository), prefix, format, DEFAULT_RELATIVE_IF_SHARED);
+ print_path(repo_get_common_dir(the_repository), prefix, arg_path_format, PATH_FORMAT_RELATIVE_IF_SHARED);
continue;
}
if (!strcmp(arg, "--is-inside-git-dir")) {
@@ -1089,7 +1032,7 @@ int cmd_rev_parse(int argc,
if (the_repository->index->split_index) {
const struct object_id *oid = &the_repository->index->split_index->base_oid;
const char *path = repo_git_path_replace(the_repository, &buf, "sharedindex.%s", oid_to_hex(oid));
- print_path(path, prefix, format, DEFAULT_RELATIVE);
+ print_path(path, prefix, arg_path_format, PATH_FORMAT_RELATIVE);
}
continue;
}
--
2.54.0
^ permalink raw reply related
* [GSoC Patch v4 3/4] repo: add path.commondir with absolute and relative suffix formatting
From: K Jayatheerth @ 2026-06-15 4:51 UTC (permalink / raw)
To: git
Cc: a3205153416, gitster, jltobler, kumarayushjha123,
lucasseikioshiro, phillip.wood, sandals, kristofferhaugsbakk,
K Jayatheerth
In-Reply-To: <20260615045112.50686-1-jayatheerthkulkarni2005@gmail.com>
Scripts working with worktree setups need a reliable way to discover
the common directory, which diverges from the git directory when
multiple worktrees are in use. There is no way to retrieve this path
from git repo info today.
Introduce path.commondir.absolute and path.commondir.relative keys.
Exposing explicit format variants rather than a single key with a
default avoids ambiguity for scripts that require predictable output.
Add a test helper test_repo_info_path that creates isolated
repositories per test case to prevent state leaks, captures the repo
root before changing directories to avoid eval, and accepts an optional
init_command to cover environment variable overrides such as
GIT_COMMON_DIR and GIT_DIR.
Mentored-by: Justin Tobler <jltobler@gmail.com>
Mentored-by: Lucas Seiki Oshiro <lucasseikioshiro@gmail.com>
Signed-off-by: K Jayatheerth <jayatheerthkulkarni2005@gmail.com>
---
Documentation/git-repo.adoc | 9 ++++++
builtin/repo.c | 26 ++++++++++++++++
t/t1900-repo-info.sh | 61 +++++++++++++++++++++++++++++++++++++
3 files changed, 96 insertions(+)
diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
index 42262c1983..890c34051d 100644
--- a/Documentation/git-repo.adoc
+++ b/Documentation/git-repo.adoc
@@ -104,6 +104,15 @@ values that they return:
`object.format`::
The object format (hash algorithm) used in the repository.
+`path.commondir.absolute`::
+ The canonical absolute path to the Git repository's common
+ directory (the shared `.git` directory containing objects,
+ refs, and global configuration).
+
+`path.commondir.relative`::
+ The path to the Git repository's common directory relative to
+ the current working directory.
+
`references.format`::
The reference storage format. The valid values are:
+
diff --git a/builtin/repo.c b/builtin/repo.c
index 71a5c1c29c..c4cc3bf3fc 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -7,12 +7,14 @@
#include "hex.h"
#include "odb.h"
#include "parse-options.h"
+#include "path.h"
#include "path-walk.h"
#include "progress.h"
#include "quote.h"
#include "ref-filter.h"
#include "refs.h"
#include "revision.h"
+#include "setup.h"
#include "strbuf.h"
#include "string-list.h"
#include "shallow.h"
@@ -75,6 +77,28 @@ static int get_object_format(struct repository *repo, struct strbuf *buf)
return 0;
}
+static int get_path_commondir_absolute(struct repository *repo, struct strbuf *buf)
+{
+ const char *common_dir = repo_get_common_dir(repo);
+
+ if (!common_dir)
+ return error(_("unable to get common directory"));
+
+ append_formatted_path(buf, common_dir, startup_info->prefix, PATH_FORMAT_CANONICAL);
+ return 0;
+}
+
+static int get_path_commondir_relative(struct repository *repo, struct strbuf *buf)
+{
+ const char *common_dir = repo_get_common_dir(repo);
+
+ if (!common_dir)
+ return error(_("unable to get common directory"));
+
+ append_formatted_path(buf, common_dir, startup_info->prefix, PATH_FORMAT_RELATIVE);
+ return 0;
+}
+
static int get_references_format(struct repository *repo, struct strbuf *buf)
{
strbuf_addstr(buf,
@@ -87,6 +111,8 @@ static const struct repo_info_field repo_info_field[] = {
{ "layout.bare", get_layout_bare },
{ "layout.shallow", get_layout_shallow },
{ "object.format", get_object_format },
+ { "path.commondir.absolute", get_path_commondir_absolute },
+ { "path.commondir.relative", get_path_commondir_relative },
{ "references.format", get_references_format },
};
diff --git a/t/t1900-repo-info.sh b/t/t1900-repo-info.sh
index 39bb77dda0..0c0228687f 100755
--- a/t/t1900-repo-info.sh
+++ b/t/t1900-repo-info.sh
@@ -155,4 +155,65 @@ test_expect_success 'git repo info -h shows only repo info usage' '
test_grep ! "git repo structure" actual
'
+# Helper function to test path keys in both absolute and relative formats.
+# $1: label for the test
+# $2: field_name (e.g., commondir)
+# $3: unique repo name for isolation
+# $4: expect_absolute (suffix appended to repo root)
+# $5: expect_relative (the relative path string expected)
+# $6: init_command (extra setup like exporting env vars)
+test_repo_info_path () {
+ label=$1
+ field_name=$2
+ repo_name=$3
+ expect_absolute_suffix=$4
+ expect_relative=$5
+ init_command=$6
+
+ absolute_root="$repo_name-absolute"
+ relative_root="$repo_name-relative"
+
+ test_expect_success "setup: $label" '
+ git init "$absolute_root" &&
+ git init "$relative_root" &&
+ mkdir -p "$absolute_root/sub" "$relative_root/sub"
+ '
+
+ test_expect_success "absolute: $label" '
+ (
+ cd "$absolute_root/sub" &&
+ ROOT="$(test-tool path-utils real_path ..)" && export ROOT &&
+ eval "$init_command" &&
+ expect_path="$ROOT${expect_absolute_suffix:+/$expect_absolute_suffix}" &&
+ echo "path.$field_name.absolute=$expect_path" >expect &&
+ git repo info "path.$field_name.absolute" >actual &&
+ test_cmp expect actual
+ )
+ '
+
+ test_expect_success "relative: $label" '
+ (
+ cd "$relative_root/sub" &&
+ ROOT="$(test-tool path-utils real_path ..)" && export ROOT &&
+ eval "$init_command" &&
+ echo "path.$field_name.relative=$expect_relative" >expect &&
+ git repo info "path.$field_name.relative" >actual &&
+ test_cmp expect actual
+ )
+ '
+}
+
+test_repo_info_path 'commondir standard' 'commondir' 'commondir-std' \
+ '.git' '../.git'
+
+test_repo_info_path 'commondir with GIT_COMMON_DIR and GIT_DIR' 'commondir' \
+ 'commondir-envs' 'custom-common' '../custom-common' \
+ 'GIT_COMMON_DIR="$ROOT/custom-common" && export GIT_COMMON_DIR &&
+ GIT_DIR="../.git" && export GIT_DIR &&
+ git init --bare "$ROOT/custom-common"'
+
+test_repo_info_path 'commondir with only GIT_DIR' 'commondir' \
+ 'commondir-only-gitdir' '.git' '../.git' \
+ 'GIT_DIR="../.git" && export GIT_DIR'
+
test_done
--
2.54.0
^ permalink raw reply related
* [GSoC Patch v4 4/4] repo: add path.gitdir with absolute and relative suffix formatting
From: K Jayatheerth @ 2026-06-15 4:51 UTC (permalink / raw)
To: git
Cc: a3205153416, gitster, jltobler, kumarayushjha123,
lucasseikioshiro, phillip.wood, sandals, kristofferhaugsbakk,
K Jayatheerth
In-Reply-To: <20260615045112.50686-1-jayatheerthkulkarni2005@gmail.com>
Scripts need a stable way to locate the git directory without
parsing rev-parse output or relying on its flag-driven path format
selection. There is no way to retrieve this path from git repo info
today.
Introduce path.gitdir.absolute and path.gitdir.relative keys,
consistent with the path.commondir keys added in the previous patch.
Reuse the test_repo_info_path helper introduced there to validate
both variants.
Mentored-by: Justin Tobler <jltobler@gmail.com>
Mentored-by: Lucas Seiki Oshiro <lucasseikioshiro@gmail.com>
Signed-off-by: K Jayatheerth <jayatheerthkulkarni2005@gmail.com>
---
Documentation/git-repo.adoc | 6 ++++++
builtin/repo.c | 24 ++++++++++++++++++++++++
t/t1900-repo-info.sh | 7 +++++++
3 files changed, 37 insertions(+)
diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
index 890c34051d..ed7d80c690 100644
--- a/Documentation/git-repo.adoc
+++ b/Documentation/git-repo.adoc
@@ -113,6 +113,12 @@ values that they return:
The path to the Git repository's common directory relative to
the current working directory.
+`path.gitdir.absolute`::
+ The canonical absolute path to the Git repository directory (the `.git` directory).
+
+`path.gitdir.relative`::
+ The path to the Git repository directory relative to the current working directory.
+
`references.format`::
The reference storage format. The valid values are:
+
diff --git a/builtin/repo.c b/builtin/repo.c
index c4cc3bf3fc..9a312d127a 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -99,6 +99,28 @@ static int get_path_commondir_relative(struct repository *repo, struct strbuf *b
return 0;
}
+static int get_path_gitdir_absolute(struct repository *repo, struct strbuf *buf)
+{
+ const char *git_dir = repo_get_git_dir(repo);
+
+ if (!git_dir)
+ return error(_("unable to get git directory"));
+
+ append_formatted_path(buf, git_dir, startup_info->prefix, PATH_FORMAT_CANONICAL);
+ return 0;
+}
+
+static int get_path_gitdir_relative(struct repository *repo, struct strbuf *buf)
+{
+ const char *git_dir = repo_get_git_dir(repo);
+
+ if (!git_dir)
+ return error(_("unable to get git directory"));
+
+ append_formatted_path(buf, git_dir, startup_info->prefix, PATH_FORMAT_RELATIVE);
+ return 0;
+}
+
static int get_references_format(struct repository *repo, struct strbuf *buf)
{
strbuf_addstr(buf,
@@ -113,6 +135,8 @@ static const struct repo_info_field repo_info_field[] = {
{ "object.format", get_object_format },
{ "path.commondir.absolute", get_path_commondir_absolute },
{ "path.commondir.relative", get_path_commondir_relative },
+ { "path.gitdir.absolute", get_path_gitdir_absolute },
+ { "path.gitdir.relative", get_path_gitdir_relative },
{ "references.format", get_references_format },
};
diff --git a/t/t1900-repo-info.sh b/t/t1900-repo-info.sh
index 0c0228687f..45741fc9f1 100755
--- a/t/t1900-repo-info.sh
+++ b/t/t1900-repo-info.sh
@@ -216,4 +216,11 @@ test_repo_info_path 'commondir with only GIT_DIR' 'commondir' \
'commondir-only-gitdir' '.git' '../.git' \
'GIT_DIR="../.git" && export GIT_DIR'
+test_repo_info_path 'gitdir standard' 'gitdir' 'gitdir-std' \
+ '.git' '../.git'
+
+test_repo_info_path 'gitdir with explicit GIT_DIR' 'gitdir' \
+ 'gitdir-env' '.git' '../.git' \
+ 'GIT_DIR="../.git" && export GIT_DIR'
+
test_done
--
2.54.0
^ permalink raw reply related
* [PATCH v3] log: improve --follow following renames for non-linear history
From: Miklos Vajna @ 2026-06-15 6:22 UTC (permalink / raw)
To: Junio C Hamano; +Cc: Jeff King, git
In-Reply-To: <xmqqo6hglncl.fsf@gitster.g>
Have a repo with a subtree merge, do a 'git log --follow prefix/test.c',
the output only contains history in the outer repo, not commits that
were merged via a subtree merge.
What happens is that 'git log --follow' stores the followed path only in
opt->diffopt.pathspec, so in case the commit history is non-linear, and
multiple parents have renames to the followed path, then the end result
isn't really defined: the first commit that happens to be visited in one
of the parents update opt->diffopt.pathspec, and from that point, only
that updated path is visited.
Fix the problem by introducing a commit -> path map
(follow_pathspec_slab) that stores what will be a path to follow when
visiting that parent. At the top of log_tree_commit(), if the slab has
an entry for this commit, we replace opt->diffopt.pathspec with a path
from this entry, so the correct path is followed, even if an unrelated
sub-tree changed the path to be followed to something else. After
log_tree_diff() runs, we record each parent's path in the slab. As a
result, the walk order doesn't matter, which was exactly the source of
problems previously.
This helps with subtree merges (rename happens inside the merge commit),
but also fixes the general case when the rename happens in the history
of parents, not in the merge commit itself.
Signed-off-by: Miklos Vajna <vmiklos@collabora.com>
---
Hi Junio,
On Thu, Jun 11, 2026 at 03:32:42PM -0700, Junio C Hamano <gitster@pobox.com> wrote:
> Missing sign-off; omitting sign-off to say that this is primarily
> for requesting comments and not ready for application (often we see
> RFC on the Subject line when this is done) is fine, though.
I've fixed that, this is meant to be ready for application now.
> My answer to my (rhetorical) question (Can a "map" cut it?) actually
> was "we probably can", since our "rename following" code does not
> handle cases where two paths in a parent is merged into a single
> path in a child, or a single path in a parent is split to form
> multiple paths in a child.
This is what confused me. Seeing that the "rename following" code
doesn't handle splits, I can indeed go back to just track one path per
commit, which makes the patch simpler, so I'm quite happy with that.
> Are any of your test cases added by this patch behave differently
> with this version (vs the "single path assigned to each commit"
> version you had earlier)? If so, then obviously there is some hole
> in my above discussion.
Ignoring the setup ones, I had 3 tests in the patch:
1) The original subtree merge use-case, with unrelated histories, rename
happening in the merge commit itself.
2) Your unrelated histories use-case from
https://lore.kernel.org/git/xmqqjysz7r41.fsf@gitster.g/
which pointed out the design issue in the --follow feature.
3) A last one, which tried to handle splits, in retrospect not really
successfully.
So I suggest let's forget about the 3rd case, and the first two behave
the same when storing just one path in the slab, so that validates your
discussion.
Now that you pointed out a 3rd use-case, with related histories, I also
added a test for that, with a history like this:
B---X
/ \
A M---Z
\ /
C---Y
Where:
- A has path0
- B (child of A) modifies path0
- X (child of B) renames path0 to path1
- C (child of A) modifies path0
- Y (child of C) renames path0 to path2
- M merges path1 and path2 to just path
- Z modifies path
and 'git log --follow path' finds all 6 non-merge commits. I turned this
into a (new) 3rd testcase in the patch, since related histories were not
tested so far.
> Eek. That's a subtle workaround to break the built-in safety to
> ensure there is only one pathspec element while following.
I now took that out, since the slab now just has one path for each
commit.
> t4218 seems to be taken by another topic in-flight, so this needs
> renumbering.
OK, t4219 seems to be free in 'next', let me take that, then.
Thanks,
Miklos
Documentation/config/log.adoc | 3 +-
log-tree.c | 116 ++++++++++++++++++++++++++++++
log-tree.h | 1 +
revision.c | 2 +
revision.h | 4 ++
t/meson.build | 1 +
t/t4219-log-follow-merge.sh | 129 ++++++++++++++++++++++++++++++++++
7 files changed, 254 insertions(+), 2 deletions(-)
create mode 100755 t/t4219-log-follow-merge.sh
diff --git a/Documentation/config/log.adoc b/Documentation/config/log.adoc
index f20cc25cd7..757a7be196 100644
--- a/Documentation/config/log.adoc
+++ b/Documentation/config/log.adoc
@@ -53,8 +53,7 @@ This is the same as the `--decorate` option of the `git log`.
`log.follow`::
If `true`, `git log` will act as if the `--follow` option was used when
a single <path> is given. This has the same limitations as `--follow`,
- i.e. it cannot be used to follow multiple files and does not work well
- on non-linear history.
+ i.e. it cannot be used to follow multiple files.
`log.graphColors`::
A list of colors, separated by commas, that can be used to draw
diff --git a/log-tree.c b/log-tree.c
index 7e048701d0..90f933063e 100644
--- a/log-tree.c
+++ b/log-tree.c
@@ -3,6 +3,7 @@
#include "git-compat-util.h"
#include "commit-reach.h"
+#include "commit-slab.h"
#include "config.h"
#include "diff.h"
#include "diffcore.h"
@@ -1089,6 +1090,96 @@ static int do_remerge_diff(struct rev_info *opt,
return !opt->loginfo;
}
+/* Per-commit path storage for --follow across merges */
+define_commit_slab(follow_pathspec_slab, char *);
+
+static const char *pathspec_single_path(const struct pathspec *ps)
+{
+ if (ps->nr != 1)
+ return NULL;
+ return ps->items[0].match;
+}
+
+static void set_pathspec_to_single_path(struct pathspec *ps, const char *path)
+{
+ const char *paths[2] = { path, NULL };
+
+ clear_pathspec(ps);
+ parse_pathspec(ps,
+ PATHSPEC_ALL_MAGIC & ~PATHSPEC_LITERAL,
+ PATHSPEC_LITERAL_PATH, "", paths);
+}
+
+static void remember_follow_pathspec(struct rev_info *opt,
+ struct commit *c, const char *path)
+{
+ char **slot;
+
+ if (!path)
+ return;
+ if (!opt->follow_pathspec_slab) {
+ opt->follow_pathspec_slab = xmalloc(sizeof(*opt->follow_pathspec_slab));
+ init_follow_pathspec_slab(opt->follow_pathspec_slab);
+ }
+ slot = follow_pathspec_slab_at(opt->follow_pathspec_slab, c);
+ if (*slot && !strcmp(*slot, path))
+ return;
+ free(*slot);
+ *slot = xstrdup(path);
+}
+
+static const char *recall_follow_pathspec(struct rev_info *opt,
+ struct commit *c)
+{
+ char **slot;
+
+ if (!opt->follow_pathspec_slab)
+ return NULL;
+ slot = follow_pathspec_slab_peek(opt->follow_pathspec_slab, c);
+ return slot ? *slot : NULL;
+}
+
+static void free_follow_pathspec_slot(char **slot)
+{
+ FREE_AND_NULL(*slot);
+}
+
+void release_follow_pathspec_slab(struct rev_info *opt)
+{
+ if (!opt->follow_pathspec_slab)
+ return;
+ deep_clear_follow_pathspec_slab(opt->follow_pathspec_slab,
+ free_follow_pathspec_slot);
+ FREE_AND_NULL(opt->follow_pathspec_slab);
+}
+
+/* Compute a path to follow in parent, if there is one */
+static void propagate_follow_pathspec_to_parent(struct rev_info *opt,
+ struct commit *commit,
+ struct commit *parent)
+{
+ struct diff_options diff_opts;
+ const char *path;
+
+ parse_commit_or_die(parent);
+ repo_diff_setup(opt->diffopt.repo, &diff_opts);
+ copy_pathspec(&diff_opts.pathspec, &opt->diffopt.pathspec);
+ diff_opts.flags.recursive = 1;
+ diff_opts.flags.follow_renames = 1;
+ diff_opts.output_format = DIFF_FORMAT_NO_OUTPUT;
+ diff_setup_done(&diff_opts);
+ diff_tree_oid(get_commit_tree_oid(parent),
+ get_commit_tree_oid(commit),
+ "", &diff_opts);
+
+ path = pathspec_single_path(&diff_opts.pathspec);
+ if (path)
+ remember_follow_pathspec(opt, parent, path);
+
+ diff_queue_clear(&diff_queued_diff);
+ diff_free(&diff_opts);
+}
+
/*
* Show the diff of a commit.
*
@@ -1179,6 +1270,16 @@ int log_tree_commit(struct rev_info *opt, struct commit *commit)
opt->loginfo = &log;
opt->diffopt.no_free = 1;
+ /* Any recorded path for this commit? If so, restore it */
+ if (opt->diffopt.flags.follow_renames) {
+ const char *stored = recall_follow_pathspec(opt, commit);
+ if (stored) {
+ const char *current = pathspec_single_path(&opt->diffopt.pathspec);
+ if (!current || strcmp(current, stored))
+ set_pathspec_to_single_path(&opt->diffopt.pathspec, stored);
+ }
+ }
+
/* NEEDSWORK: no restoring of no_free? Why? */
if (opt->line_level_traverse)
return line_log_print(opt, commit);
@@ -1195,6 +1296,21 @@ int log_tree_commit(struct rev_info *opt, struct commit *commit)
fprintf(opt->diffopt.file, "\n%s\n", opt->break_bar);
if (shown)
show_diff_of_diff(opt);
+
+ /* Record what path each parent of this commit should use */
+ if (opt->diffopt.flags.follow_renames) {
+ struct commit_list *parents = get_saved_parents(opt, commit);
+ if (parents && parents->next) {
+ struct commit_list *p;
+ for (p = parents; p; p = p->next)
+ propagate_follow_pathspec_to_parent(opt, commit,
+ p->item);
+ } else if (parents) {
+ remember_follow_pathspec(opt, parents->item,
+ pathspec_single_path(&opt->diffopt.pathspec));
+ }
+ }
+
opt->loginfo = NULL;
maybe_flush_or_die(opt->diffopt.file, "stdout");
opt->diffopt.no_free = no_free;
diff --git a/log-tree.h b/log-tree.h
index 07924be8bc..e8679b6c4a 100644
--- a/log-tree.h
+++ b/log-tree.h
@@ -26,6 +26,7 @@ struct decoration_options {
int parse_decorate_color_config(const char *var, const char *slot_name, const char *value);
int log_tree_diff_flush(struct rev_info *);
int log_tree_commit(struct rev_info *, struct commit *);
+void release_follow_pathspec_slab(struct rev_info *);
void show_log(struct rev_info *opt);
void format_decorations(struct strbuf *sb, const struct commit *commit,
enum git_colorbool use_color, const struct decoration_options *opts);
diff --git a/revision.c b/revision.c
index 5693618be4..caa85fb4c6 100644
--- a/revision.c
+++ b/revision.c
@@ -26,6 +26,7 @@
#include "decorate.h"
#include "string-list.h"
#include "line-log.h"
+#include "log-tree.h"
#include "mailmap.h"
#include "commit-slab.h"
#include "cache-tree.h"
@@ -3284,6 +3285,7 @@ void release_revisions(struct rev_info *revs)
line_log_free(revs);
oidset_clear(&revs->missing_commits);
release_revisions_bloom_keyvecs(revs);
+ release_follow_pathspec_slab(revs);
}
static void add_child(struct rev_info *revs, struct commit *parent, struct commit *child)
diff --git a/revision.h b/revision.h
index c9a11827cc..607113ca74 100644
--- a/revision.h
+++ b/revision.h
@@ -65,6 +65,7 @@ struct repository;
struct rev_info;
struct string_list;
struct saved_parents;
+struct follow_pathspec_slab;
struct bloom_keyvec;
struct bloom_filter_settings;
struct option;
@@ -354,6 +355,9 @@ struct rev_info {
/* copies of the parent lists, for --full-diff display */
struct saved_parents *saved_parents_slab;
+ /* per-commit pathspec for --follow across merges */
+ struct follow_pathspec_slab *follow_pathspec_slab;
+
struct commit_list *previous_parents;
struct commit_list *ancestry_path_bottoms;
const char *break_bar;
diff --git a/t/meson.build b/t/meson.build
index c5832fee05..8c4636565b 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -576,6 +576,7 @@ integration_tests = [
't4215-log-skewed-merges.sh',
't4216-log-bloom.sh',
't4217-log-limit.sh',
+ 't4219-log-follow-merge.sh',
't4252-am-options.sh',
't4253-am-keep-cr-dos.sh',
't4254-am-corrupt.sh',
diff --git a/t/t4219-log-follow-merge.sh b/t/t4219-log-follow-merge.sh
new file mode 100755
index 0000000000..e370f82955
--- /dev/null
+++ b/t/t4219-log-follow-merge.sh
@@ -0,0 +1,129 @@
+#!/bin/sh
+
+test_description='Test --follow follows renames across merges'
+
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=master
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
+. ./test-lib.sh
+
+test_expect_success 'setup subtree-merged repository' '
+ git init inner &&
+ echo inner >inner/inner.txt &&
+ git -C inner add inner.txt &&
+ git -C inner commit -m "inner init" &&
+
+ git init outer &&
+ echo outer >outer/outer.txt &&
+ git -C outer add outer.txt &&
+ git -C outer commit -m "outer init" &&
+
+ git -C outer fetch ../inner master &&
+ git -C outer merge -s ours --no-commit --allow-unrelated-histories \
+ FETCH_HEAD &&
+ git -C outer read-tree --prefix=inner/ -u FETCH_HEAD &&
+ git -C outer commit -m "Merge inner repo into inner/ subdirectory"
+'
+
+test_expect_success '--follow finds the pre-merge commit through a subtree merge' '
+ git -C outer log --follow --pretty=tformat:%s inner/inner.txt >actual &&
+ echo "inner init" >expect &&
+ test_cmp expect actual
+'
+
+test_expect_success 'setup merge of two branches that both renamed a file to README' '
+ git init foo &&
+ mkdir foo/foo &&
+ echo "foo readme" >foo/foo/README &&
+ git -C foo add foo/README &&
+ git -C foo commit -m "add foo README" &&
+
+ git -C foo mv foo/README README &&
+ git -C foo commit -m "promote foo README to toplevel" &&
+
+ echo "foo c" >foo/foo.c &&
+ git -C foo add foo.c &&
+ git -C foo commit -m "add foo C impl" &&
+
+ git init bar &&
+ mkdir bar/bar &&
+ echo "bar readme" >bar/bar/README &&
+ git -C bar add bar/README &&
+ git -C bar commit -m "add bar README" &&
+
+ git -C bar mv bar/README README &&
+ git -C bar commit -m "promote bar README to toplevel" &&
+
+ echo "bar c" >bar/bar.c &&
+ git -C bar add bar.c &&
+ git -C bar commit -m "add bar C impl" &&
+
+ git -C foo fetch ../bar master &&
+ git -C foo merge -s ours --no-commit --allow-unrelated-histories \
+ FETCH_HEAD &&
+ git -C foo checkout FETCH_HEAD -- bar.c &&
+ git -C foo commit -m "merge bar into foo"
+'
+
+test_expect_success '--follow follows renames across both sides of a merge' '
+ git -C foo log --follow --pretty=tformat:%s README >actual &&
+ sort actual >actual.sorted &&
+ cat >expect <<-\EOF &&
+ add bar README
+ add foo README
+ promote bar README to toplevel
+ promote foo README to toplevel
+ EOF
+ test_cmp expect actual.sorted
+'
+
+test_expect_success 'setup diamond with renames on both sides of a fork' '
+ git init diamond &&
+ test_lines="line 1\nline 2\nline 3\nline 4\nline 5\n" &&
+
+ printf "$test_lines" >diamond/path0 &&
+ git -C diamond add path0 &&
+ git -C diamond commit -m "A: add path0" &&
+
+ git -C diamond checkout -b upper &&
+ printf "line 1\nline 2\nline 3 modified by B\nline 4\nline 5\n" \
+ >diamond/path0 &&
+ git -C diamond commit -am "B: modify path0 on upper" &&
+ git -C diamond mv path0 path1 &&
+ git -C diamond commit -m "X: rename path0 to path1" &&
+
+ git -C diamond checkout -b lower master &&
+ printf "line 1\nline 2\nline 3 modified by C\nline 4\nline 5\n" \
+ >diamond/path0 &&
+ git -C diamond commit -am "C: modify path0 on lower" &&
+ git -C diamond mv path0 path2 &&
+ git -C diamond commit -m "Y: rename path0 to path2" &&
+
+ git -C diamond checkout upper &&
+ git -C diamond merge -s ours --no-commit lower &&
+ git -C diamond rm path1 &&
+ printf "line 1\nline 2\nline 3 merged\nline 4\nline 5\n" \
+ >diamond/path &&
+ git -C diamond add path &&
+ git -C diamond commit -m "M: merge with rename to path" &&
+
+ printf "line 1\nline 2\nline 3 merged again\nline 4\nline 5\n" \
+ >diamond/path &&
+ git -C diamond commit -am "Z: modify path"
+'
+
+test_expect_success '--follow follows renames through a fork in a single history' '
+ git -C diamond log --follow --pretty=tformat:%s path >actual &&
+ sort actual >actual.sorted &&
+ cat >expect <<-\EOF &&
+ A: add path0
+ B: modify path0 on upper
+ C: modify path0 on lower
+ X: rename path0 to path1
+ Y: rename path0 to path2
+ Z: modify path
+ EOF
+ test_cmp expect actual.sorted
+'
+
+test_done
--
2.51.0
^ permalink raw reply related
* Re: [PATCH] gitattributes: fix eol attribute for Perl scripts
From: Patrick Steinhardt @ 2026-06-15 7:22 UTC (permalink / raw)
To: Koutian Wu via GitGitGadget; +Cc: git, Koutian Wu
In-Reply-To: <pull.2151.git.1781497525828.gitgitgadget@gmail.com>
On Mon, Jun 15, 2026 at 04:25:25AM +0000, Koutian Wu via GitGitGadget wrote:
> From: ktwu01 <ktwu01@gmail.com>
>
> The *.pl pattern currently sets eof=lf, which is not a built-in
> attribute used for line-ending normalization.
>
> Use eol=lf instead, matching the neighboring *.perl and *.pm rules, so
> Perl scripts are checked out with LF line endings.
>
> Signed-off-by: ktwu01 <ktwu01@gmail.com>
The Signed-off-by and commit author should use your real name, if
possible. See [1].
> diff --git a/.gitattributes b/.gitattributes
> index 556322be01..26490ad60a 100644
> --- a/.gitattributes
> +++ b/.gitattributes
> @@ -2,7 +2,7 @@
> *.[ch] whitespace=indent,trail,space,incomplete diff=cpp
> *.sh whitespace=indent,trail,space,incomplete text eol=lf
> *.perl text eol=lf diff=perl
> -*.pl text eof=lf diff=perl
> +*.pl text eol=lf diff=perl
> *.pm text eol=lf diff=perl
> *.py text eol=lf diff=python
> *.bat text eol=crlf
Yeah, this looks obviously correct to me. Thanks for the fix!
Patrick
[1]: https://git-scm.com/docs/SubmittingPatches#real-name
^ permalink raw reply
* Re: [PATCH] cat-file: speed up default format
From: Patrick Steinhardt @ 2026-06-15 7:27 UTC (permalink / raw)
To: René Scharfe; +Cc: Git List
In-Reply-To: <5a7ed929-6fe0-496c-83bd-65dee57c2241@web.de>
On Sun, Jun 14, 2026 at 06:28:34PM +0200, René Scharfe wrote:
> eb54a3391b (cat-file: skip expanding default format, 2022-03-15) added
> special handling for the default batch format. In the meantime it has
> fallen behind the code path for handling arbitrary formats. Bring it up
> to speed by using the new and more efficient strbuf_add_oid_hex() and
> strbuf_add_uint() instead of strbuf_addf():
>
> Benchmark 1: ./git_main cat-file --batch-all-objects --batch-check='%(objectname) %(objecttype) %(objectsize)'
> Time (mean ± σ): 1.051 s ± 0.003 s [User: 1.027 s, System: 0.023 s]
> Range (min … max): 1.049 s … 1.058 s 10 runs
>
> Benchmark 2: ./git_main cat-file --batch-all-objects --batch-check='%(objectname)-%(objecttype)-%(objectsize)'
> Time (mean ± σ): 1.012 s ± 0.002 s [User: 0.988 s, System: 0.023 s]
> Range (min … max): 1.010 s … 1.018 s 10 runs
>
> Benchmark 3: ./git cat-file --batch-all-objects --batch-check='%(objectname) %(objecttype) %(objectsize)'
> Time (mean ± σ): 979.0 ms ± 1.1 ms [User: 954.1 ms, System: 23.2 ms]
> Range (min … max): 977.7 ms … 980.8 ms 10 runs
>
> Summary
> ./git cat-file --batch-all-objects --batch-check='%(objectname) %(objecttype) %(objectsize)' ran
> 1.03 ± 0.00 times faster than ./git_main cat-file --batch-all-objects --batch-check='%(objectname)-%(objecttype)-%(objectsize)'
> 1.07 ± 0.00 times faster than ./git_main cat-file --batch-all-objects --batch-check='%(objectname) %(objecttype) %(objectsize)'
This almost makes me wonder whether it even makes sense to keep around
the handler for the default format. Is a 3% speedup worth the additional
complexity and the need to keep those sites in sync?
> diff --git a/builtin/cat-file.c b/builtin/cat-file.c
> index 2b64f8f733..d7f7895e30 100644
> --- a/builtin/cat-file.c
> +++ b/builtin/cat-file.c
> @@ -461,9 +461,12 @@ static void print_object_or_die(struct batch_options *opt, struct expand_data *d
> static void print_default_format(struct strbuf *scratch, struct expand_data *data,
> struct batch_options *opt)
> {
> - strbuf_addf(scratch, "%s %s %"PRIuMAX"%c", oid_to_hex(&data->oid),
> - type_name(data->type),
> - (uintmax_t)data->size, opt->output_delim);
> + strbuf_add_oid_hex(scratch, &data->oid);
> + strbuf_addch(scratch, ' ');
> + strbuf_addstr(scratch, type_name(data->type));
> + strbuf_addch(scratch, ' ');
> + strbuf_add_uint(scratch, data->size);
> + strbuf_addch(scratch, opt->output_delim);
> }
The change itself looks obviously good to me though, thanks!
Patrick
^ permalink raw reply
* [PATCH v2] gitattributes: fix eol attribute for Perl scripts
From: Koutian Wu via GitGitGadget @ 2026-06-15 7:53 UTC (permalink / raw)
To: git; +Cc: Koutian Wu, Koutian Wu
In-Reply-To: <pull.2151.git.1781497525828.gitgitgadget@gmail.com>
From: Koutian Wu <ktwu01@gmail.com>
The *.pl pattern currently sets eof=lf, which is not a built-in
attribute used for line-ending normalization.
Use eol=lf instead, matching the neighboring *.perl and *.pm rules, so
Perl scripts are checked out with LF line endings.
Signed-off-by: Koutian Wu <ktwu01@gmail.com>
---
gitattributes: fix eol attribute for Perl scripts
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2151%2Fktwu01%2Fkw%2Ffix-pl-eol-attribute-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2151/ktwu01/kw/fix-pl-eol-attribute-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/2151
Range-diff vs v1:
1: 92ba4d499d ! 1: f4b4ca30c7 gitattributes: fix eol attribute for Perl scripts
@@
## Metadata ##
-Author: ktwu01 <ktwu01@gmail.com>
+Author: Koutian Wu <ktwu01@gmail.com>
## Commit message ##
gitattributes: fix eol attribute for Perl scripts
@@ Commit message
Use eol=lf instead, matching the neighboring *.perl and *.pm rules, so
Perl scripts are checked out with LF line endings.
- Signed-off-by: ktwu01 <ktwu01@gmail.com>
+ Signed-off-by: Koutian Wu <ktwu01@gmail.com>
## .gitattributes ##
@@
.gitattributes | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.gitattributes b/.gitattributes
index 556322be01..26490ad60a 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -2,7 +2,7 @@
*.[ch] whitespace=indent,trail,space,incomplete diff=cpp
*.sh whitespace=indent,trail,space,incomplete text eol=lf
*.perl text eol=lf diff=perl
-*.pl text eof=lf diff=perl
+*.pl text eol=lf diff=perl
*.pm text eol=lf diff=perl
*.py text eol=lf diff=python
*.bat text eol=crlf
base-commit: ea97ad8d017de0c9037451a78008a0fd60abea0c
--
gitgitgadget
^ permalink raw reply related
* Re: [PATCH] commit-graph: use timestamp_t for max parent generation accumulator
From: Patrick Steinhardt @ 2026-06-15 8:11 UTC (permalink / raw)
To: Elijah Newren via GitGitGadget; +Cc: git, Elijah Newren
In-Reply-To: <pull.2148.git.1781420271100.gitgitgadget@gmail.com>
On Sun, Jun 14, 2026 at 06:57:50AM +0000, Elijah Newren via GitGitGadget wrote:
> commit-graph: use timestamp_t for max parent generation accumulator
>
> We found a few repositories in the wild with commits whose authors were
> apparently on a computer in the year 2120 when they recorded their
> commits. Apparently, in a century from now, some folks are going to have
> a really weird timezone as well (-13068837), though the timezone doesn't
> factor into this patch at all.
I'd really be curious which other parts of Git will start to break once
we cross that threshold. Would it make sense if we maybe expanded our
linux-TEST-VARS job to create commits with a date beyond UINT32_MAX?
Something like the patch at the end of this mail. And yes, many tests
break with the patch applied. From all I've seen though many of those
failures are benign, even though I'd bet that there might even be some
"proper" failures in there.
Anyway, this is of course outside the scope of this patch series.
> diff --git a/commit-graph.c b/commit-graph.c
> index 9abe62bd5a..4b7156fd76 100644
> --- a/commit-graph.c
> +++ b/commit-graph.c
> @@ -1669,7 +1669,7 @@ static void compute_reachable_generation_numbers(
> struct commit *current = list->item;
> struct commit_list *parent;
> int all_parents_computed = 1;
> - uint32_t max_gen = 0;
> + timestamp_t max_gen = 0;
>
> for (parent = current->parents; parent; parent = parent->next) {
> repo_parse_commit(info->r, parent->item);
This looks obviously correct.
> diff --git a/t/t5328-commit-graph-64bit-time.sh b/t/t5328-commit-graph-64bit-time.sh
> index d8891e6a92..bc651b69de 100755
> --- a/t/t5328-commit-graph-64bit-time.sh
> +++ b/t/t5328-commit-graph-64bit-time.sh
> @@ -74,6 +74,15 @@ test_expect_success 'single commit with generation data exceeding UINT32_MAX' '
> git -C repo-uint32-max commit-graph verify
> '
>
> +test_expect_success 'descendant of commit with date exceeding UINT32_MAX' '
> + git init repo-uint32-max-descendant &&
> + test_commit -C repo-uint32-max-descendant \
> + --date "@4294967300 +0000" future-parent &&
> + test_commit -C repo-uint32-max-descendant present-day-child &&
> + git -C repo-uint32-max-descendant commit-graph write --reachable &&
> + git -C repo-uint32-max-descendant commit-graph verify
> +'
Makes sense. Thanks!
Patrick
diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh
index 809c662124..e78902b671 100644
--- a/t/test-lib-functions.sh
+++ b/t/test-lib-functions.sh
@@ -136,12 +136,19 @@ sane_unset () {
test_tick () {
if test -z "${test_tick+set}"
then
- test_tick=1112911993
+ if test_bool_env GIT_TEST_FUTURE false
+ then
+ test_tick=4294697600
+ test_tick_prefix=@
+ else
+ test_tick=1112911993
+ test_tick_prefix=
+ fi
else
test_tick=$(($test_tick + 60))
fi
- GIT_COMMITTER_DATE="$test_tick -0700"
- GIT_AUTHOR_DATE="$test_tick -0700"
+ GIT_COMMITTER_DATE="$test_tick_prefix$test_tick -0700"
+ GIT_AUTHOR_DATE="$test_tick_prefix$test_tick -0700"
export GIT_COMMITTER_DATE GIT_AUTHOR_DATE
}
diff --git a/t/test-lib.sh b/t/test-lib.sh
index 4a7357b547..54798fb3f1 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -558,12 +558,26 @@ TEST_AUTHOR_LOCALNAME=author
TEST_AUTHOR_DOMAIN=example.com
GIT_AUTHOR_EMAIL=${TEST_AUTHOR_LOCALNAME}@${TEST_AUTHOR_DOMAIN}
GIT_AUTHOR_NAME='A U Thor'
-GIT_AUTHOR_DATE='1112354055 +0200'
TEST_COMMITTER_LOCALNAME=committer
TEST_COMMITTER_DOMAIN=example.com
GIT_COMMITTER_EMAIL=${TEST_COMMITTER_LOCALNAME}@${TEST_COMMITTER_DOMAIN}
GIT_COMMITTER_NAME='C O Mitter'
-GIT_COMMITTER_DATE='1112354055 +0200'
+
+case "${GIT_TEST_FUTURE:-false}" in
+1|on|true|yes)
+ GIT_AUTHOR_DATE="${GIT_TEST_DATE:-@4294697300 +0200}"
+ GIT_COMMITTER_DATE="${GIT_TEST_DATE:-@4294697300 +0200}"
+ ;;
+0|off|false|no)
+ GIT_AUTHOR_DATE="${GIT_TEST_DATE:-1112354055 +0200}"
+ GIT_COMMITTER_DATE="${GIT_TEST_DATE:-1112354055 +0200}"
+ ;;
+*)
+ echo "GIT_TEST_FUTURE requires a boolean" >&2
+ exit 1
+ ;;
+esac
+
GIT_MERGE_VERBOSITY=5
GIT_MERGE_AUTOEDIT=no
export GIT_MERGE_VERBOSITY GIT_MERGE_AUTOEDIT
^ permalink raw reply related
* Re: [PATCH 0/2] rebase: add --fixup to fold a range into its oldest commit
From: Harald Nordgren @ 2026-06-15 8:18 UTC (permalink / raw)
To: Junio C Hamano; +Cc: Harald Nordgren via GitGitGadget, git
In-Reply-To: <xmqqqzm8d0j7.fsf@gitster.g>
> > Adds git rebase --autosquash --fixup [<upstream>] to fold a range of commits
> > into its oldest one, reusing that commit's message.
>
> [2/2] seems to add "--fixup-all" but I agree with the "related idea"
> that naming it and modelling it after "merge --squash" would be
> easier to understand.
Sounds reasonable.
> I also wonder if we can do something like this without adding any
> new option or command. E.g., if you have four patch series, where
> the initial implementation HEAD~3 is followed by "oops it was still
> wrong" fix-up HEAD~2, HEAD~1 and HEAD, then
>
> git reset --soft HEAD~3 && git commit --amend --no-edit
>
> is what the user wants to do, no?
I don't think it's enough. First of all the user has to know the N for
HEAD~N, and then 'git reset --soft HEAD~N && git commit --amend
--no-edit' is still quite ugly.
Harald
^ permalink raw reply
* Re: [PATCH 3/6] hash algorithms: use size_t for section lengths
From: Patrick Steinhardt @ 2026-06-15 8:35 UTC (permalink / raw)
To: Philip Oakley via GitGitGadget; +Cc: git, Johannes Schindelin, Philip Oakley
In-Reply-To: <253d6f8004e710d05b5de1f8279d67d2220f83de.1780593313.git.gitgitgadget@gmail.com>
On Thu, Jun 04, 2026 at 05:15:09PM +0000, Philip Oakley via GitGitGadget wrote:
> diff --git a/object-file.c b/object-file.c
> index 1f5f9daf24..c648cecd80 100644
> --- a/object-file.c
> +++ b/object-file.c
> @@ -581,7 +581,7 @@ static void write_object_file_prepare(const struct git_hash_algo *algo,
> /* Generate the header */
> *hdrlen = format_object_header(hdr, *hdrlen, type, len);
>
> - /* Sha1.. */
> + /* Hash (function pointers) computation */
> hash_object_body(algo, &c, buf, len, oid, hdr, hdrlen);
> }
>
Thanks for updating this comment while at it :)
> diff --git a/t/t1007-hash-object.sh b/t/t1007-hash-object.sh
> index 7867fd1dbf..10382a815e 100755
> --- a/t/t1007-hash-object.sh
> +++ b/t/t1007-hash-object.sh
> @@ -261,7 +261,7 @@ test_expect_success '--stdin outside of repository (uses default hash)' '
> test_cmp expect actual
> '
>
> -test_expect_failure EXPENSIVE,SIZE_T_IS_64BIT,!LONG_IS_64BIT \
> +test_expect_success EXPENSIVE,SIZE_T_IS_64BIT,!LONG_IS_64BIT \
> 'files over 4GB hash literally' '
> test-tool genzeros $((5*1024*1024*1024)) >big &&
> test_oid large5GB >expect &&
Previously we required `!LONG_IS_64BIT`, because the test would have
succeeded on platforms where it is 64 bit wide. But now that this test
works on all platforms I rather wonder whether we should completely drop
that prerequisite here, as we expect it to pass regardless of whether or
not long is 64 bit now.
Patrick
^ permalink raw reply
* Re: [PATCH 2/6] object-file.c: use size_t for header lengths
From: Patrick Steinhardt @ 2026-06-15 8:35 UTC (permalink / raw)
To: Philip Oakley via GitGitGadget; +Cc: git, Johannes Schindelin, Philip Oakley
In-Reply-To: <809d83e46fb46baeb5d0dfcd12eb7fc63580eec4.1780593313.git.gitgitgadget@gmail.com>
On Thu, Jun 04, 2026 at 05:15:08PM +0000, Philip Oakley via GitGitGadget wrote:
> From: Philip Oakley <philipoakley@iee.email>
>
> Continue walking the code path for the >4GB `hash-object --literally`
> test. The `hash_object_file_literally()` function internally uses both
> `hash_object_file()` and `write_object_file_prepare()`. Both function
> signatures use `unsigned long` rather than `size_t` for the mem buffer
> sizes. Use `size_t` instead, for LLP64 compatibility.
>
> While at it, convert those function's object's header buffer length to
> `size_t` for consistency. The value is already upcast to `uintmax_t` for
> print format compatibility.
One thing I was wondering is whether we should rather migrate to a size
that is consistent across different platforms. We could e.g. `typedef
uint64_t objsize_t` and then use that going forward.
I guess the question though is whether that'd buy us anything. In other
words, are there any platforms that we care about where `size_t` is only
32 bit wide? And would such platforms even be able to handle such large
objects?
Patrick
^ permalink raw reply
* Re: [PATCH 4/6] hash-object --stdin: verify that it works with >4GB/LLP64
From: Patrick Steinhardt @ 2026-06-15 8:35 UTC (permalink / raw)
To: Philip Oakley via GitGitGadget; +Cc: git, Johannes Schindelin, Philip Oakley
In-Reply-To: <ba629a3f03d59b6d20f1199ec86c140b0db63308.1780593313.git.gitgitgadget@gmail.com>
On Thu, Jun 04, 2026 at 05:15:10PM +0000, Philip Oakley via GitGitGadget wrote:
> diff --git a/t/t1007-hash-object.sh b/t/t1007-hash-object.sh
> index 10382a815e..59efee3aff 100755
> --- a/t/t1007-hash-object.sh
> +++ b/t/t1007-hash-object.sh
> @@ -269,4 +269,12 @@ test_expect_success EXPENSIVE,SIZE_T_IS_64BIT,!LONG_IS_64BIT \
> test_cmp expect actual
> '
>
> +test_expect_success EXPENSIVE,SIZE_T_IS_64BIT,!LONG_IS_64BIT \
> + 'files over 4GB hash correctly via --stdin' '
> + { test -f big || test-tool genzeros $((5*1024*1024*1024)) >big; } &&
> + test_oid large5GB >expect &&
> + git hash-object --stdin <big >actual &&
> + test_cmp expect actual
> +'
Same comment here: can we drop the `!LONG_IS_64BIT` prereq?
Patrick
^ permalink raw reply
* Re: [PATCH 5/6] hash-object: add another >4GB/LLP64 test case
From: Patrick Steinhardt @ 2026-06-15 8:35 UTC (permalink / raw)
To: Philip Oakley via GitGitGadget; +Cc: git, Johannes Schindelin, Philip Oakley
In-Reply-To: <f48d570bba87f7604158646873b998725a4a9db9.1780593313.git.gitgitgadget@gmail.com>
On Thu, Jun 04, 2026 at 05:15:11PM +0000, Philip Oakley via GitGitGadget wrote:
> diff --git a/t/t1007-hash-object.sh b/t/t1007-hash-object.sh
> index 59efee3aff..f2722380ee 100755
> --- a/t/t1007-hash-object.sh
> +++ b/t/t1007-hash-object.sh
> @@ -277,4 +277,12 @@ test_expect_success EXPENSIVE,SIZE_T_IS_64BIT,!LONG_IS_64BIT \
> test_cmp expect actual
> '
>
> +test_expect_success EXPENSIVE,SIZE_T_IS_64BIT,!LONG_IS_64BIT \
> + 'files over 4GB hash correctly' '
> + { test -f big || test-tool genzeros $((5*1024*1024*1024)) >big; } &&
> + test_oid large5GB >expect &&
> + git hash-object -- big >actual &&
> + test_cmp expect actual
> +'
Same comment here.
Nit: I feel like we could've easily introduced all of these tests in the
first commit.
Patrick
^ permalink raw reply
* [PATCH v2 0/2] rebase: add --squash to fold a range into its first commit
From: Harald Nordgren via GitGitGadget @ 2026-06-15 8:37 UTC (permalink / raw)
To: git; +Cc: Harald Nordgren
In-Reply-To: <pull.2337.git.git.1781465141.gitgitgadget@gmail.com>
Rename to rebase --squash.
Harald Nordgren (2):
t3415: remove prepare-commit-msg hook after use
rebase: add --squash to fold a range
Documentation/git-rebase.adoc | 11 ++++
builtin/rebase.c | 16 ++++-
sequencer.c | 24 ++++++-
sequencer.h | 2 +-
t/t3415-rebase-autosquash.sh | 118 ++++++++++++++++++++++++++++++++++
5 files changed, 166 insertions(+), 5 deletions(-)
base-commit: ea97ad8d017de0c9037451a78008a0fd60abea0c
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2337%2FHaraldNordgren%2Frebase-fixup-fold-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2337/HaraldNordgren/rebase-fixup-fold-v2
Pull-Request: https://github.com/git/git/pull/2337
Range-diff vs v1:
1: c55b9cd6f7 = 1: c55b9cd6f7 t3415: remove prepare-commit-msg hook after use
2: bd1bc62aa8 ! 2: 22d4276ff5 rebase: add --fixup-all to fold a range
@@ Metadata
Author: Harald Nordgren <haraldnordgren@gmail.com>
## Commit message ##
- rebase: add --fixup-all to fold a range
+ rebase: add --squash to fold a range
Folding a series of commits into one required either an interactive
rebase where each commit after the first was hand-edited to "fixup", or
a "git reset --soft" to the merge base followed by "git commit --amend".
- Add "git rebase --autosquash --fixup-all [<upstream>]" to do this
- directly. It keeps the first commit in the range as a "pick" and turns
- every later commit into a "fixup", so the whole range collapses into a
- single commit that reuses the first commit's message. With no <upstream>
- argument the range is "@{upstream}..HEAD", folding all unpushed commits
- into one.
+ Add "git rebase --squash [<upstream>]" to do this directly. It keeps
+ the first commit in the range as a "pick" and turns every later commit
+ into a "fixup", so the whole range collapses into a single commit that
+ reuses the first commit's message. With no <upstream> argument the range
+ is "@{upstream}..HEAD", folding all unpushed commits into one.
- Fold the commits in their original order, so that any fixup!/squash!
- commits already present in the range are folded in as well. Allow the
- flag only together with --autosquash, and reject --rebase-merges since a
- merge commit cannot be folded into another commit.
+ The option implies the merge backend, so it works on its own without
+ --autosquash. Fold the commits in their original order, so that any
+ fixup!/squash! commits already present in the range are folded in as
+ well. Reject --rebase-merges since a merge commit cannot be folded into
+ another commit.
+ Inspired-by: Sergey Chernov <serega.morph@gmail.com>
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
## Documentation/git-rebase.adoc ##
@@ Documentation/git-rebase.adoc: option can be used to override that setting.
+
See also INCOMPATIBLE OPTIONS below.
-+--fixup-all::
-+ Valid only when used with `--autosquash`. Keep the first commit in
-+ the range as a `pick` and change every later commit to a `fixup`, so
-+ the whole range is folded into a single commit that reuses the first
-+ commit's message. With no `<upstream>` argument this folds all commits
-+ since `@{upstream}` into one. The commits are folded in their original
-+ order, so any `fixup!`/`squash!` commits already in the range are folded
-+ in as well. Cannot be combined with `--rebase-merges`, as a merge
-+ commit cannot be folded into another commit.
++--squash::
++ Keep the first commit in the range as a `pick` and change every later
++ commit to a `fixup`, so the whole range is folded into a single commit
++ that reuses the first commit's message. With no `<upstream>` argument
++ this folds all commits since `@{upstream}` into one. The commits are
++ folded in their original order, so any `fixup!`/`squash!` commits
++ already in the range are folded in as well. Cannot be combined with
++ `--rebase-merges`, as a merge commit cannot be folded into another
++ commit.
+
--autostash::
--no-autostash::
@@ Documentation/git-rebase.adoc: are incompatible with the following options:
* --strategy
* --strategy-option
* --autosquash
-+ * --fixup-all
++ * --squash
* --rebase-merges
* --interactive
* --exec
@@ builtin/rebase.c: struct rebase_options {
int allow_rerere_autoupdate;
int keep_empty;
int autosquash;
-+ int fixup_all;
++ int squash;
char *gpg_sign_opt;
int autostash;
int committer_date_is_author_date;
@@ builtin/rebase.c: static int do_interactive_rebase(struct rebase_options *opts,
shortrevisions, opts->onto_name, opts->onto,
&opts->orig_head->object.oid, &opts->exec,
- opts->autosquash, opts->update_refs, &todo_list);
-+ opts->autosquash, opts->fixup_all, opts->update_refs,
++ opts->autosquash, opts->squash, opts->update_refs,
+ &todo_list);
cleanup:
@@ builtin/rebase.c: int cmd_rebase(int argc,
OPT_BOOL(0, "autosquash", &options.autosquash,
N_("move commits that begin with "
"squash!/fixup! under -i")),
-+ OPT_BOOL(0, "fixup-all", &options.fixup_all,
++ OPT_BOOL(0, "squash", &options.squash,
+ N_("fold all commits in the range into the first one")),
OPT_BOOL(0, "update-refs", &options.update_refs,
N_("update branches that point to commits "
"that are being rebased")),
+@@ builtin/rebase.c: int cmd_rebase(int argc,
+ if ((options.flags & REBASE_INTERACTIVE_EXPLICIT) ||
+ (options.action != ACTION_NONE) ||
+ (options.exec.nr > 0) ||
+- options.autosquash == 1) {
++ options.autosquash == 1 ||
++ options.squash) {
+ allow_preemptive_ff = 0;
+ }
+ if (options.committer_date_is_author_date || options.ignore_date)
@@ builtin/rebase.c: int cmd_rebase(int argc,
options.rebase_merges = (options.rebase_merges >= 0) ? options.rebase_merges :
((options.config_rebase_merges >= 0) ? options.config_rebase_merges : 0);
-+ if (options.fixup_all && options.autosquash != 1)
-+ die(_("--fixup-all requires --autosquash"));
-+
-+ if (options.fixup_all && options.rebase_merges)
++ if (options.squash && options.rebase_merges)
+ die(_("options '%s' and '%s' cannot be used together"),
-+ "--fixup-all", "--rebase-merges");
++ "--squash", "--rebase-merges");
++
++ if (options.squash)
++ imply_merge(&options, "--squash");
+
if (options.autosquash == 1) {
imply_merge(&options, "--autosquash");
@@ sequencer.c: static int todo_list_add_update_ref_commands(struct todo_list *todo
struct commit *onto, const struct object_id *orig_head,
struct string_list *commands, unsigned autosquash,
- unsigned update_refs,
-+ unsigned fixup_all, unsigned update_refs,
++ unsigned squash, unsigned update_refs,
struct todo_list *todo_list)
{
char shortonto[GIT_MAX_HEXSZ + 1];
@@ sequencer.c: int complete_action(struct repository *r, struct replay_opts *opts,
return -1;
- if (autosquash && todo_list_rearrange_squash(todo_list))
-+ if (fixup_all)
++ if (squash)
+ todo_list_fixup_all_but_first(todo_list);
+ else if (autosquash && todo_list_rearrange_squash(todo_list))
return -1;
@@ sequencer.h: int complete_action(struct repository *r, struct replay_opts *opts,
struct commit *onto, const struct object_id *orig_head,
struct string_list *commands, unsigned autosquash,
- unsigned update_refs,
-+ unsigned fixup_all, unsigned update_refs,
++ unsigned squash, unsigned update_refs,
struct todo_list *todo_list);
int todo_list_rearrange_squash(struct todo_list *todo_list);
@@ t/t3415-rebase-autosquash.sh: test_expect_success 'pick and fixup respect commit
test_commit_message HEAD -m "something"
'
-+test_expect_success '--fixup-all folds the range into the first commit' '
++test_expect_success '--squash folds the range into the first commit' '
+ git reset --hard base &&
+ test_commit --no-tag fold1 file_fold a &&
+ test_commit --no-tag fold2 file_fold b &&
+ test_commit --no-tag fold3 file_fold c &&
-+ git rebase --autosquash --fixup-all HEAD~3 &&
++ git rebase --squash HEAD~3 &&
+ test_cmp_rev base HEAD~1 &&
+ test_commit_message HEAD -m "fold1" &&
+ echo c >expect &&
+ test_cmp expect file_fold
+'
+
-+test_expect_success '--fixup-all folds smoothly when a fixup! commit is in the series' '
++test_expect_success '--squash folds smoothly when a fixup! commit is in the series' '
+ git reset --hard base &&
+ test_commit --no-tag foldA file_fold a &&
+ test_commit --no-tag foldB file_fold b &&
+ git commit --allow-empty --fixup HEAD~1 &&
-+ git rebase --autosquash --fixup-all HEAD~3 &&
++ git rebase --squash HEAD~3 &&
+ test_cmp_rev base HEAD~1 &&
+ test_commit_message HEAD -m "foldA" &&
+ echo b >expect &&
+ test_cmp expect file_fold
+'
+
-+test_expect_success '--fixup-all picks the first commit even if it is a fixup!' '
++test_expect_success '--squash picks the first commit even if it is a fixup!' '
+ git reset --hard base &&
+ test_commit --no-tag fixupbase file_fix a &&
+ git commit --allow-empty --fixup HEAD &&
+ test_commit --no-tag fixuptail file_fix b &&
-+ git rebase --autosquash --fixup-all HEAD~3 &&
++ git rebase --squash HEAD~3 &&
+ test_cmp_rev base HEAD~1 &&
+ echo b >expect &&
+ test_cmp expect file_fix
+'
+
-+test_expect_success '--fixup-all with a single commit in range is a no-op' '
++test_expect_success '--squash with a single commit in range is a no-op' '
+ git reset --hard base &&
+ test_commit --no-tag solo file_solo a &&
+ git rev-parse HEAD >expect &&
-+ git rebase --autosquash --fixup-all HEAD~1 &&
++ git rebase --squash HEAD~1 &&
+ git rev-parse HEAD >actual &&
+ test_cmp expect actual
+'
+
-+test_expect_success '--fixup-all with an empty range succeeds' '
++test_expect_success '--squash with an empty range succeeds' '
+ git reset --hard base &&
-+ git rebase --autosquash --fixup-all HEAD &&
++ git rebase --squash HEAD &&
+ test_cmp_rev base HEAD
+'
+
-+test_expect_success '--fixup-all skips a dropped commit in the range' '
++test_expect_success '--squash skips a dropped commit in the range' '
+ git reset --hard base &&
+ test_commit --no-tag fixdrop1 file_drop a &&
+ git commit --allow-empty -m "empty in the middle" &&
+ test_commit --no-tag fixdrop3 file_drop b &&
-+ git rebase --autosquash --empty=drop --fixup-all HEAD~3 &&
++ git rebase --squash --empty=drop HEAD~3 &&
+ test_cmp_rev base HEAD~1 &&
+ test_commit_message HEAD -m "fixdrop1" &&
+ echo b >expect &&
+ test_cmp expect file_drop
+'
+
-+test_expect_success '--fixup-all folds a merge commit in the middle of the range' '
++test_expect_success '--squash folds a merge commit in the middle of the range' '
+ git reset --hard base &&
+ test_commit --no-tag mid-first &&
+ git checkout -b mid-side &&
@@ t/t3415-rebase-autosquash.sh: test_expect_success 'pick and fixup respect commit
+ git checkout - &&
+ git merge --no-ff -m "merge mid-side" mid-side &&
+ test_commit --no-tag mid-last &&
-+ git rebase --autosquash --fixup-all base &&
++ git rebase --squash base &&
+ test_cmp_rev base HEAD~1 &&
+ test_commit_message HEAD -m "mid-first" &&
+ test_path_is_file mid-merged.t
+'
+
-+test_expect_success '--fixup-all keeps the first flattened commit when a merge sorts first' '
++test_expect_success '--squash keeps the first flattened commit when a merge sorts first' '
+ git reset --hard base &&
+ git checkout -b head-side &&
+ test_commit --no-tag head-merged &&
+ git checkout - &&
+ git merge --no-ff -m "merge head-side" head-side &&
+ test_commit --no-tag head-last &&
-+ git rebase --autosquash --fixup-all base &&
++ git rebase --squash base &&
+ test_cmp_rev base HEAD~1 &&
+ test_commit_message HEAD -m "head-merged" &&
+ test_path_is_file head-merged.t
+'
+
-+test_expect_success '--fixup-all requires --autosquash' '
++test_expect_success '--squash takes precedence over --autosquash' '
+ git reset --hard base &&
-+ test_must_fail git rebase --fixup-all HEAD~1 2>err &&
-+ test_grep "fixup-all requires --autosquash" err &&
-+ test_must_fail git rebase --no-autosquash --fixup-all HEAD~1 2>err &&
-+ test_grep "fixup-all requires --autosquash" err
++ test_commit --no-tag combo-first &&
++ test_commit --no-tag combo-mid &&
++ git commit --allow-empty --fixup HEAD~1 &&
++ test_commit --no-tag combo-last &&
++ git rebase --autosquash --squash base &&
++ test_cmp_rev base HEAD~1 &&
++ test_commit_message HEAD -m "combo-first"
++'
++
++test_expect_success '--squash folds the range with rebase.autosquash set' '
++ test_config rebase.autosquash true &&
++ git reset --hard base &&
++ test_commit --no-tag cfg-first &&
++ test_commit --no-tag cfg-last &&
++ git rebase --squash base &&
++ test_cmp_rev base HEAD~1 &&
++ test_commit_message HEAD -m "cfg-first"
+'
+
-+test_expect_success '--fixup-all and --rebase-merges cannot be combined' '
++test_expect_success '--squash and --rebase-merges cannot be combined' '
+ git reset --hard base &&
-+ test_must_fail git rebase --autosquash --rebase-merges \
-+ --fixup-all HEAD~1 2>err &&
++ test_must_fail git rebase --rebase-merges --squash HEAD~1 2>err &&
+ test_grep "cannot be used together" err &&
+ test_path_is_missing .git/rebase-merge
+'
--
gitgitgadget
^ permalink raw reply
* [PATCH v2 1/2] t3415: remove prepare-commit-msg hook after use
From: Harald Nordgren via GitGitGadget @ 2026-06-15 8:37 UTC (permalink / raw)
To: git; +Cc: Harald Nordgren, Harald Nordgren
In-Reply-To: <pull.2337.v2.git.git.1781512625.gitgitgadget@gmail.com>
From: Harald Nordgren <haraldnordgren@gmail.com>
The "pick and fixup respect commit.cleanup" test left its
prepare-commit-msg hook in place, leaking it into later tests. Remove it
with test_when_finished.
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
t/t3415-rebase-autosquash.sh | 1 +
1 file changed, 1 insertion(+)
diff --git a/t/t3415-rebase-autosquash.sh b/t/t3415-rebase-autosquash.sh
index 5033411a43..8964d1cc88 100755
--- a/t/t3415-rebase-autosquash.sh
+++ b/t/t3415-rebase-autosquash.sh
@@ -490,6 +490,7 @@ test_expect_success 'pick and fixup respect commit.cleanup' '
git reset --hard base &&
test_commit --no-tag "fixup! second commit" file1 fixup &&
test_commit something &&
+ test_when_finished "rm -f .git/hooks/prepare-commit-msg" &&
write_script .git/hooks/prepare-commit-msg <<-\EOF &&
printf "\n# Prepared\n" >> "$1"
EOF
--
gitgitgadget
^ permalink raw reply related
* [PATCH v2 2/2] rebase: add --squash to fold a range
From: Harald Nordgren via GitGitGadget @ 2026-06-15 8:37 UTC (permalink / raw)
To: git; +Cc: Harald Nordgren, Harald Nordgren
In-Reply-To: <pull.2337.v2.git.git.1781512625.gitgitgadget@gmail.com>
From: Harald Nordgren <haraldnordgren@gmail.com>
Folding a series of commits into one required either an interactive
rebase where each commit after the first was hand-edited to "fixup", or
a "git reset --soft" to the merge base followed by "git commit --amend".
Add "git rebase --squash [<upstream>]" to do this directly. It keeps
the first commit in the range as a "pick" and turns every later commit
into a "fixup", so the whole range collapses into a single commit that
reuses the first commit's message. With no <upstream> argument the range
is "@{upstream}..HEAD", folding all unpushed commits into one.
The option implies the merge backend, so it works on its own without
--autosquash. Fold the commits in their original order, so that any
fixup!/squash! commits already present in the range are folded in as
well. Reject --rebase-merges since a merge commit cannot be folded into
another commit.
Inspired-by: Sergey Chernov <serega.morph@gmail.com>
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
Documentation/git-rebase.adoc | 11 ++++
builtin/rebase.c | 16 ++++-
sequencer.c | 24 ++++++-
sequencer.h | 2 +-
t/t3415-rebase-autosquash.sh | 117 ++++++++++++++++++++++++++++++++++
5 files changed, 165 insertions(+), 5 deletions(-)
diff --git a/Documentation/git-rebase.adoc b/Documentation/git-rebase.adoc
index f6c22d1598..4244288148 100644
--- a/Documentation/git-rebase.adoc
+++ b/Documentation/git-rebase.adoc
@@ -602,6 +602,16 @@ option can be used to override that setting.
+
See also INCOMPATIBLE OPTIONS below.
+--squash::
+ Keep the first commit in the range as a `pick` and change every later
+ commit to a `fixup`, so the whole range is folded into a single commit
+ that reuses the first commit's message. With no `<upstream>` argument
+ this folds all commits since `@{upstream}` into one. The commits are
+ folded in their original order, so any `fixup!`/`squash!` commits
+ already in the range are folded in as well. Cannot be combined with
+ `--rebase-merges`, as a merge commit cannot be folded into another
+ commit.
+
--autostash::
--no-autostash::
Automatically create a temporary stash entry before the operation
@@ -652,6 +662,7 @@ are incompatible with the following options:
* --strategy
* --strategy-option
* --autosquash
+ * --squash
* --rebase-merges
* --interactive
* --exec
diff --git a/builtin/rebase.c b/builtin/rebase.c
index fa4f5d9306..2df9f04728 100644
--- a/builtin/rebase.c
+++ b/builtin/rebase.c
@@ -118,6 +118,7 @@ struct rebase_options {
int allow_rerere_autoupdate;
int keep_empty;
int autosquash;
+ int squash;
char *gpg_sign_opt;
int autostash;
int committer_date_is_author_date;
@@ -329,7 +330,8 @@ static int do_interactive_rebase(struct rebase_options *opts, unsigned flags)
ret = complete_action(the_repository, &replay, flags,
shortrevisions, opts->onto_name, opts->onto,
&opts->orig_head->object.oid, &opts->exec,
- opts->autosquash, opts->update_refs, &todo_list);
+ opts->autosquash, opts->squash, opts->update_refs,
+ &todo_list);
cleanup:
replay_opts_release(&replay);
@@ -1205,6 +1207,8 @@ int cmd_rebase(int argc,
OPT_BOOL(0, "autosquash", &options.autosquash,
N_("move commits that begin with "
"squash!/fixup! under -i")),
+ OPT_BOOL(0, "squash", &options.squash,
+ N_("fold all commits in the range into the first one")),
OPT_BOOL(0, "update-refs", &options.update_refs,
N_("update branches that point to commits "
"that are being rebased")),
@@ -1471,7 +1475,8 @@ int cmd_rebase(int argc,
if ((options.flags & REBASE_INTERACTIVE_EXPLICIT) ||
(options.action != ACTION_NONE) ||
(options.exec.nr > 0) ||
- options.autosquash == 1) {
+ options.autosquash == 1 ||
+ options.squash) {
allow_preemptive_ff = 0;
}
if (options.committer_date_is_author_date || options.ignore_date)
@@ -1594,6 +1599,13 @@ int cmd_rebase(int argc,
options.rebase_merges = (options.rebase_merges >= 0) ? options.rebase_merges :
((options.config_rebase_merges >= 0) ? options.config_rebase_merges : 0);
+ if (options.squash && options.rebase_merges)
+ die(_("options '%s' and '%s' cannot be used together"),
+ "--squash", "--rebase-merges");
+
+ if (options.squash)
+ imply_merge(&options, "--squash");
+
if (options.autosquash == 1) {
imply_merge(&options, "--autosquash");
} else if (options.autosquash == -1) {
diff --git a/sequencer.c b/sequencer.c
index 57855b0066..bb42b40796 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -6554,11 +6554,29 @@ static int todo_list_add_update_ref_commands(struct todo_list *todo_list)
return 0;
}
+static void todo_list_fixup_all_but_first(struct todo_list *todo_list)
+{
+ int i, seen_first = 0;
+
+ for (i = 0; i < todo_list->nr; i++) {
+ struct todo_item *item = todo_list->items + i;
+
+ if (!item->commit || item->command == TODO_DROP)
+ continue;
+ if (!seen_first) {
+ seen_first = 1;
+ item->command = TODO_PICK;
+ continue;
+ }
+ item->command = TODO_FIXUP;
+ }
+}
+
int complete_action(struct repository *r, struct replay_opts *opts, unsigned flags,
const char *shortrevisions, const char *onto_name,
struct commit *onto, const struct object_id *orig_head,
struct string_list *commands, unsigned autosquash,
- unsigned update_refs,
+ unsigned squash, unsigned update_refs,
struct todo_list *todo_list)
{
char shortonto[GIT_MAX_HEXSZ + 1];
@@ -6581,7 +6599,9 @@ int complete_action(struct repository *r, struct replay_opts *opts, unsigned fla
if (update_refs && todo_list_add_update_ref_commands(todo_list))
return -1;
- if (autosquash && todo_list_rearrange_squash(todo_list))
+ if (squash)
+ todo_list_fixup_all_but_first(todo_list);
+ else if (autosquash && todo_list_rearrange_squash(todo_list))
return -1;
if (commands->nr)
diff --git a/sequencer.h b/sequencer.h
index 3164bd437d..1d5a164f02 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -196,7 +196,7 @@ int complete_action(struct repository *r, struct replay_opts *opts, unsigned fla
const char *shortrevisions, const char *onto_name,
struct commit *onto, const struct object_id *orig_head,
struct string_list *commands, unsigned autosquash,
- unsigned update_refs,
+ unsigned squash, unsigned update_refs,
struct todo_list *todo_list);
int todo_list_rearrange_squash(struct todo_list *todo_list);
diff --git a/t/t3415-rebase-autosquash.sh b/t/t3415-rebase-autosquash.sh
index 8964d1cc88..ce9abe5147 100755
--- a/t/t3415-rebase-autosquash.sh
+++ b/t/t3415-rebase-autosquash.sh
@@ -511,4 +511,121 @@ test_expect_success 'pick and fixup respect commit.cleanup' '
test_commit_message HEAD -m "something"
'
+test_expect_success '--squash folds the range into the first commit' '
+ git reset --hard base &&
+ test_commit --no-tag fold1 file_fold a &&
+ test_commit --no-tag fold2 file_fold b &&
+ test_commit --no-tag fold3 file_fold c &&
+ git rebase --squash HEAD~3 &&
+ test_cmp_rev base HEAD~1 &&
+ test_commit_message HEAD -m "fold1" &&
+ echo c >expect &&
+ test_cmp expect file_fold
+'
+
+test_expect_success '--squash folds smoothly when a fixup! commit is in the series' '
+ git reset --hard base &&
+ test_commit --no-tag foldA file_fold a &&
+ test_commit --no-tag foldB file_fold b &&
+ git commit --allow-empty --fixup HEAD~1 &&
+ git rebase --squash HEAD~3 &&
+ test_cmp_rev base HEAD~1 &&
+ test_commit_message HEAD -m "foldA" &&
+ echo b >expect &&
+ test_cmp expect file_fold
+'
+
+test_expect_success '--squash picks the first commit even if it is a fixup!' '
+ git reset --hard base &&
+ test_commit --no-tag fixupbase file_fix a &&
+ git commit --allow-empty --fixup HEAD &&
+ test_commit --no-tag fixuptail file_fix b &&
+ git rebase --squash HEAD~3 &&
+ test_cmp_rev base HEAD~1 &&
+ echo b >expect &&
+ test_cmp expect file_fix
+'
+
+test_expect_success '--squash with a single commit in range is a no-op' '
+ git reset --hard base &&
+ test_commit --no-tag solo file_solo a &&
+ git rev-parse HEAD >expect &&
+ git rebase --squash HEAD~1 &&
+ git rev-parse HEAD >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success '--squash with an empty range succeeds' '
+ git reset --hard base &&
+ git rebase --squash HEAD &&
+ test_cmp_rev base HEAD
+'
+
+test_expect_success '--squash skips a dropped commit in the range' '
+ git reset --hard base &&
+ test_commit --no-tag fixdrop1 file_drop a &&
+ git commit --allow-empty -m "empty in the middle" &&
+ test_commit --no-tag fixdrop3 file_drop b &&
+ git rebase --squash --empty=drop HEAD~3 &&
+ test_cmp_rev base HEAD~1 &&
+ test_commit_message HEAD -m "fixdrop1" &&
+ echo b >expect &&
+ test_cmp expect file_drop
+'
+
+test_expect_success '--squash folds a merge commit in the middle of the range' '
+ git reset --hard base &&
+ test_commit --no-tag mid-first &&
+ git checkout -b mid-side &&
+ test_commit --no-tag mid-merged &&
+ git checkout - &&
+ git merge --no-ff -m "merge mid-side" mid-side &&
+ test_commit --no-tag mid-last &&
+ git rebase --squash base &&
+ test_cmp_rev base HEAD~1 &&
+ test_commit_message HEAD -m "mid-first" &&
+ test_path_is_file mid-merged.t
+'
+
+test_expect_success '--squash keeps the first flattened commit when a merge sorts first' '
+ git reset --hard base &&
+ git checkout -b head-side &&
+ test_commit --no-tag head-merged &&
+ git checkout - &&
+ git merge --no-ff -m "merge head-side" head-side &&
+ test_commit --no-tag head-last &&
+ git rebase --squash base &&
+ test_cmp_rev base HEAD~1 &&
+ test_commit_message HEAD -m "head-merged" &&
+ test_path_is_file head-merged.t
+'
+
+test_expect_success '--squash takes precedence over --autosquash' '
+ git reset --hard base &&
+ test_commit --no-tag combo-first &&
+ test_commit --no-tag combo-mid &&
+ git commit --allow-empty --fixup HEAD~1 &&
+ test_commit --no-tag combo-last &&
+ git rebase --autosquash --squash base &&
+ test_cmp_rev base HEAD~1 &&
+ test_commit_message HEAD -m "combo-first"
+'
+
+test_expect_success '--squash folds the range with rebase.autosquash set' '
+ test_config rebase.autosquash true &&
+ git reset --hard base &&
+ test_commit --no-tag cfg-first &&
+ test_commit --no-tag cfg-last &&
+ git rebase --squash base &&
+ test_cmp_rev base HEAD~1 &&
+ test_commit_message HEAD -m "cfg-first"
+'
+
+test_expect_success '--squash and --rebase-merges cannot be combined' '
+ git reset --hard base &&
+ test_must_fail git rebase --rebase-merges --squash HEAD~1 2>err &&
+ test_grep "cannot be used together" err &&
+ test_path_is_missing .git/rebase-merge
+'
+
test_done
--
gitgitgadget
^ permalink raw reply related
* Re: [PATCH v2] gitattributes: fix eol attribute for Perl scripts
From: Patrick Steinhardt @ 2026-06-15 8:37 UTC (permalink / raw)
To: Koutian Wu via GitGitGadget; +Cc: git, Koutian Wu
In-Reply-To: <pull.2151.v2.git.1781510039164.gitgitgadget@gmail.com>
On Mon, Jun 15, 2026 at 07:53:58AM +0000, Koutian Wu via GitGitGadget wrote:
> Range-diff vs v1:
>
> 1: 92ba4d499d ! 1: f4b4ca30c7 gitattributes: fix eol attribute for Perl scripts
> @@
> ## Metadata ##
> -Author: ktwu01 <ktwu01@gmail.com>
> +Author: Koutian Wu <ktwu01@gmail.com>
>
> ## Commit message ##
> gitattributes: fix eol attribute for Perl scripts
> @@ Commit message
> Use eol=lf instead, matching the neighboring *.perl and *.pm rules, so
> Perl scripts are checked out with LF line endings.
>
> - Signed-off-by: ktwu01 <ktwu01@gmail.com>
> + Signed-off-by: Koutian Wu <ktwu01@gmail.com>
>
> ## .gitattributes ##
> @@
Thanks, this version looks good to me!
Patrick
^ permalink raw reply
* Re: [PATCH v4 0/3] compat/posix.h: enable UNUSED warning messages for Clang
From: Patrick Steinhardt @ 2026-06-15 8:48 UTC (permalink / raw)
To: Junio C Hamano; +Cc: Dominik Loidolt, git, asedeno, asedeno, avarab
In-Reply-To: <xmqqse6qe6oo.fsf@gitster.g>
On Sat, Jun 13, 2026 at 09:39:03AM -0700, Junio C Hamano wrote:
> Dominik Loidolt <dominik.loidolt@univie.ac.at> writes:
>
> > This series enables the intended UNUSED warning message with Clang by
> > adding a dedicated Clang version check. It also cleans up the nearby
> > GIT_GNUC_PREREQ() and UNUSED macros.
> >
> > Changes since v3:
> > - split style-only cleanups into their own patch
> > - fix the UNUSED preprocessor indentation style
> > - simplify the GIT_GNUC_PREREQ() comparison commit message
> > - keep the Clang-specific note in the patch that adds GIT_CLANG_PREREQ()
> >
> > Thanks,
> > Dominik
> >
> > Dominik Loidolt (3):
> > compat/posix.h: enable UNUSED warning messages for Clang
> > compat/posix.h: clean up GIT_GNUC_PREREQ() and UNUSED
> > compat/posix.h: simplify GIT_GNUC_PREREQ() comparison
>
> Looking good and all the points Patrick raised during the review of
> the previous round seem to have been addressed nicely.
>
> Will replace. Shall we mark it for 'next' now?
Yeah, I'm happy with this version. Thanks!
Patrick
^ permalink raw reply
* Re: [PATCH v3] update-ref: add --rename option
From: Patrick Steinhardt @ 2026-06-15 8:57 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git
In-Reply-To: <xmqqqzmbhikj.fsf@gitster.g>
On Fri, Jun 12, 2026 at 08:41:48AM -0700, Junio C Hamano wrote:
> Patrick Steinhardt <ps@pks.im> writes:
>
> > A slight tangent: this is part of why I really don't like commands that
> > determine their mode via flags: you now have to worry about every
> > combination of flags and whether they even make sense. With subcommands
> > we at least only have to worry about the set of flags that directly
> > apply to that given subcommand.
> >
> > Makes me wonder whether I should have a look at extending git-refs(1)
> > further:
> >
> > git refs delete <ref> [<oldvalue>]
> > git refs update <ref> <newvalue> [<oldvalue>]
> > git refs rename <ref> <oldname> <newname>
> >
> > I always wanted to do this eventually so that we have one top-level
> > command that knows how to do "everything refs".
>
> That may indeed be a better direction to go, but isn't update-ref
> the "everything refs" command already?
Well, it doesn't handle reading references, which is something that
git-refs(1) already knows to do.
Patrick
^ permalink raw reply
* Re: [PATCH 2/7] patch-delta: use size_t for sizes
From: Johannes Schindelin @ 2026-06-15 9:29 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: Johannes Schindelin via GitGitGadget, git, Kristofer Karlsson
In-Reply-To: <aibJTHKsmqe_EJHc@pks.im>
Hi Patrick,
On Mon, 15 Jun 2026, Patrick Steinhardt wrote:
> On Thu, Jun 04, 2026 at 10:51:07AM +0000, Johannes Schindelin via GitGitGadget wrote:
> > From: Johannes Schindelin <johannes.schindelin@gmx.de>
> >
> > `patch_delta()` takes the source and delta sizes by value and writes
> > back the reconstructed target size through an `unsigned long *`. That
> > datatype cannot represent a value that exceeds 4 GiB on systems where
> > `unsigned long` is 32-bit (notably 64-bit Windows builds), though, even
> > though the delta encoding itself, the on-disk layout, and the in-memory
> > buffers happily carry such sizes. A `size_t` companion to
> > `get_delta_hdr_size()`, `get_delta_hdr_size_sz()`, was introduced in
> > 17fa077596 (delta, packfile: use size_t for delta header sizes,
> > 2026-05-08) precisely so that `patch_delta()` could be widened without
> > changing the on-the-wire decoding helper's signature.
> >
> > Widen `patch_delta()`'s three size parameters to `size_t` and switch
> > its internal use of `get_delta_hdr_size()` to the `_sz` variant.
> > Then propagate the wider type through the callers.
>
> Does `get_delta_hdr_size()` have any remaining callers after this patch
> series? I currently only spot two such callers, and you convert both of
> them in this patch.
As you noticed later on in the review: No, there are no such callers left,
and the `_sz` variant gets renamed, concluding the incremental migration
of that function from `unsigned long` to `size_t`.
> And can we reasonably add a test case that exercises this change?
Not reasonably, no. This would require constructing another artificial
_large_ object, this time with an unpacked Git object with a size >=4GB
that needs to be transmogrified into a different object.
Better leave the verification of this patch to static analysis (GCC or
Clang have become quite good at spotting things like this; Coverity would
be, too, if it ever comes back up from its "upgrades to the Scan servers",
https://web.archive.org/web/20260516152422/https://scan.coverity.com/
seems to be the start date of this update).
>
> > diff --git a/packfile.c b/packfile.c
> > index 89366abfe3..e202f48837 100644
> > --- a/packfile.c
> > +++ b/packfile.c
> > @@ -1964,10 +1964,8 @@ 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, &sz);
> > - size = sz;
> > + delta_size, &size);
>
> Nice that we get rid of this awkward construct.
Awkward, but necessary to allow for an incremental, reviewable conversion
;-)
Ciao,
Johannes
^ permalink raw reply
* Re: [PATCH 3/7] pack-objects(check_pack_inflate()): use size_t instead of unsigned long
From: Johannes Schindelin @ 2026-06-15 9:29 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: Johannes Schindelin via GitGitGadget, git, Kristofer Karlsson
In-Reply-To: <aibJVSrKPCfDVXw7@pks.im>
Hi Patrick,
On Mon, 15 Jun 2026, Patrick Steinhardt wrote:
> On Thu, Jun 04, 2026 at 10:51:08AM +0000, Johannes Schindelin via GitGitGadget wrote:
> > From: Johannes Schindelin <johannes.schindelin@gmx.de>
> >
> > `write_reuse_object()` learned to track its packed-object size as
> > `size_t` in 606c192380 (odb, packfile: use size_t for streaming
> > object sizes, 2026-05-08), but the comparison sink it feeds,
> > `check_pack_inflate()`, still takes the expected decompressed size
> > as `unsigned long`. The call site bridges the mismatch with
> > `cast_size_t_to_ulong()`, which on Windows turns a >4 GiB object
> > into an immediate die().
> >
> > That function only uses `expect` once: as the right-hand side of a
> > `stream.total_out == expect` equality test against zlib's counter.
> > zlib's own `total_out` counter is `uLong` and is therefore still
> > 32-bit-bound on Windows. Widening `expect` to `size_t` cannot fix that,
> > but it is a strict improvement nonetheless: instead of dying outright,
> > an oversized object now simply makes the equality fail and lets
> > `write_reuse_object()` fall back to `write_no_reuse_object()`, which
> > decompresses and re-deflates the content (and which the larger
> > pack-objects widening series targets separately).
>
> Hm. I wonder whether it's possible to reset `stream.total_out` on every
> iteration and instead have a local `size_t` variable that we use to
> track the total number of inflated bytes?
Possible? Yes. Appropriate? Unlikely. We would now pretend to have
inflated less bytes, _just_ to appease a data type limitation that we
already worked around in d05d666977 (git-zlib: handle data streams larger
than 4GB, 2026-05-08).
Ciao,
Johannes
^ permalink raw reply
* Re: [PATCH 4/7] packfile: widen unpack_entry()'s size out-parameter to size_t
From: Johannes Schindelin @ 2026-06-15 9:29 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: Johannes Schindelin via GitGitGadget, git, Kristofer Karlsson
In-Reply-To: <aibJW3h4PaYhOqFb@pks.im>
Hi Patrick,
On Mon, 15 Jun 2026, Patrick Steinhardt wrote:
> On Thu, Jun 04, 2026 at 10:51:09AM +0000, Johannes Schindelin via GitGitGadget wrote:
> > diff --git a/builtin/fast-import.c b/builtin/fast-import.c
> > index 82bc6dcc00..3dff898c43 100644
> > --- a/builtin/fast-import.c
> > +++ b/builtin/fast-import.c
> > @@ -1239,6 +1239,8 @@ static void *gfi_unpack_entry(
> > unsigned long *sizep)
> > {
> > enum object_type type;
> > + size_t size_st = 0;
> > + void *data;
> > struct packed_git *p = all_packs[oe->pack_id];
> > if (p == pack_data && p->pack_size < (pack_size + the_hash_algo->rawsz)) {
> > /* The object is stored in the packfile we are writing to
> > @@ -1260,7 +1262,10 @@ static void *gfi_unpack_entry(
> > */
> > p->pack_size = pack_size + the_hash_algo->rawsz;
> > }
> > - return unpack_entry(the_repository, p, oe->idx.offset, &type, sizep);
> > + data = unpack_entry(the_repository, p, oe->idx.offset, &type, &size_st);
> > + if (sizep)
> > + *sizep = cast_size_t_to_ulong(size_st);
> > + return data;
> > }
>
> Nit, please feel free to ignore: do we want to add a NEEDSWORK comment
> here?
Hehe... My mind translates the `cast_size_t_to_ulong()` function to
"NEEDSWORK!" already ;-)
Ciao,
Johannes
^ permalink raw reply
* Re: [PATCH 7/7] odb: use size_t for object_info.sizep and the size APIs
From: Johannes Schindelin @ 2026-06-15 9:29 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: Johannes Schindelin via GitGitGadget, git, Kristofer Karlsson
In-Reply-To: <aibJZ8EXoQSD2lsB@pks.im>
Hi Patrick,
On Mon, 15 Jun 2026, Patrick Steinhardt wrote:
> On Thu, Jun 04, 2026 at 10:51:12AM +0000, Johannes Schindelin via GitGitGadget wrote:
> > diff --git a/builtin/cat-file.c b/builtin/cat-file.c
> > index fa45f774d7..fa6e396ddc 100644
> > --- a/builtin/cat-file.c
> > +++ b/builtin/cat-file.c
> > @@ -120,7 +120,7 @@ static int cat_one_file(int opt, const char *exp_type, const char *obj_name)
> > struct object_id oid;
> > enum object_type type;
> > char *buf;
> > - unsigned long size;
> > + size_t size;
> > struct object_context obj_context = {0};
> > struct object_info oi = OBJECT_INFO_INIT;
> > unsigned flags = OBJECT_INFO_LOOKUP_REPLACE;
> > @@ -166,7 +166,7 @@ static int cat_one_file(int opt, const char *exp_type, const char *obj_name)
> > if (use_mailmap && (type == OBJ_COMMIT || type == OBJ_TAG)) {
> > size_t s = size;
> > buf = replace_idents_using_mailmap(buf, &s);
> > - size = cast_size_t_to_ulong(s);
> > + size = s;
> > }
> >
> > printf("%"PRIuMAX"\n", (uintmax_t)size);
>
> Can't we drop this local variable completely and instead supply `&size`
> directly?
Well spotted!
> > @@ -219,7 +225,7 @@ static int cat_one_file(int opt, const char *exp_type, const char *obj_name)
> > if (use_mailmap) {
> > size_t s = size;
> > buf = replace_idents_using_mailmap(buf, &s);
> > - size = cast_size_t_to_ulong(s);
> > + size = s;
> > }
> >
> > /* otherwise just spit out the data */
> > @@ -266,7 +272,7 @@ static int cat_one_file(int opt, const char *exp_type, const char *obj_name)
> > if (use_mailmap) {
> > size_t s = size;
> > buf = replace_idents_using_mailmap(buf, &s);
> > - size = cast_size_t_to_ulong(s);
> > + size = s;
> > }
> > break;
> > }
> > @@ -446,7 +455,7 @@ static void print_object_or_die(struct batch_options *opt, struct expand_data *d
> > if (use_mailmap) {
> > size_t s = size;
> > contents = replace_idents_using_mailmap(contents, &s);
> > - size = cast_size_t_to_ulong(s);
> > + size = s;
> > }
> >
> > if (type != data->type)
>
> Likewise for these three instances.
I totally agree.
> > @@ -555,7 +564,7 @@ static void batch_object_write(const char *obj_name,
> > if (!buf)
> > die(_("unable to read %s"), oid_to_hex(&data->oid));
> > buf = replace_idents_using_mailmap(buf, &s);
> > - data->size = cast_size_t_to_ulong(s);
> > + data->size = s;
> >
> > free(buf);
> > }
>
> And I think this site here can be adapted, as well.
Indeed!
> > diff --git a/diff.c b/diff.c
> > index 5a584fa1d5..816b89dc6c 100644
> > --- a/diff.c
> > +++ b/diff.c
> > @@ -4594,8 +4594,9 @@ int diff_populate_filespec(struct repository *r,
> > }
> > }
> > else {
> > + size_t size_st = 0;
> > struct object_info info = {
> > - .sizep = &s->size
> > + .sizep = &size_st
> > };
> >
> > if (!(size_only || check_binary))
> > @@ -4617,6 +4618,7 @@ int diff_populate_filespec(struct repository *r,
> > die("unable to read %s", oid_to_hex(&s->oid));
> >
> > object_read:
> > + s->size = cast_size_t_to_ulong(size_st);
> > if (size_only || check_binary) {
> > if (size_only)
> > return 0;
> > @@ -4631,6 +4633,7 @@ object_read:
> > if (odb_read_object_info_extended(r->objects, &s->oid, &info,
> > OBJECT_INFO_LOOKUP_REPLACE))
> > die("unable to read %s", oid_to_hex(&s->oid));
> > + s->size = cast_size_t_to_ulong(size_st);
> > }
> > s->should_free = 1;
> > }
>
> The flow in this function is quite weird if you ask me, but that's a
> preexisting issue. This does look correct to me, even if it's awkward.
Yes, on all four accounts.
Ciao,
Johannes
^ permalink raw reply
* Re: [PATCH v14 4/6] branch: add --prune-merged <branch>
From: Phillip Wood @ 2026-06-15 9:46 UTC (permalink / raw)
To: Harald Nordgren via GitGitGadget, git
Cc: Kristoffer Haugsbakk, Johannes Sixt, Harald Nordgren
In-Reply-To: <9924373da0a0598cabe4f08f3bc4200833679171.1780999917.git.gitgitgadget@gmail.com>
Hi Harald
On 09/06/2026 11:11, Harald Nordgren via GitGitGadget wrote:
> From: Harald Nordgren <haraldnordgren@gmail.com>
>
> git branch --prune-merged <branch>...
Please see my comments on the previous version about the naming of this
option. I really think we need to start a discussion to find a better
name for this option as the other options to delete a branch are named
"delete" rather than "prune" and this does not remove the branches
listed by "--merge"
> deletes the local branches that "--forked <branch>" would list,
> keeping only those whose tip is reachable from their configured
> upstream: the work has already landed on the upstream they track,
> so the local copy is no longer needed.
>
> Reachability is read from local refs; nothing is fetched. Run
> "git fetch" first if you want fresh upstream refs.
I don't think this sentence adds anything - git never fetches unless
the user explicitly asks it to.
>
> Three kinds of branches are spared:
>
> * any branch checked out in any worktree;
> * any branch whose upstream no longer resolves locally, since a
> missing upstream is not by itself a sign of integration;
> * any branch whose push destination equals its upstream
> (<branch>@{push} is the same as <branch>@{upstream}), such as
> a local "main" that tracks and pushes to "origin/main". Right
> after a pull it just looks "fully merged", so it is left
> alone. Only branches that push somewhere other than their
> upstream, typically topics in a fork workflow, are candidates.
>
> Branches that are not yet merged into their upstream are reported
> as a short warning and skipped, so one unmerged topic does not
> abort the whole sweep.
I'm not sure about this warning - the user has asked us to delete the
branches whose upstreams match those passed on the commandline and that
have been merged so do they really want to hear about the ones that have
not been merged? It might be useful to have a way to list those that
have not been merged in the future.
> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
> ---
> Documentation/git-branch.adoc | 24 ++++
> builtin/branch.c | 67 +++++++++++-
> t/t3200-branch.sh | 201 ++++++++++++++++++++++++++++++++++
> 3 files changed, 290 insertions(+), 2 deletions(-)
>
> diff --git a/Documentation/git-branch.adoc b/Documentation/git-branch.adoc
> index 62ebab6051..fdaccc9662 100644
> --- a/Documentation/git-branch.adoc
> +++ b/Documentation/git-branch.adoc
> @@ -25,6 +25,7 @@ git branch (-m|-M) [<old-branch>] <new-branch>
> git branch (-c|-C) [<old-branch>] <new-branch>
> git branch (-d|-D) [-r] <branch-name>...
> git branch --edit-description [<branch-name>]
> +git branch --prune-merged <branch>...
>
> DESCRIPTION
> -----------
> @@ -201,6 +202,29 @@ This option is only applicable in non-verbose mode.
> Print the name of the current branch. In detached `HEAD` state,
> nothing is printed.
>
> +`--prune-merged <branch>...`::
> + Delete the local branches that `--forked` would list for the
> + given _<branch>_ arguments, but only those whose tip is
> + reachable from their configured upstream. In other words, the
> + work on the branch has already landed on the upstream it
> + tracks, so the local copy is no longer needed. Several
> + _<branch>_ patterns may be given, e.g. `git branch
> + --prune-merged origin/main 'feature*'`.
> ++
> +Reachability is checked against whatever the upstream refs say
> +locally; nothing is fetched. Run `git fetch` first if you want
> +the upstream refs refreshed.
Maybe
Reachability is checked against the remote-tracking branch. Run `git
fetch` first if you want update the remote-tracking branch.
> ++
> +A branch is left alone if any of the following holds:
s/left alone/not deleted/
> +its upstream no longer resolves locally; it is checked out in any
s/upstream no longer resolves locally/upstream remote-tracking branch no
longer exists/
> +worktree; or its push destination (`<branch>@{push}`) equals its
> +upstream (`<branch>@{upstream}`), so it cannot be distinguished
> +from a freshly pulled trunk that just looks "fully merged".
What's a "freshly pulled trunk"? "trunk" does not appear in gitglossary(7)
> ++
> +Branches refused by the "fully merged" safety check are listed as
> +warnings and skipped; pass them to `git branch -D` explicitly if
> +you want them gone.
s/them gone/to delete them/
> +
> `-v`::
> `-vv`::
> `--verbose`::
> diff --git a/builtin/branch.c b/builtin/branch.c
> index 2cc5a8cde0..af37a0ceb7 100644
> --- a/builtin/branch.c
> +++ b/builtin/branch.c
> @@ -38,6 +38,7 @@ static const char * const builtin_branch_usage[] = {
> N_("git branch [<options>] (-c | -C) [<old-branch>] <new-branch>"),
> N_("git branch [<options>] [-r | -a] [--points-at]"),
> N_("git branch [<options>] [-r | -a] [--format]"),
> + N_("git branch [<options>] --prune-merged <branch>..."),
> NULL
> };
>
> @@ -715,6 +716,61 @@ static int parse_opt_forked(const struct option *opt, const char *arg, int unset
> return 0;
> }
>
> +static int prune_merged_branches(int argc, const char **argv,
> + int quiet)
> +{
> + struct ref_store *refs = get_main_ref_store(the_repository);
> + struct ref_filter filter = REF_FILTER_INIT;
> + struct ref_array candidates;
> + struct strvec deletable = STRVEC_INIT;
> + int i, ret = 0;
> +
> + if (!argc)
> + die(_("--prune-merged requires at least one <branch>"));
> +
> + for (i = 0; i < argc; i++)
> + if (ref_filter_forked_add(&filter, argv[i]) < 0)
> + die(_("'%s' is not a valid branch or pattern"), argv[i]);
> +
> + filter.kind = FILTER_REFS_BRANCHES;
> + memset(&candidates, 0, sizeof(candidates));
It would be nicer to add "= { 0 }" to the declaration of candidates above.
> + filter_refs(&candidates, &filter, filter.kind);
> +
> + for (i = 0; i < candidates.nr; i++) {
> + const char *full_name = candidates.items[i]->refname;
> + const char *short_name;
> + struct branch *branch;
> + const char *upstream, *push;
> +
> + if (!skip_prefix(full_name, "refs/heads/", &short_name))
> + continue;
If we've set filter.kind = FILTER_REFS_BRANCHS how can this condition fail?
> + if (branch_checked_out(full_name))
> + continue;
> +
> + branch = branch_get(short_name);
> + upstream = branch ? branch_get_upstream(branch, NULL) : NULL;
How can branch be NULL? Don't we require branch_get() to succeed in
order to filter it?
> + if (!upstream || !refs_ref_exists(refs, upstream))
> + continue;
> + push = branch ? branch_get_push(branch, NULL) : NULL;
> + if (!push || !strcmp(push, upstream))
> + continue;
By the time we've reached this point we know that
branch@{upstream}exists and does not match branch@{push} - good
> + strvec_push(&deletable, short_name);
> + }
> +
> + if (deletable.nr)
> + ret = delete_branches(deletable.nr, deletable.v,
> + FILTER_REFS_BRANCHES,
> + DELETE_BRANCH_WARN_ONLY |
> + DELETE_BRANCH_NO_HEAD_FALLBACK |
> + (quiet ? DELETE_BRANCH_QUIET : 0));
Here we delete the branches - good.
> + OPT_BOOL(0, "prune-merged", &prune_merged,
> + N_("delete local branches whose upstream matches <branch> and is merged")),
s/is/are/
Sorry I didn't get round to reviewing these last week, I'll try and take
a look at the tests and the other patches tomorrow
Thanks
Phillip
> diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
> index 4e7deddc04..27ea1319bb 100755
> --- a/t/t3200-branch.sh
> +++ b/t/t3200-branch.sh
> @@ -1809,4 +1809,205 @@ test_expect_success '--forked requires a value' '
> test_grep "requires a value" err
> '
>
> +test_expect_success '--prune-merged: setup' '
> + test_create_repo pm-upstream &&
> + test_commit -C pm-upstream base &&
> + git -C pm-upstream checkout -b next &&
> + test_commit -C pm-upstream one-commit &&
> + test_commit -C pm-upstream two-commit &&
> + git -C pm-upstream branch one HEAD~ &&
> + git -C pm-upstream branch two HEAD &&
> + git -C pm-upstream branch wip main &&
> + git -C pm-upstream checkout main &&
> + test_create_repo pm-fork
> +'
> +
> +test_expect_success '--prune-merged deletes branches integrated into upstream' '
> + test_when_finished "rm -rf pm-merged" &&
> + git clone pm-upstream pm-merged &&
> + git -C pm-merged remote add fork ../pm-fork &&
> + test_config -C pm-merged remote.pushDefault fork &&
> + test_config -C pm-merged push.default current &&
> + git -C pm-merged branch one one-commit &&
> + git -C pm-merged branch --set-upstream-to=origin/next one &&
> + git -C pm-merged branch two two-commit &&
> + git -C pm-merged branch --set-upstream-to=origin/next two &&
> +
> + git -C pm-merged branch --prune-merged "origin/*" &&
> +
> + test_must_fail git -C pm-merged rev-parse --verify refs/heads/one &&
> + test_must_fail git -C pm-merged rev-parse --verify refs/heads/two
> +'
> +
> +test_expect_success '--prune-merged accepts a literal upstream' '
> + test_when_finished "rm -rf pm-literal" &&
> + git clone pm-upstream pm-literal &&
> + git -C pm-literal remote add fork ../pm-fork &&
> + test_config -C pm-literal remote.pushDefault fork &&
> + test_config -C pm-literal push.default current &&
> + git -C pm-literal branch one one-commit &&
> + git -C pm-literal branch --set-upstream-to=origin/next one &&
> +
> + git -C pm-literal branch --prune-merged origin/next &&
> +
> + test_must_fail git -C pm-literal rev-parse --verify refs/heads/one
> +'
> +
> +test_expect_success '--prune-merged unions multiple <branch> arguments' '
> + test_when_finished "rm -rf pm-union" &&
> + git clone pm-upstream pm-union &&
> + git -C pm-union remote add fork ../pm-fork &&
> + test_config -C pm-union remote.pushDefault fork &&
> + test_config -C pm-union push.default current &&
> + git -C pm-union branch one one-commit &&
> + git -C pm-union branch --set-upstream-to=origin/next one &&
> + git -C pm-union branch two base &&
> + git -C pm-union branch --set-upstream-to=origin/main two &&
> + git -C pm-union checkout --detach &&
> +
> + git -C pm-union branch --prune-merged origin/next origin/main &&
> +
> + test_must_fail git -C pm-union rev-parse --verify refs/heads/one &&
> + test_must_fail git -C pm-union rev-parse --verify refs/heads/two
> +'
> +
> +test_expect_success '--prune-merged accepts a local upstream' '
> + test_when_finished "rm -rf pm-local" &&
> + git clone pm-upstream pm-local &&
> + git -C pm-local remote add fork ../pm-fork &&
> + test_config -C pm-local remote.pushDefault fork &&
> + test_config -C pm-local push.default current &&
> + git -C pm-local checkout -b trunk &&
> + git -C pm-local branch one one-commit &&
> + git -C pm-local branch --set-upstream-to=trunk one &&
> + git -C pm-local merge --ff-only one-commit &&
> +
> + git -C pm-local branch --prune-merged trunk &&
> +
> + test_must_fail git -C pm-local rev-parse --verify refs/heads/one
> +'
> +
> +test_expect_success '--prune-merged warns instead of erroring on un-integrated commits' '
> + test_when_finished "rm -rf pm-unmerged" &&
> + git clone pm-upstream pm-unmerged &&
> + git -C pm-unmerged remote add fork ../pm-fork &&
> + test_config -C pm-unmerged remote.pushDefault fork &&
> + test_config -C pm-unmerged push.default current &&
> + git -C pm-unmerged checkout -b wip origin/wip &&
> + git -C pm-unmerged branch --set-upstream-to=origin/next wip &&
> + test_commit -C pm-unmerged local-only &&
> + git -C pm-unmerged checkout - &&
> +
> + git -C pm-unmerged branch --prune-merged "origin/*" 2>err &&
> + test_grep "not fully merged" err &&
> + test_grep ! "If you are sure you want to delete it" err &&
> + git -C pm-unmerged rev-parse --verify refs/heads/wip
> +'
> +
> +test_expect_success '--prune-merged is silent about not-merged-to-HEAD' '
> + test_when_finished "rm -rf pm-nohead" &&
> + git clone pm-upstream pm-nohead &&
> + git -C pm-nohead remote add fork ../pm-fork &&
> + test_config -C pm-nohead remote.pushDefault fork &&
> + test_config -C pm-nohead push.default current &&
> + git -C pm-nohead branch topic one-commit &&
> + git -C pm-nohead branch --set-upstream-to=origin/next topic &&
> +
> + git -C pm-nohead branch --prune-merged "origin/*" 2>err &&
> +
> + test_grep ! "not yet merged to HEAD" err &&
> + test_must_fail git -C pm-nohead rev-parse --verify refs/heads/topic
> +'
> +
> +test_expect_success '--prune-merged skips branches whose upstream is gone' '
> + test_when_finished "rm -rf pm-upstream-gone" &&
> + git clone pm-upstream pm-upstream-gone &&
> + git -C pm-upstream-gone remote add fork ../pm-fork &&
> + test_config -C pm-upstream-gone remote.pushDefault fork &&
> + test_config -C pm-upstream-gone push.default current &&
> + git -C pm-upstream-gone branch one one-commit &&
> + git -C pm-upstream-gone branch --set-upstream-to=origin/next one &&
> +
> + git -C pm-upstream-gone update-ref -d refs/remotes/origin/next &&
> + git -C pm-upstream-gone branch --prune-merged "origin/*" &&
> +
> + git -C pm-upstream-gone rev-parse --verify refs/heads/one
> +'
> +
> +test_expect_success '--prune-merged never deletes the checked-out branch' '
> + test_when_finished "rm -rf pm-head" &&
> + git clone pm-upstream pm-head &&
> + git -C pm-head remote add fork ../pm-fork &&
> + test_config -C pm-head remote.pushDefault fork &&
> + test_config -C pm-head push.default current &&
> + git -C pm-head checkout -b one one-commit &&
> + git -C pm-head branch --set-upstream-to=origin/next one &&
> +
> + git -C pm-head branch --prune-merged "origin/*" &&
> +
> + git -C pm-head rev-parse --verify refs/heads/one
> +'
> +
> +test_expect_success '--prune-merged spares branches that push back to their upstream' '
> + test_when_finished "rm -rf pm-push-eq" &&
> + git clone pm-upstream pm-push-eq &&
> + git -C pm-push-eq checkout --detach &&
> +
> + git -C pm-push-eq branch --prune-merged "origin/*" &&
> +
> + git -C pm-push-eq rev-parse --verify refs/heads/main
> +'
> +
> +test_expect_success '--prune-merged spares a per-branch pushRemote==upstream remote' '
> + test_when_finished "rm -rf pm-push-branch" &&
> + git clone pm-upstream pm-push-branch &&
> + git -C pm-push-branch remote add fork ../pm-fork &&
> + test_config -C pm-push-branch remote.pushDefault fork &&
> + test_config -C pm-push-branch push.default current &&
> + test_config -C pm-push-branch branch.main.pushRemote origin &&
> + git -C pm-push-branch checkout --detach &&
> +
> + git -C pm-push-branch branch --prune-merged "origin/*" &&
> +
> + git -C pm-push-branch rev-parse --verify refs/heads/main
> +'
> +
> +test_expect_success '--prune-merged prunes when @{push} differs from @{upstream}' '
> + test_when_finished "rm -rf pm-push-diff" &&
> + git clone pm-upstream pm-push-diff &&
> + git -C pm-push-diff remote add fork ../pm-fork &&
> + test_config -C pm-push-diff remote.pushDefault fork &&
> + test_config -C pm-push-diff push.default current &&
> + git -C pm-push-diff branch topic one-commit &&
> + git -C pm-push-diff branch --set-upstream-to=origin/next topic &&
> + git -C pm-push-diff checkout --detach &&
> +
> + git -C pm-push-diff branch --prune-merged "origin/*" &&
> +
> + test_must_fail git -C pm-push-diff rev-parse --verify refs/heads/topic
> +'
> +
> +test_expect_success '--prune-merged requires at least one <branch>' '
> + test_must_fail git -C forked branch --prune-merged 2>err &&
> + test_grep "requires at least one <branch>" err
> +'
> +
> +test_expect_success '--prune-merged takes positional <branch> arguments' '
> + test_when_finished "rm -rf pm-positional" &&
> + git clone pm-upstream pm-positional &&
> + git -C pm-positional remote add fork ../pm-fork &&
> + test_config -C pm-positional remote.pushDefault fork &&
> + test_config -C pm-positional push.default current &&
> + git -C pm-positional branch one one-commit &&
> + git -C pm-positional branch --set-upstream-to=origin/next one &&
> + git -C pm-positional branch two base &&
> + git -C pm-positional branch --set-upstream-to=origin/main two &&
> + git -C pm-positional checkout --detach &&
> +
> + git -C pm-positional branch --prune-merged origin/next origin/main &&
> +
> + test_must_fail git -C pm-positional rev-parse --verify refs/heads/one &&
> + test_must_fail git -C pm-positional rev-parse --verify refs/heads/two
> +'
> +
> test_done
^ permalink raw reply
* Re: [PATCH v14 1/6] branch: add --forked filter for --list mode
From: Phillip Wood @ 2026-06-15 9:46 UTC (permalink / raw)
To: Harald Nordgren via GitGitGadget, git
Cc: Kristoffer Haugsbakk, Johannes Sixt, Harald Nordgren
In-Reply-To: <7383872f4b2f422ec36b11ab5fb31cce08e6106a.1780999917.git.gitgitgadget@gmail.com>
Hi Harald
On 09/06/2026 11:11, Harald Nordgren via GitGitGadget wrote:
> From: Harald Nordgren <haraldnordgren@gmail.com>
>
> Add a --forked option to "git branch" list mode that lists only
> branches whose configured upstream matches <branch>. The argument
> can be a ref (e.g. "origin/main", "master") or a shell glob
> (e.g. "origin/*"), and may be repeated to widen the filter.
>
> It is an ordinary list filter, so it combines with the others:
>
> git branch --merged origin/main --forked 'origin/*'
>
> lists branches forked from origin that are already merged into
> origin/main, and --no-merged inverts the question.
>
> This is the building block for --prune-merged, which deletes the
> listed branches once they have landed on their upstream.
>
> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
> ---
> Documentation/git-branch.adoc | 10 +++-
> builtin/branch.c | 18 ++++++-
> ref-filter.c | 70 ++++++++++++++++++++++++++
> ref-filter.h | 10 ++++
> t/t3200-branch.sh | 92 +++++++++++++++++++++++++++++++++++
> 5 files changed, 197 insertions(+), 3 deletions(-)
It's nice to see that moving the code into the ref-filter.c has reduced
the overall number of additions by ~50 lines. The documentation and
implementation look fine though I have a couple of thoughts:
- Previous iterations supported "origin" as a short hand for the branch
origin/HEAD points to. That was nice because it means we can use the
same syntax for "git checkout -b" and "git branch --forked". It
would probably be a good idea to support it.
- We could probably be a bit smarter about the way we handle patterns
by copying what dwim_ref() does to support things like
remotes/origin/* but I don't think we need to do that now.
> diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
> index e7829c2c4b..4e7deddc04 100755
> --- a/t/t3200-branch.sh
> +++ b/t/t3200-branch.sh
> @@ -1717,4 +1717,96 @@ test_expect_success 'errors if given a bad branch name' '
> test_cmp expect actual
> '
>
> +test_expect_success '--forked: setup' '
> + test_create_repo forked-upstream &&
> + test_commit -C forked-upstream base &&
> + git -C forked-upstream branch one base &&
> + git -C forked-upstream branch two base &&
> +
> + test_create_repo forked-other &&
> + test_commit -C forked-other other-base &&
> + git -C forked-other branch foreign other-base &&
> +
> + git clone forked-upstream forked &&
> + git -C forked remote add other ../forked-other &&
We can use "add -f" to fetch here rather than doing it separately.
> + git -C forked fetch other &&
> + git -C forked branch local-base &&
> + git -C forked branch --track local-one origin/one &&
> + git -C forked branch --track local-two origin/two &&
> + git -C forked branch --track local-foreign other/foreign &&
> + git -C forked branch detached &&
Normally we use "detached" to mean no branch, lets read on and see how
this is used ...
> + git -C forked branch --track local-trunk local-base
> +'
> +
> +test_expect_success '--forked <upstream-tracking-branch> filters by upstream' '
> + git -C forked branch --forked origin/one --format="%(refname:short)" >actual &&
origin/one and origin/two point to the same commit, so this demonstrates
that we're checking the branch names, not the topology which is good.
All of the local branches point at their upstream which isn't very
realistic - I wonder if we should add some local commits?
The tests all look sensible, but there is no coverage for combining
--forked with branch names as in
git branch --forked <arg> <branch>
Thanks
Phillip
> + echo local-one >expect &&
> + test_cmp expect actual
> +'
> +
> +test_expect_success '--forked <glob> filters by wildmatch' '
> + git -C forked branch --forked "origin/*" --format="%(refname:short)" >actual &&
> + cat >expect <<-\EOF &&
> + local-one
> + local-two
> + main
> + EOF
> + test_cmp expect actual
> +'
> +
> +test_expect_success '--forked <local-branch> matches branches with local upstream' '
> + git -C forked branch --forked local-base --format="%(refname:short)" >actual &&
> + echo local-trunk >expect &&
> + test_cmp expect actual
> +'
> +
> +test_expect_success '--forked can be repeated to widen the filter' '
> + git -C forked branch --forked origin/one --forked other/foreign --format="%(refname:short)" >actual &&
> + cat >expect <<-\EOF &&
> + local-foreign
> + local-one
> + EOF
> + test_cmp expect actual
> +'
> +
> +test_expect_success '--forked combines literal and glob arguments' '
> + git -C forked branch --forked local-base --forked "other/*" --format="%(refname:short)" >actual &&
> + cat >expect <<-\EOF &&
> + local-foreign
> + local-trunk
> + EOF
> + test_cmp expect actual
> +'
> +
> +test_expect_success '--forked "*/*" covers every remote-tracking upstream' '
> + git -C forked branch --forked "*/*" --format="%(refname:short)" >actual &&
> + cat >expect <<-\EOF &&
> + local-foreign
> + local-one
> + local-two
> + main
> + EOF
> + test_cmp expect actual
> +'
> +
> +test_expect_success '--forked composes with --no-merged' '
> + test_when_finished "git -C forked checkout detached" &&
> + git -C forked checkout local-one &&
> + test_commit -C forked local-only &&
> + git -C forked branch --forked "origin/*" --no-merged origin/one \
> + --format="%(refname:short)" >actual &&
> + echo local-one >expect &&
> + test_cmp expect actual
> +'
> +
> +test_expect_success '--forked rejects unknown branch/pattern' '
> + test_must_fail git -C forked branch --forked nope 2>err &&
> + test_grep "not a valid branch or pattern" err
> +'
> +
> +test_expect_success '--forked requires a value' '
> + test_must_fail git -C forked branch --forked 2>err &&
> + test_grep "requires a value" err
> +'
> +
> test_done
^ permalink raw reply
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