* Re: [PATCH v2 0/5] builtin/refs: add ability to write references
From: Junio C Hamano @ 2026-06-17 12:26 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git
In-Reply-To: <20260617-pks-refs-writing-subcommands-v2-0-07f3d18336f9@pks.im>
Patrick Steinhardt <ps@pks.im> writes:
> Hi,
>
> Reference-related functionality in Git is currently spread across many
> different commands: git-update-ref(1), git-for-each-ref(1),
> git-show-ref(1), git-pack-refs(1) and git-symbolic-ref(1). This makes it
> hard for users to discover what functionality we have available to work
> with references.
>
> We have thus started to consolidate this functionality into git-refs(1),
> which is a toolbox of everything related to references. Until now, the
> command doesn't handle functionality of git-update-ref(1).
>
> This patch series backfills most of the functionality by introducing
> three new commands:
>
> - `git refs delete` to delete references. This is the equivalent of
> `git update-ref -d`.
>
> - `git refs update` to update references. This is the equivalent of
> `git update-ref <refname> <oldvalue> <newvalue>`.
>
> - `git refs rename` to rename a reference, including its reflog. This
> does not have an equivalent in git-update-ref(1), but is inspired by
> and supersedes [1].
... and `git refs create`, but we can guess what it would do ;-).
Will queue. Thanks.
^ permalink raw reply
* Re: [PATCH] rebase: mention --abort alongside --continue
From: Junio C Hamano @ 2026-06-17 12:19 UTC (permalink / raw)
To: Phillip Wood; +Cc: Harald Nordgren via GitGitGadget, git, Harald Nordgren
In-Reply-To: <bd7dc183-6597-4fd0-ae64-682d46480cd4@gmail.com>
Phillip Wood <phillip.wood123@gmail.com> writes:
>> It is very true that users who know what they are doing and got into
>> such conflicts are opted to go into such a situation tnat it is
>> unlikely that they would appreciate a choice to abort.
>
> That's not quite what I was trying to say which was that aborting in the
> case of conflicts is more likely than in the case of a failed exec.
Ah, I misread the intention. And I agree with you that "failed
test" case is very likely to lead to "further changes/amends" and
not "aborted rebase".
> So if I've understood we'd print a message explaining what's happened
> and how to continue followed by a hint about aborting. The message would
> depend on what problem caused the rebase to stop, but the hint would be
> the same in each case. That sounds fine to me.
Yeah, and "failed test" would not be one of the problem that would
invite the hint to "abort". I am OK with that, too. FWIW, I am OK
if the "you can abort" hint cannot be configured away, either ;-)
^ permalink raw reply
* Re: [PATCH 4/4] builtin/refs: add "rename" subcommand
From: Junio C Hamano @ 2026-06-17 12:13 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git
In-Reply-To: <ajJMqayXuie1FyIW@pks.im>
Patrick Steinhardt <ps@pks.im> writes:
>> If we rename a ref that does not have a reflog, would it leave the
>> ref under the new name without reflog, or would we get a reflog with
>> a single entry that marks the fact the old ref was renamed into the
>> new ref? Should that be controlled via --create-reflog option?
>
> It would leave it without a reflog. In theory I agree that it might make
> sense to introduce a "--create-reflog" option, but that would require
> some new plumbing in `refs_rename_ref()`. So I'd say that we can add it
> at a later point as needed.
OK.
^ permalink raw reply
* Re: [PATCH 3/4] builtin/refs: add "update" subcommand
From: Junio C Hamano @ 2026-06-17 12:11 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git
In-Reply-To: <ajJMnZchqdpiuKTg@pks.im>
Patrick Steinhardt <ps@pks.im> writes:
> We can:
>
> $ git update-ref $NEW_OID $NULL_OID
> $ git refs update $NEW_OID $NULL_OID
>
> This will verify that the reference doesn't exist before actually
> writing it. Will add a test.
I think refname is missing from the command line, but the above is
good. I forgot we had update-ref already doing that ;-)
Thanks.
^ permalink raw reply
* Re: How does GitGitGadget generate range-diffs, was Re: [PATCH v2 0/6] Support hashing objects larger than 4GB on Windows
From: Junio C Hamano @ 2026-06-17 11:58 UTC (permalink / raw)
To: Johannes Schindelin
Cc: Johannes Schindelin via GitGitGadget, git, Philip Oakley,
Patrick Steinhardt
In-Reply-To: <fcb9e52a-5f71-1fd0-a18e-c48e22e6e28c@gmx.de>
Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
> GitGitGadget is using range-diff to compare between iterations of
> essentially the same patches, therefore it encourages `range-diff` to try
> harder to look for matches via `--creation-factor=95`:
Thanks, I'll use the matching 95 in my local "sanity check after
applying" step. As you say, it is not like comparing an integration
branch with many topics with the same integration branch from a
different day, which would need to avoid misidentifying two unrelted
ones as if they are related, so the tool should asssume most of them
match with each other.
^ permalink raw reply
* Re: [PATCH] osxkeychain: fix build with Rust
From: Junio C Hamano @ 2026-06-17 11:54 UTC (permalink / raw)
To: Johannes Schindelin via GitGitGadget; +Cc: git, Johannes Schindelin
In-Reply-To: <pull.2154.git.1781691074710.gitgitgadget@gmail.com>
"Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com>
writes:
> From: Johannes Schindelin <johannes.schindelin@gmx.de>
>
> Without NO_RUST defined, the varint encoder/decoder lives in the
> RUST_LIB, which needs to be linked. Symptom:
>
> cc [... -o contrib/credential/osxkeychain/git-credential-osxkeychain [...]
> Undefined symbols for architecture x86_64:
> "_decode_varint", referenced from:
> _read_untracked_extension in libgit.a[x86_64][63](dir.o)
> _read_untracked_extension in libgit.a[x86_64][63](dir.o)
> _read_one_dir in libgit.a[x86_64][63](dir.o)
> _read_one_dir in libgit.a[x86_64][63](dir.o)
> _load_cache_entry_block in libgit.a[x86_64][174](read-cache.o)
> "_encode_varint", referenced from:
> _write_untracked_extension in libgit.a[x86_64][63](dir.o)
> _write_untracked_extension in libgit.a[x86_64][63](dir.o)
> _write_untracked_extension in libgit.a[x86_64][63](dir.o)
> _write_one_dir in libgit.a[x86_64][63](dir.o)
> _write_one_dir in libgit.a[x86_64][63](dir.o)
> _do_write_index in libgit.a[x86_64][174](read-cache.o)
> ld: symbol(s) not found for architecture x86_64
>
> While it is curious why these functions are needed at all (osxkeychain
> does not read or write the index), the compile error is a real problem.
>
> Instead of trying to play games to add `GITLIBS` while filtering out
> `common-main.o`, replace the `$(LIB_FILE) $(EXTLIBS)` construct with the
> much shorter `$(LIBS)` construct that _already_ filters out
> `common-main.o` and adds the Rust library when needed.
>
> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> ---
Hmph, we do not build this at GitHub Actions based CI? Just being
curious.
Let me take this directly to 'master' before tagging -rc1. Thanks.
> osxkeychain: fix build with Rust
^ permalink raw reply
* Re: [PATCH v2 0/7] Introduce fetch.followRemoteHEAD config variable
From: Junio C Hamano @ 2026-06-17 11:51 UTC (permalink / raw)
To: Matt Hunter; +Cc: git, Bence Ferdinandy, Jeff King
In-Reply-To: <xmqqh5n213bw.fsf@gitster.g>
Junio C Hamano <gitster@pobox.com> writes:
> ... to some "unspecified" or "default" value? What does the existing
> parser routine for remote.*.followremotehead do?
>
> Ideally,
>
> (1) If the "fetch" operation ends up with not needing to consult
> the value of fetch.followRemoteHEAD at all (e.g., it is a
> one-shot fetch that updates no remote-tracking hierarchy, or it
> has a more specific per-remote setting that this variable is
> meant to serve as a mere fallback), any bogus or unknown value
> will not get any warning.
>
> (2) If fetch.followRemoteHEAD ends up being _used_, and if it has
> an unknown value, we should at least warn "we do not understand
> what you wrote, 'awlays', and we ignore it", or die "we do not
> understand 'reset', perhaps it is from a future version of Git?".
>
> I do not think customization based on git_config() callback like the
> above can easily implement such an ideal semantics.
>
> And I suspect that the existing per-remote configuration that this
> variable is meant to serve as a fallback definition would not work
> in such an ideal way (i.e., even if we are doing one-shot fetch that
> does not touch any remote-tracking hierarchies, "git fetch" may warn
> if the value is not understood, and when we do need the value, the
> code would only warn and does not die), ...
Having said all that, I do not think it is a blocker for this series
that it does not take us into the more ideal direction and still
makes a syntax check on a value that will not be used and complains
to the user. We may want an in-code NEEDSWORK comment to hint
future developers that they may want to revamp both of the code
paths for fetch.followRemoteHEAD and remote.*.followremotehead not
to complain when the values are unneeded and die when the unrecognized
value is needed to continue, though.
Other than that, this looks excellent. Thanks.
^ permalink raw reply
* Re: [PATCH v15 0/7] branch: delete-merged
From: Harald Nordgren @ 2026-06-17 11:17 UTC (permalink / raw)
To: Phillip Wood
Cc: Harald Nordgren via GitGitGadget, git, Kristoffer Haugsbakk,
Johannes Sixt
In-Reply-To: <f68e2a11-02a5-47b9-a01a-458eba821c37@gmail.com>
On Wed, Jun 17, 2026 at 12:01 PM Phillip Wood <phillip.wood123@gmail.com> wrote:
>
> Hi Harald
>
> Our SubmittingPatches documentation recommends waiting for the
> discussion to settle before sending a new version. When you know someone
> is going send more comments on a series it is a good idea to wait for
> them before sending a new version to avoid too much churn on the list
> which makes it hard for people to keep up. I'm not going to read this
> version in detail because I know another version will be needed but I
> did spot a couple of things in the summary below.
Got it. I think I am waiting a fair bit between sending new versions.
My last version here was 2 days ago.
> Not changing force sounds like a bad idea. The whole point of unpacking
> the flags at the start of the function is to avoid accidental
> regressions. Unpacking the flags into separate variables means the rest
> of the function does not need to know that the function arguments have
> changed.
My reason for keeping it like this was to avoid the slightly awkward
double re-assignment of both flag and boolean:
```
case FILTER_REFS_REMOTES:
...
flags |= DELETE_BRANCH_FORCE;
force = true;
```
But your way is likely still better, because the definitions at the
top of the function are clearer.
Harald
^ permalink raw reply
* [PATCH v2] checkout/switch: add --create-if-missing option
From: Lei Zhu via GitGitGadget @ 2026-06-17 10:57 UTC (permalink / raw)
To: git; +Cc: Lei Zhu, Korov
In-Reply-To: <pull.2324.git.git.1780997009796.gitgitgadget@gmail.com>
From: Korov <korov9.c@gmail.com>
Add a new `--create-if-missing` option to `git switch` and `git
checkout` that behaves like an idempotent form of branch switching.
Users who often switch between topic branches may not know whether the
local branch already exists. Without this option, they need to check for
the branch first and then choose between switching to it or creating it.
The new option folds that workflow into a single command.
When the target branch does not exist, `--create-if-missing <branch>`
behaves like `git switch -c <branch>` or `git checkout -b <branch>`,
including existing `--track` and `--no-track` handling.
When the target branch already exists, `--create-if-missing <branch>`
switches to it without resetting the branch tip. If `--track` is given,
update the branch's upstream configuration using the explicit
start-point, or the current branch when no start-point is provided. Fail
in detached HEAD state when no start-point is available for tracking
setup.
For `git checkout`, keep this as a branch operation and reject pathspec
usage with `--create-if-missing` to avoid mixing branch switching with
path checkout semantics.
Document the new option and add tests covering branch creation,
existing-branch switching, tracking updates, pathspec rejection, and
detached-HEAD failure cases.
Signed-off-by: Korov <korov9.c@gmail.com>
---
checkout/switch: add --create-if-missing option
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2324%2FKorov%2Fdev3-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2324/Korov/dev3-v2
Pull-Request: https://github.com/git/git/pull/2324
Range-diff vs v1:
1: 64a6947ad1 ! 1: 0070592d49 switch: add --ensure option
@@ Metadata
Author: Korov <korov9.c@gmail.com>
## Commit message ##
- switch: add --ensure option
+ checkout/switch: add --create-if-missing option
- Add a new `git switch --ensure` (`-e`) option that behaves like an
- idempotent form of branch switching.
+ Add a new `--create-if-missing` option to `git switch` and `git
+ checkout` that behaves like an idempotent form of branch switching.
Users who often switch between topic branches may not know whether the
- local branch already exists. Without this option, they need to check
- for the branch first and then choose between `git switch <branch>` and
- `git switch -c <branch>`. The new option folds that workflow into a
- single command.
+ local branch already exists. Without this option, they need to check for
+ the branch first and then choose between switching to it or creating it.
+ The new option folds that workflow into a single command.
- When the target branch does not exist, `git switch -e <branch>`
- behaves like `git switch -c <branch>`, including existing `--track`
- and `--no-track` handling.
+ When the target branch does not exist, `--create-if-missing <branch>`
+ behaves like `git switch -c <branch>` or `git checkout -b <branch>`,
+ including existing `--track` and `--no-track` handling.
- When the target branch already exists, `git switch -e <branch>`
- switches to it without resetting the branch tip. If `--track` is
- given, update the branch's upstream configuration using the explicit
- start-point, or the current branch when no start-point is provided.
- Fail in detached HEAD state when no start-point is available for
- tracking setup.
+ When the target branch already exists, `--create-if-missing <branch>`
+ switches to it without resetting the branch tip. If `--track` is given,
+ update the branch's upstream configuration using the explicit
+ start-point, or the current branch when no start-point is provided. Fail
+ in detached HEAD state when no start-point is available for tracking
+ setup.
- Document the new option and add tests covering create-branch tracking,
- existing-branch tracking updates, and detached-HEAD failure cases.
+ For `git checkout`, keep this as a branch operation and reject pathspec
+ usage with `--create-if-missing` to avoid mixing branch switching with
+ path checkout semantics.
+
+ Document the new option and add tests covering branch creation,
+ existing-branch switching, tracking updates, pathspec rejection, and
+ detached-HEAD failure cases.
Signed-off-by: Korov <korov9.c@gmail.com>
+ ## Documentation/git-checkout.adoc ##
+@@ Documentation/git-checkout.adoc: git checkout [-q] [-f] [-m] [<branch>]
+ git checkout [-q] [-f] [-m] --detach [<branch>]
+ git checkout [-q] [-f] [-m] [--detach] <commit>
+ git checkout [-q] [-f] [-m] [[-b|-B|--orphan] <new-branch>] [<start-point>]
++git checkout [-q] [-f] [-m] --create-if-missing <branch> [<start-point>]
+ git checkout <tree-ish> [--] <pathspec>...
+ git checkout <tree-ish> --pathspec-from-file=<file> [--pathspec-file-nul]
+ git checkout [-f|--ours|--theirs|-m|--conflict=<style>] [--] <pathspec>...
+@@ Documentation/git-checkout.adoc: This will fail if there's an error checking out _<new-branch>_, for
+ example if checking out the `<start-point>` commit would overwrite your
+ uncommitted changes.
+
++`git checkout --create-if-missing <branch> [<start-point>]`::
++
++ Check out _<branch>_ if it already exists, or create it from
++ _<start-point>_ before checking it out if it does not.
+++
++When _<branch>_ does not already exist, this behaves like
++`git checkout -b <branch> [<start-point>]`, including any `--track`
++or `--no-track` options.
+++
++When _<branch>_ already exists, the branch tip is not changed. If
++`--track[=(direct|inherit)]` is given, the existing branch's upstream
++configuration is updated using _<start-point>_ when one is provided,
++or the current branch when _<start-point>_ is omitted. This form fails
++when `HEAD` is detached and no _<start-point>_ is given.
++
+ `git checkout -B <branch> [<start-point>]`::
+
+ The same as `-b`, except that if the branch already exists it
+@@ Documentation/git-checkout.adoc: of it").
+ The same as `-b`, except that if the branch already exists it
+ resets _<branch>_ to the start point instead of failing.
+
++`--create-if-missing <branch>`::
++ Check out _<branch>_ if it already exists, or create it from
++ _<start-point>_ before checking it out if it does not.
+++
++When _<branch>_ does not already exist, this behaves like
++`git checkout -b <branch> [<start-point>]`, including any `--track`
++or `--no-track` options.
+++
++When _<branch>_ already exists, the branch tip is not changed. If
++`--track[=(direct|inherit)]` is given, the existing branch's upstream
++configuration is updated using _<start-point>_ when one is provided,
++or the current branch when _<start-point>_ is omitted. This form fails
++when `HEAD` is detached and no _<start-point>_ is given.
+++
++This option cannot be used when checking out paths.
++
+ `-t`::
+ `--track[=(direct|inherit)]`::
+ When creating a new branch, set up "upstream" configuration. See
+
## Documentation/git-switch.adoc ##
@@ Documentation/git-switch.adoc: SYNOPSIS
git switch [<options>] [--no-guess] <branch>
git switch [<options>] --detach [<start-point>]
git switch [<options>] (-c|-C) <new-branch> [<start-point>]
-+git switch [<options>] -e <branch> [<start-point>]
++git switch [<options>] --create-if-missing <branch> [<start-point>]
git switch [<options>] --orphan <new-branch>
DESCRIPTION
@@ Documentation/git-switch.adoc: $ git branch -f _<new-branch>_
$ git switch _<new-branch>_
------------
-+`-e <branch>`::
-+`--ensure <branch>`::
++`--create-if-missing <branch>`::
+ Switch to _<branch>_ if it already exists, or create it from
+ _<start-point>_ before switching to it if it does not.
++
@@ builtin/checkout.c: struct checkout_opts {
const char *new_branch;
const char *new_branch_force;
const char *new_orphan_branch;
-+ const char *ensure_branch;
-+ const char *ensure_branch_start;
++ const char *create_if_missing_branch;
++ const char *create_if_missing_start;
int new_branch_log;
enum branch_track track;
struct diff_options diff_options;
+@@ builtin/checkout.c: static int checkout_paths(const struct checkout_opts *opts,
+ die(_("Cannot update paths and switch to branch '%s' at the same time."),
+ opts->new_branch);
+
++ if (opts->create_if_missing_branch)
++ die(_("Cannot update paths and switch to branch '%s' at the same time."),
++ opts->create_if_missing_branch);
++
+ if (!opts->checkout_worktree && !opts->checkout_index)
+ die(_("neither '%s' or '%s' is specified"),
+ "--staged", "--worktree");
@@ builtin/checkout.c: static void update_refs_for_switch(const struct checkout_opts *opts,
free(new_branch_info->refname);
new_branch_info->name = xstrdup(opts->new_branch);
setup_branch_path(new_branch_info);
-+ } else if (opts->ensure_branch && opts->branch_exists &&
++ } else if (opts->create_if_missing_branch && opts->branch_exists &&
+ opts->track != BRANCH_TRACK_UNSPECIFIED) {
-+ const char *tracking_source = opts->ensure_branch_start ?
-+ opts->ensure_branch_start :
++ const char *tracking_source = opts->create_if_missing_start ?
++ opts->create_if_missing_start :
+ old_branch_info->name;
-+ dwim_and_setup_tracking(the_repository, opts->ensure_branch,
++ dwim_and_setup_tracking(the_repository, opts->create_if_missing_branch,
+ tracking_source, opts->track,
+ opts->quiet);
-+ remote_state_clear(the_repository->remote_state);
}
old_desc = old_branch_info->name;
+@@ builtin/checkout.c: static void update_refs_for_switch(const struct checkout_opts *opts,
+ fprintf(stderr, _("Switched to and reset branch '%s'\n"), new_branch_info->name);
+ else
+ fprintf(stderr, _("Switched to a new branch '%s'\n"), new_branch_info->name);
++ } else if (opts->create_if_missing_branch &&
++ opts->branch_exists) {
++ fprintf(stderr, _("Switched to existing branch '%s'\n"),
++ new_branch_info->name);
+ } else {
+ fprintf(stderr, _("Switched to branch '%s'\n"),
+ new_branch_info->name);
@@ builtin/checkout.c: static int checkout_main(int argc, const char **argv, const char *prefix,
die(_("options '-%c', '-%c', and '%s' cannot be used together"),
cb_option, toupper(cb_option), "--orphan");
-+ if (opts->ensure_branch) {
++ if (opts->create_if_missing_branch) {
+ struct strbuf ref = STRBUF_INIT;
+ int exists;
+
+ if (opts->new_branch || opts->new_branch_force || opts->new_orphan_branch)
-+ die(_("'%s' cannot be used with '%s'"), "-e", "-c/-C/--orphan");
++ die(_("'%s' cannot be used with '%s'"), "--create-if-missing", "-c/-C/--orphan");
+ if (opts->force_detach)
-+ die(_("'%s' cannot be used with '%s'"), "-e", "--detach");
++ die(_("'%s' cannot be used with '%s'"), "--create-if-missing", "--detach");
+
-+ exists = validate_branchname(opts->ensure_branch, &ref);
++ exists = validate_branchname(opts->create_if_missing_branch, &ref);
+ strbuf_release(&ref);
+
+ /* Save an explicit start point for tracking setup. */
+ if (argc > 0 && opts->track != BRANCH_TRACK_UNSPECIFIED)
-+ opts->ensure_branch_start = argv[0];
++ opts->create_if_missing_start = argv[0];
+
+ if (exists) {
+ /*
@@ builtin/checkout.c: static int checkout_main(int argc, const char **argv, const
+ opts->branch_exists = 1;
+ } else {
+ /* Branch doesn't exist: create it like -c */
-+ opts->new_branch = opts->ensure_branch;
++ opts->new_branch = opts->create_if_missing_branch;
+ }
+ }
+
-+ if (opts->ensure_branch && opts->branch_exists &&
++ if (opts->create_if_missing_branch && opts->branch_exists &&
+ opts->track != BRANCH_TRACK_UNSPECIFIED &&
-+ !opts->ensure_branch_start) {
++ !opts->create_if_missing_start) {
+ struct object_id head_oid;
+ char *head = refs_resolve_refdup(get_main_ref_store(the_repository),
+ "HEAD", 0, &head_oid, NULL);
@@ builtin/checkout.c: static int checkout_main(int argc, const char **argv, const
- /* --track without -c/-C/-b/-B/--orphan should DWIM */
- if (opts->track != BRANCH_TRACK_UNSPECIFIED && !opts->new_branch) {
-+ /* --track without -c/-C/-b/-B/--orphan/-e should DWIM */
++ /* --track without -c/-C/-b/-B/--orphan/--create-if-missing should DWIM */
+ if (opts->track != BRANCH_TRACK_UNSPECIFIED && !opts->new_branch &&
-+ !(opts->ensure_branch && opts->branch_exists)) {
++ !(opts->create_if_missing_branch && opts->branch_exists)) {
const char *argv0 = argv[0];
if (!argc || !strcmp(argv0, "--"))
die(_("--track needs a branch name"));
@@ builtin/checkout.c: static int checkout_main(int argc, const char **argv, const
}
+ /*
-+ * Handle -e with existing branch: set up new_branch_info to switch
-+ * to the existing branch.
++ * Handle --create-if-missing with existing branch: set up
++ * new_branch_info to switch to the existing branch.
+ */
-+ if (opts->ensure_branch && opts->branch_exists) {
++ if (opts->create_if_missing_branch && opts->branch_exists) {
+ struct object_id rev;
+
++ if (repo_get_oid_mb(the_repository, opts->create_if_missing_branch,
++ &rev))
++ die(_("could not resolve '%s'"),
++ opts->create_if_missing_branch);
++
+ branch_info_release(&new_branch_info);
+ memset(&new_branch_info, 0, sizeof(new_branch_info));
-+ new_branch_info.name = xstrdup(opts->ensure_branch);
-+ setup_branch_path(&new_branch_info);
-+
-+ if (new_branch_info.path &&
-+ !refs_read_ref(get_main_ref_store(the_repository),
-+ new_branch_info.path, &rev)) {
-+ new_branch_info.commit = lookup_commit_reference_gently(
-+ the_repository, &rev, 1);
-+ if (new_branch_info.commit)
-+ parse_commit_or_die(new_branch_info.commit);
-+ }
++ setup_new_branch_info_and_source_tree(&new_branch_info, opts, &rev,
++ opts->create_if_missing_branch);
+ }
+
if (argc) {
parse_pathspec(&opts->pathspec, 0,
opts->patch_mode ? PATHSPEC_PREFIX_ORIGIN : 0,
+@@ builtin/checkout.c: int cmd_checkout(int argc,
+ N_("create and checkout a new branch")),
+ OPT_STRING('B', NULL, &opts.new_branch_force, N_("branch"),
+ N_("create/reset and checkout a branch")),
++ OPT_STRING_F(0, "create-if-missing", &opts.create_if_missing_branch, N_("branch"),
++ N_("create if needed and checkout branch"),
++ PARSE_OPT_NONEG),
+ OPT_BOOL('l', NULL, &opts.new_branch_log, N_("create reflog for new branch")),
+ OPT_BOOL(0, "guess", &opts.dwim_new_local_branch,
+ N_("second guess 'git checkout <no-such-branch>' (default)")),
@@ builtin/checkout.c: int cmd_switch(int argc,
N_("create and switch to a new branch")),
OPT_STRING('C', "force-create", &opts.new_branch_force, N_("branch"),
N_("create/reset and switch to a branch")),
-+ OPT_STRING('e', "ensure", &opts.ensure_branch, N_("branch"),
-+ N_("create if needed and switch to branch")),
++ OPT_STRING_F(0, "create-if-missing", &opts.create_if_missing_branch, N_("branch"),
++ N_("create if needed and switch to branch"),
++ PARSE_OPT_NONEG),
OPT_BOOL(0, "guess", &opts.dwim_new_local_branch,
N_("second guess 'git switch <no-such-branch>'")),
OPT_BOOL(0, "discard-changes", &opts.discard_changes,
+ ## contrib/completion/git-completion.bash ##
+@@ contrib/completion/git-completion.bash: _git_checkout ()
+ local dwim_opt="$(__git_checkout_default_dwim_mode)"
+
+ case "$prev" in
+- -b|-B|--orphan)
++ -b|-B|--orphan|--create-if-missing)
+ # Complete local branches (and DWIM branch
+ # remote branch names) for an option argument
+ # specifying a new branch name. This is for
+@@ contrib/completion/git-completion.bash: _git_checkout ()
+ ;;
+ *)
+ # At this point, we've already handled special completion for
+- # the arguments to -b/-B, and --orphan. There are 3 main
+- # things left we can possibly complete:
+- # 1) a start-point for -b/-B, -d/--detach, or --orphan
++ # the arguments to -b/-B, --orphan, and
++ # --create-if-missing. There are 3 main things left
++ # we can possibly complete:
++ # 1) a start-point for -b/-B, -d/--detach, --orphan,
++ # or --create-if-missing
+ # 2) a remote head, for --track
+ # 3) an arbitrary reference, possibly including DWIM names
+ #
+
+- if [ -n "$(__git_find_on_cmdline "-b -B -d --detach --orphan")" ]; then
++ if [ -n "$(__git_find_on_cmdline "-b -B -d --detach --orphan --create-if-missing")" ]; then
+ __git_complete_refs --mode="refs"
+ elif [ -n "$(__git_find_on_cmdline "-t --track")" ]; then
+ __git_complete_refs --mode="remote-heads"
+@@ contrib/completion/git-completion.bash: _git_switch ()
+ local dwim_opt="$(__git_checkout_default_dwim_mode)"
+
+ case "$prev" in
+- -c|-C|--orphan)
++ -c|-C|--orphan|--create-if-missing)
+ # Complete local branches (and DWIM branch
+ # remote branch names) for an option argument
+ # specifying a new branch name. This is for
+@@ contrib/completion/git-completion.bash: _git_switch ()
+ fi
+
+ # At this point, we've already handled special completion for
+- # -c/-C, and --orphan. There are 3 main things left to
+- # complete:
+- # 1) a start-point for -c/-C or -d/--detach
++ # -c/-C, --orphan, and --create-if-missing. There
++ # are 3 main things left to complete:
++ # 1) a start-point for -c/-C, -d/--detach, or --create-if-missing
+ # 2) a remote head, for --track
+ # 3) a branch name, possibly including DWIM remote branches
+
+- if [ -n "$(__git_find_on_cmdline "-c -C -d --detach")" ]; then
++ if [ -n "$(__git_find_on_cmdline "-c -C -d --detach --create-if-missing")" ]; then
+ __git_complete_refs --mode="refs"
+ elif [ -n "$(__git_find_on_cmdline "-t --track")" ]; then
+ __git_complete_refs --mode="remote-heads"
+
+ ## t/t2018-checkout-branch.sh ##
+@@ t/t2018-checkout-branch.sh: test_expect_success 'checkout -B to the current branch works' '
+ test_dirty_mergeable
+ '
+
++test_expect_success 'checkout --create-if-missing creates a branch' '
++ test_when_finished "
++ git checkout branch1 &&
++ test_might_fail git branch -D create-if-missing-new
++ " &&
++ git checkout --create-if-missing create-if-missing-new $HEAD1 &&
++ echo refs/heads/create-if-missing-new >expect &&
++ git symbolic-ref HEAD >actual &&
++ test_cmp expect actual &&
++ test_cmp_rev $HEAD1 HEAD
++'
++
++test_expect_success 'checkout --create-if-missing switches to existing branch' '
++ test_when_finished "
++ git checkout branch1 &&
++ test_might_fail git branch -D create-if-missing-existing
++ " &&
++ git branch create-if-missing-existing $HEAD1 &&
++ git checkout branch1 &&
++ git checkout --create-if-missing create-if-missing-existing 2>err &&
++ test_grep "Switched to existing branch '\''create-if-missing-existing'\''" err &&
++ echo refs/heads/create-if-missing-existing >expect &&
++ git symbolic-ref HEAD >actual &&
++ test_cmp expect actual &&
++ test_cmp_rev $HEAD1 HEAD
++'
++
+ test_expect_success 'checkout -b after clone --no-checkout does a checkout of HEAD' '
+ git init src &&
+ test_commit -C src a &&
+@@ t/t2018-checkout-branch.sh: test_expect_success 'checkout -b rejects an extra path argument' '
+ test_grep "Cannot update paths and switch to branch" err
+ '
+
++test_expect_success 'checkout --create-if-missing rejects a path argument' '
++ test_when_finished "
++ git checkout branch1 &&
++ test_might_fail git branch -D create-if-missing-path
++ " &&
++ git branch create-if-missing-path branch1 &&
++ test_must_fail git checkout --create-if-missing create-if-missing-path -- file1 2>err &&
++ test_grep "Cannot update paths and switch to branch '\''create-if-missing-path'\''" err
++'
++
+ test_done
+
+ ## t/t2027-checkout-track.sh ##
+@@ t/t2027-checkout-track.sh: test_expect_success 'checkout --track -b creates a new tracking branch' '
+ test $(git config --get branch.branch1.merge) = refs/heads/main
+ '
+
++test_expect_success 'checkout --create-if-missing --track creates branch from current branch' '
++ test_when_finished "
++ git checkout main &&
++ git branch -D branch2
++ " &&
++ git checkout main &&
++ git checkout --create-if-missing branch2 --track &&
++ test $(git rev-parse --abbrev-ref HEAD) = branch2 &&
++ test_cmp_config . branch.branch2.remote &&
++ test_cmp_config refs/heads/main branch.branch2.merge
++'
++
++test_expect_success 'checkout --create-if-missing --track uses current branch for existing branch' '
++ test_when_finished "
++ git checkout main &&
++ git branch -D branch3 branch3-source
++ " &&
++ git checkout -b branch3-source main &&
++ git branch branch3 main &&
++ git checkout --create-if-missing branch3 --track >out 2>err &&
++ test_grep "branch '\''branch3'\'' set up to track '\''branch3-source'\''." out &&
++ test_grep "Switched to existing branch '\''branch3'\''" err &&
++ test_cmp_config . branch.branch3.remote &&
++ test_cmp_config refs/heads/branch3-source branch.branch3.merge
++'
++
++test_expect_success 'checkout --create-if-missing --track fails from detached HEAD without start-point' '
++ test_when_finished "
++ git checkout main &&
++ git branch -D branch4
++ " &&
++ git branch branch4 main &&
++ git checkout --detach main &&
++ test_must_fail git checkout --create-if-missing branch4 --track 2>err &&
++ test_grep "cannot set up tracking information; starting point '\''HEAD'\'' is not a branch" err
++'
++
+ test_expect_success 'checkout --track -b rejects an extra path argument' '
+ test_must_fail git checkout --track -b branch2 main one.t 2>err &&
+ test_grep "cannot be used with updating paths" err
+
## t/t2060-switch.sh ##
@@ t/t2060-switch.sh: test_expect_success 'tracking info copied with autoSetupMerge=inherit' '
test_cmp_config "" --default "" branch.main2.merge
'
-+test_expect_success 'switch -e --track creates branch from current branch' '
++test_expect_success 'switch --create-if-missing --track creates branch from current branch' '
+ test_when_finished "
+ git switch main || :
+ git branch -D ensure-new-current || :
+ " &&
+ git switch main &&
-+ git switch -e ensure-new-current --track &&
++ git switch --create-if-missing ensure-new-current --track &&
+ test_cmp_rev refs/heads/main refs/heads/ensure-new-current &&
+ test_cmp_config . branch.ensure-new-current.remote &&
+ test_cmp_config refs/heads/main branch.ensure-new-current.merge
+'
+
-+test_expect_success 'switch -e --track creates branch from remote-tracking branch' '
++test_expect_success 'switch --create-if-missing --track creates branch from remote-tracking branch' '
+ test_when_finished "
+ git switch main || :
+ git branch -D ensure-new || :
+ " &&
-+ git switch -e ensure-new --track origin/foo &&
++ git switch --create-if-missing ensure-new --track origin/foo &&
+ test_cmp_rev refs/remotes/origin/foo refs/heads/ensure-new &&
+ test_cmp_config origin branch.ensure-new.remote &&
+ test_cmp_config refs/heads/foo branch.ensure-new.merge
+'
+
-+test_expect_success 'switch -e --track uses current branch for existing branch' '
++test_expect_success 'switch --create-if-missing switches to existing branch' '
++ test_when_finished "
++ git switch main || :
++ git branch -D ensure-existing-plain || :
++ " &&
++ git branch ensure-existing-plain main &&
++ git switch --create-if-missing ensure-existing-plain 2>err &&
++ test_grep "Switched to existing branch '\''ensure-existing-plain'\''" err
++'
++
++test_expect_success 'switch --create-if-missing reports tracking for existing branch' '
++ test_when_finished "
++ git switch main || :
++ git branch -D ensure-existing-report || :
++ git update-ref refs/remotes/origin/foo first-branch || :
++ " &&
++ git branch ensure-existing-report first-branch &&
++ git config branch.ensure-existing-report.remote origin &&
++ git config branch.ensure-existing-report.merge refs/heads/foo &&
++ git update-ref refs/remotes/origin/foo main &&
++ git switch --create-if-missing ensure-existing-report >out 2>err &&
++ test_grep "Switched to existing branch '\''ensure-existing-report'\''" err &&
++ test_grep "Your branch is behind '\''origin/foo'\''" out
++'
++
++test_expect_success 'switch --create-if-missing --track uses current branch for existing branch' '
+ test_when_finished "
+ git switch main || :
+ git branch -D ensure-existing source-for-track || :
+ " &&
+ git switch -c source-for-track main &&
+ git branch ensure-existing main &&
-+ git switch -e ensure-existing --track &&
++ git switch --create-if-missing ensure-existing --track >out 2>err &&
++ test_grep "branch '\''ensure-existing'\'' set up to track '\''source-for-track'\''." out &&
++ test_grep "Switched to existing branch '\''ensure-existing'\''" err &&
+ test_cmp_config . branch.ensure-existing.remote &&
+ test_cmp_config refs/heads/source-for-track branch.ensure-existing.merge
+'
+
-+test_expect_success 'switch -e --track fails from detached HEAD without start-point' '
++test_expect_success 'switch --create-if-missing --track fails from detached HEAD without start-point' '
+ test_when_finished "
+ git switch main || :
+ git branch -D detached-target || :
+ " &&
+ git branch detached-target main &&
+ git switch --detach main &&
-+ test_must_fail git switch -e detached-target --track 2>stderr &&
++ test_must_fail git switch --create-if-missing detached-target --track 2>stderr &&
+ test_grep "cannot set up tracking information; starting point '\''HEAD'\'' is not a branch" stderr
+'
+
test_expect_success 'switch back when temporarily detached and checked out elsewhere ' '
test_when_finished "
git worktree remove wt1 ||:
+
+ ## t/t9902-completion.sh ##
+@@ t/t9902-completion.sh: test_expect_success 'double dash "git checkout"' '
+ --quiet Z
+ --detach Z
+ --track Z
++ --create-if-missing=Z
+ --orphan=Z
+ --ours Z
+ --theirs Z
Documentation/git-checkout.adoc | 32 +++++++++
Documentation/git-switch.adoc | 15 +++++
builtin/checkout.c | 93 +++++++++++++++++++++++++-
contrib/completion/git-completion.bash | 22 +++---
t/t2018-checkout-branch.sh | 37 ++++++++++
t/t2027-checkout-track.sh | 37 ++++++++++
t/t2060-switch.sh | 73 ++++++++++++++++++++
t/t9902-completion.sh | 1 +
8 files changed, 298 insertions(+), 12 deletions(-)
diff --git a/Documentation/git-checkout.adoc b/Documentation/git-checkout.adoc
index a8b3b8c2e2..a80f6fe6f6 100644
--- a/Documentation/git-checkout.adoc
+++ b/Documentation/git-checkout.adoc
@@ -12,6 +12,7 @@ git checkout [-q] [-f] [-m] [<branch>]
git checkout [-q] [-f] [-m] --detach [<branch>]
git checkout [-q] [-f] [-m] [--detach] <commit>
git checkout [-q] [-f] [-m] [[-b|-B|--orphan] <new-branch>] [<start-point>]
+git checkout [-q] [-f] [-m] --create-if-missing <branch> [<start-point>]
git checkout <tree-ish> [--] <pathspec>...
git checkout <tree-ish> --pathspec-from-file=<file> [--pathspec-file-nul]
git checkout [-f|--ours|--theirs|-m|--conflict=<style>] [--] <pathspec>...
@@ -58,6 +59,21 @@ This will fail if there's an error checking out _<new-branch>_, for
example if checking out the `<start-point>` commit would overwrite your
uncommitted changes.
+`git checkout --create-if-missing <branch> [<start-point>]`::
+
+ Check out _<branch>_ if it already exists, or create it from
+ _<start-point>_ before checking it out if it does not.
++
+When _<branch>_ does not already exist, this behaves like
+`git checkout -b <branch> [<start-point>]`, including any `--track`
+or `--no-track` options.
++
+When _<branch>_ already exists, the branch tip is not changed. If
+`--track[=(direct|inherit)]` is given, the existing branch's upstream
+configuration is updated using _<start-point>_ when one is provided,
+or the current branch when _<start-point>_ is omitted. This form fails
+when `HEAD` is detached and no _<start-point>_ is given.
+
`git checkout -B <branch> [<start-point>]`::
The same as `-b`, except that if the branch already exists it
@@ -157,6 +173,22 @@ of it").
The same as `-b`, except that if the branch already exists it
resets _<branch>_ to the start point instead of failing.
+`--create-if-missing <branch>`::
+ Check out _<branch>_ if it already exists, or create it from
+ _<start-point>_ before checking it out if it does not.
++
+When _<branch>_ does not already exist, this behaves like
+`git checkout -b <branch> [<start-point>]`, including any `--track`
+or `--no-track` options.
++
+When _<branch>_ already exists, the branch tip is not changed. If
+`--track[=(direct|inherit)]` is given, the existing branch's upstream
+configuration is updated using _<start-point>_ when one is provided,
+or the current branch when _<start-point>_ is omitted. This form fails
+when `HEAD` is detached and no _<start-point>_ is given.
++
+This option cannot be used when checking out paths.
+
`-t`::
`--track[=(direct|inherit)]`::
When creating a new branch, set up "upstream" configuration. See
diff --git a/Documentation/git-switch.adoc b/Documentation/git-switch.adoc
index d6c4f229a5..461a6f0b96 100644
--- a/Documentation/git-switch.adoc
+++ b/Documentation/git-switch.adoc
@@ -11,6 +11,7 @@ SYNOPSIS
git switch [<options>] [--no-guess] <branch>
git switch [<options>] --detach [<start-point>]
git switch [<options>] (-c|-C) <new-branch> [<start-point>]
+git switch [<options>] --create-if-missing <branch> [<start-point>]
git switch [<options>] --orphan <new-branch>
DESCRIPTION
@@ -81,6 +82,20 @@ $ git branch -f _<new-branch>_
$ git switch _<new-branch>_
------------
+`--create-if-missing <branch>`::
+ Switch to _<branch>_ if it already exists, or create it from
+ _<start-point>_ before switching to it if it does not.
++
+When _<branch>_ does not already exist, this behaves like
+`git switch -c <branch> [<start-point>]`, including any `--track`
+or `--no-track` options.
++
+When _<branch>_ already exists, the branch tip is not changed. If
+`--track[=(direct|inherit)]` is given, the existing branch's upstream
+configuration is updated using _<start-point>_ when one is provided,
+or the current branch when _<start-point>_ is omitted. This form fails
+when `HEAD` is detached and no _<start-point>_ is given.
+
`-d`::
`--detach`::
Switch to a commit for inspection and discardable
diff --git a/builtin/checkout.c b/builtin/checkout.c
index b78b3a1d16..f5bc882f2e 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -81,6 +81,8 @@ struct checkout_opts {
const char *new_branch;
const char *new_branch_force;
const char *new_orphan_branch;
+ const char *create_if_missing_branch;
+ const char *create_if_missing_start;
int new_branch_log;
enum branch_track track;
struct diff_options diff_options;
@@ -551,6 +553,10 @@ static int checkout_paths(const struct checkout_opts *opts,
die(_("Cannot update paths and switch to branch '%s' at the same time."),
opts->new_branch);
+ if (opts->create_if_missing_branch)
+ die(_("Cannot update paths and switch to branch '%s' at the same time."),
+ opts->create_if_missing_branch);
+
if (!opts->checkout_worktree && !opts->checkout_index)
die(_("neither '%s' or '%s' is specified"),
"--staged", "--worktree");
@@ -988,6 +994,14 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
free(new_branch_info->refname);
new_branch_info->name = xstrdup(opts->new_branch);
setup_branch_path(new_branch_info);
+ } else if (opts->create_if_missing_branch && opts->branch_exists &&
+ opts->track != BRANCH_TRACK_UNSPECIFIED) {
+ const char *tracking_source = opts->create_if_missing_start ?
+ opts->create_if_missing_start :
+ old_branch_info->name;
+ dwim_and_setup_tracking(the_repository, opts->create_if_missing_branch,
+ tracking_source, opts->track,
+ opts->quiet);
}
old_desc = old_branch_info->name;
@@ -1030,6 +1044,10 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
fprintf(stderr, _("Switched to and reset branch '%s'\n"), new_branch_info->name);
else
fprintf(stderr, _("Switched to a new branch '%s'\n"), new_branch_info->name);
+ } else if (opts->create_if_missing_branch &&
+ opts->branch_exists) {
+ fprintf(stderr, _("Switched to existing branch '%s'\n"),
+ new_branch_info->name);
} else {
fprintf(stderr, _("Switched to branch '%s'\n"),
new_branch_info->name);
@@ -1927,6 +1945,52 @@ static int checkout_main(int argc, const char **argv, const char *prefix,
die(_("options '-%c', '-%c', and '%s' cannot be used together"),
cb_option, toupper(cb_option), "--orphan");
+ if (opts->create_if_missing_branch) {
+ struct strbuf ref = STRBUF_INIT;
+ int exists;
+
+ if (opts->new_branch || opts->new_branch_force || opts->new_orphan_branch)
+ die(_("'%s' cannot be used with '%s'"), "--create-if-missing", "-c/-C/--orphan");
+ if (opts->force_detach)
+ die(_("'%s' cannot be used with '%s'"), "--create-if-missing", "--detach");
+
+ exists = validate_branchname(opts->create_if_missing_branch, &ref);
+ strbuf_release(&ref);
+
+ /* Save an explicit start point for tracking setup. */
+ if (argc > 0 && opts->track != BRANCH_TRACK_UNSPECIFIED)
+ opts->create_if_missing_start = argv[0];
+
+ if (exists) {
+ /*
+ * Branch exists: just switch to it, don't reset.
+ * We'll set up tracking after the switch if --track was given.
+ */
+ opts->branch_exists = 1;
+ } else {
+ /* Branch doesn't exist: create it like -c */
+ opts->new_branch = opts->create_if_missing_branch;
+ }
+ }
+
+ if (opts->create_if_missing_branch && opts->branch_exists &&
+ opts->track != BRANCH_TRACK_UNSPECIFIED &&
+ !opts->create_if_missing_start) {
+ struct object_id head_oid;
+ char *head = refs_resolve_refdup(get_main_ref_store(the_repository),
+ "HEAD", 0, &head_oid, NULL);
+ const char *branch;
+
+ if (!head)
+ die(_("failed to resolve HEAD as a valid ref"));
+ if (!strcmp(head, "HEAD"))
+ die(_("cannot set up tracking information; starting point '%s' is not a branch"),
+ "HEAD");
+ if (!skip_prefix(head, "refs/heads/", &branch))
+ die(_("HEAD not found below refs/heads!"));
+ free(head);
+ }
+
if (opts->overlay_mode == 1 && opts->patch_mode)
die(_("options '%s' and '%s' cannot be used together"), "-p", "--overlay");
@@ -1961,8 +2025,9 @@ static int checkout_main(int argc, const char **argv, const char *prefix,
if (opts->new_orphan_branch)
opts->new_branch = opts->new_orphan_branch;
- /* --track without -c/-C/-b/-B/--orphan should DWIM */
- if (opts->track != BRANCH_TRACK_UNSPECIFIED && !opts->new_branch) {
+ /* --track without -c/-C/-b/-B/--orphan/--create-if-missing should DWIM */
+ if (opts->track != BRANCH_TRACK_UNSPECIFIED && !opts->new_branch &&
+ !(opts->create_if_missing_branch && opts->branch_exists)) {
const char *argv0 = argv[0];
if (!argc || !strcmp(argv0, "--"))
die(_("--track needs a branch name"));
@@ -2012,6 +2077,24 @@ static int checkout_main(int argc, const char **argv, const char *prefix,
die(_("reference is not a tree: %s"), opts->from_treeish);
}
+ /*
+ * Handle --create-if-missing with existing branch: set up
+ * new_branch_info to switch to the existing branch.
+ */
+ if (opts->create_if_missing_branch && opts->branch_exists) {
+ struct object_id rev;
+
+ if (repo_get_oid_mb(the_repository, opts->create_if_missing_branch,
+ &rev))
+ die(_("could not resolve '%s'"),
+ opts->create_if_missing_branch);
+
+ branch_info_release(&new_branch_info);
+ memset(&new_branch_info, 0, sizeof(new_branch_info));
+ setup_new_branch_info_and_source_tree(&new_branch_info, opts, &rev,
+ opts->create_if_missing_branch);
+ }
+
if (argc) {
parse_pathspec(&opts->pathspec, 0,
opts->patch_mode ? PATHSPEC_PREFIX_ORIGIN : 0,
@@ -2098,6 +2181,9 @@ int cmd_checkout(int argc,
N_("create and checkout a new branch")),
OPT_STRING('B', NULL, &opts.new_branch_force, N_("branch"),
N_("create/reset and checkout a branch")),
+ OPT_STRING_F(0, "create-if-missing", &opts.create_if_missing_branch, N_("branch"),
+ N_("create if needed and checkout branch"),
+ PARSE_OPT_NONEG),
OPT_BOOL('l', NULL, &opts.new_branch_log, N_("create reflog for new branch")),
OPT_BOOL(0, "guess", &opts.dwim_new_local_branch,
N_("second guess 'git checkout <no-such-branch>' (default)")),
@@ -2150,6 +2236,9 @@ int cmd_switch(int argc,
N_("create and switch to a new branch")),
OPT_STRING('C', "force-create", &opts.new_branch_force, N_("branch"),
N_("create/reset and switch to a branch")),
+ OPT_STRING_F(0, "create-if-missing", &opts.create_if_missing_branch, N_("branch"),
+ N_("create if needed and switch to branch"),
+ PARSE_OPT_NONEG),
OPT_BOOL(0, "guess", &opts.dwim_new_local_branch,
N_("second guess 'git switch <no-such-branch>'")),
OPT_BOOL(0, "discard-changes", &opts.discard_changes,
diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash
index e875787710..1c72b4c853 100644
--- a/contrib/completion/git-completion.bash
+++ b/contrib/completion/git-completion.bash
@@ -1740,7 +1740,7 @@ _git_checkout ()
local dwim_opt="$(__git_checkout_default_dwim_mode)"
case "$prev" in
- -b|-B|--orphan)
+ -b|-B|--orphan|--create-if-missing)
# Complete local branches (and DWIM branch
# remote branch names) for an option argument
# specifying a new branch name. This is for
@@ -1762,14 +1762,16 @@ _git_checkout ()
;;
*)
# At this point, we've already handled special completion for
- # the arguments to -b/-B, and --orphan. There are 3 main
- # things left we can possibly complete:
- # 1) a start-point for -b/-B, -d/--detach, or --orphan
+ # the arguments to -b/-B, --orphan, and
+ # --create-if-missing. There are 3 main things left
+ # we can possibly complete:
+ # 1) a start-point for -b/-B, -d/--detach, --orphan,
+ # or --create-if-missing
# 2) a remote head, for --track
# 3) an arbitrary reference, possibly including DWIM names
#
- if [ -n "$(__git_find_on_cmdline "-b -B -d --detach --orphan")" ]; then
+ if [ -n "$(__git_find_on_cmdline "-b -B -d --detach --orphan --create-if-missing")" ]; then
__git_complete_refs --mode="refs"
elif [ -n "$(__git_find_on_cmdline "-t --track")" ]; then
__git_complete_refs --mode="remote-heads"
@@ -2692,7 +2694,7 @@ _git_switch ()
local dwim_opt="$(__git_checkout_default_dwim_mode)"
case "$prev" in
- -c|-C|--orphan)
+ -c|-C|--orphan|--create-if-missing)
# Complete local branches (and DWIM branch
# remote branch names) for an option argument
# specifying a new branch name. This is for
@@ -2721,13 +2723,13 @@ _git_switch ()
fi
# At this point, we've already handled special completion for
- # -c/-C, and --orphan. There are 3 main things left to
- # complete:
- # 1) a start-point for -c/-C or -d/--detach
+ # -c/-C, --orphan, and --create-if-missing. There
+ # are 3 main things left to complete:
+ # 1) a start-point for -c/-C, -d/--detach, or --create-if-missing
# 2) a remote head, for --track
# 3) a branch name, possibly including DWIM remote branches
- if [ -n "$(__git_find_on_cmdline "-c -C -d --detach")" ]; then
+ if [ -n "$(__git_find_on_cmdline "-c -C -d --detach --create-if-missing")" ]; then
__git_complete_refs --mode="refs"
elif [ -n "$(__git_find_on_cmdline "-t --track")" ]; then
__git_complete_refs --mode="remote-heads"
diff --git a/t/t2018-checkout-branch.sh b/t/t2018-checkout-branch.sh
index a48ebdbf4d..f910563170 100755
--- a/t/t2018-checkout-branch.sh
+++ b/t/t2018-checkout-branch.sh
@@ -243,6 +243,33 @@ test_expect_success 'checkout -B to the current branch works' '
test_dirty_mergeable
'
+test_expect_success 'checkout --create-if-missing creates a branch' '
+ test_when_finished "
+ git checkout branch1 &&
+ test_might_fail git branch -D create-if-missing-new
+ " &&
+ git checkout --create-if-missing create-if-missing-new $HEAD1 &&
+ echo refs/heads/create-if-missing-new >expect &&
+ git symbolic-ref HEAD >actual &&
+ test_cmp expect actual &&
+ test_cmp_rev $HEAD1 HEAD
+'
+
+test_expect_success 'checkout --create-if-missing switches to existing branch' '
+ test_when_finished "
+ git checkout branch1 &&
+ test_might_fail git branch -D create-if-missing-existing
+ " &&
+ git branch create-if-missing-existing $HEAD1 &&
+ git checkout branch1 &&
+ git checkout --create-if-missing create-if-missing-existing 2>err &&
+ test_grep "Switched to existing branch '\''create-if-missing-existing'\''" err &&
+ echo refs/heads/create-if-missing-existing >expect &&
+ git symbolic-ref HEAD >actual &&
+ test_cmp expect actual &&
+ test_cmp_rev $HEAD1 HEAD
+'
+
test_expect_success 'checkout -b after clone --no-checkout does a checkout of HEAD' '
git init src &&
test_commit -C src a &&
@@ -285,4 +312,14 @@ test_expect_success 'checkout -b rejects an extra path argument' '
test_grep "Cannot update paths and switch to branch" err
'
+test_expect_success 'checkout --create-if-missing rejects a path argument' '
+ test_when_finished "
+ git checkout branch1 &&
+ test_might_fail git branch -D create-if-missing-path
+ " &&
+ git branch create-if-missing-path branch1 &&
+ test_must_fail git checkout --create-if-missing create-if-missing-path -- file1 2>err &&
+ test_grep "Cannot update paths and switch to branch '\''create-if-missing-path'\''" err
+'
+
test_done
diff --git a/t/t2027-checkout-track.sh b/t/t2027-checkout-track.sh
index c01f1cd617..67f073ddf0 100755
--- a/t/t2027-checkout-track.sh
+++ b/t/t2027-checkout-track.sh
@@ -19,6 +19,43 @@ test_expect_success 'checkout --track -b creates a new tracking branch' '
test $(git config --get branch.branch1.merge) = refs/heads/main
'
+test_expect_success 'checkout --create-if-missing --track creates branch from current branch' '
+ test_when_finished "
+ git checkout main &&
+ git branch -D branch2
+ " &&
+ git checkout main &&
+ git checkout --create-if-missing branch2 --track &&
+ test $(git rev-parse --abbrev-ref HEAD) = branch2 &&
+ test_cmp_config . branch.branch2.remote &&
+ test_cmp_config refs/heads/main branch.branch2.merge
+'
+
+test_expect_success 'checkout --create-if-missing --track uses current branch for existing branch' '
+ test_when_finished "
+ git checkout main &&
+ git branch -D branch3 branch3-source
+ " &&
+ git checkout -b branch3-source main &&
+ git branch branch3 main &&
+ git checkout --create-if-missing branch3 --track >out 2>err &&
+ test_grep "branch '\''branch3'\'' set up to track '\''branch3-source'\''." out &&
+ test_grep "Switched to existing branch '\''branch3'\''" err &&
+ test_cmp_config . branch.branch3.remote &&
+ test_cmp_config refs/heads/branch3-source branch.branch3.merge
+'
+
+test_expect_success 'checkout --create-if-missing --track fails from detached HEAD without start-point' '
+ test_when_finished "
+ git checkout main &&
+ git branch -D branch4
+ " &&
+ git branch branch4 main &&
+ git checkout --detach main &&
+ test_must_fail git checkout --create-if-missing branch4 --track 2>err &&
+ test_grep "cannot set up tracking information; starting point '\''HEAD'\'' is not a branch" err
+'
+
test_expect_success 'checkout --track -b rejects an extra path argument' '
test_must_fail git checkout --track -b branch2 main one.t 2>err &&
test_grep "cannot be used with updating paths" err
diff --git a/t/t2060-switch.sh b/t/t2060-switch.sh
index c91c4db936..f17afda28e 100755
--- a/t/t2060-switch.sh
+++ b/t/t2060-switch.sh
@@ -146,6 +146,79 @@ test_expect_success 'tracking info copied with autoSetupMerge=inherit' '
test_cmp_config "" --default "" branch.main2.merge
'
+test_expect_success 'switch --create-if-missing --track creates branch from current branch' '
+ test_when_finished "
+ git switch main || :
+ git branch -D ensure-new-current || :
+ " &&
+ git switch main &&
+ git switch --create-if-missing ensure-new-current --track &&
+ test_cmp_rev refs/heads/main refs/heads/ensure-new-current &&
+ test_cmp_config . branch.ensure-new-current.remote &&
+ test_cmp_config refs/heads/main branch.ensure-new-current.merge
+'
+
+test_expect_success 'switch --create-if-missing --track creates branch from remote-tracking branch' '
+ test_when_finished "
+ git switch main || :
+ git branch -D ensure-new || :
+ " &&
+ git switch --create-if-missing ensure-new --track origin/foo &&
+ test_cmp_rev refs/remotes/origin/foo refs/heads/ensure-new &&
+ test_cmp_config origin branch.ensure-new.remote &&
+ test_cmp_config refs/heads/foo branch.ensure-new.merge
+'
+
+test_expect_success 'switch --create-if-missing switches to existing branch' '
+ test_when_finished "
+ git switch main || :
+ git branch -D ensure-existing-plain || :
+ " &&
+ git branch ensure-existing-plain main &&
+ git switch --create-if-missing ensure-existing-plain 2>err &&
+ test_grep "Switched to existing branch '\''ensure-existing-plain'\''" err
+'
+
+test_expect_success 'switch --create-if-missing reports tracking for existing branch' '
+ test_when_finished "
+ git switch main || :
+ git branch -D ensure-existing-report || :
+ git update-ref refs/remotes/origin/foo first-branch || :
+ " &&
+ git branch ensure-existing-report first-branch &&
+ git config branch.ensure-existing-report.remote origin &&
+ git config branch.ensure-existing-report.merge refs/heads/foo &&
+ git update-ref refs/remotes/origin/foo main &&
+ git switch --create-if-missing ensure-existing-report >out 2>err &&
+ test_grep "Switched to existing branch '\''ensure-existing-report'\''" err &&
+ test_grep "Your branch is behind '\''origin/foo'\''" out
+'
+
+test_expect_success 'switch --create-if-missing --track uses current branch for existing branch' '
+ test_when_finished "
+ git switch main || :
+ git branch -D ensure-existing source-for-track || :
+ " &&
+ git switch -c source-for-track main &&
+ git branch ensure-existing main &&
+ git switch --create-if-missing ensure-existing --track >out 2>err &&
+ test_grep "branch '\''ensure-existing'\'' set up to track '\''source-for-track'\''." out &&
+ test_grep "Switched to existing branch '\''ensure-existing'\''" err &&
+ test_cmp_config . branch.ensure-existing.remote &&
+ test_cmp_config refs/heads/source-for-track branch.ensure-existing.merge
+'
+
+test_expect_success 'switch --create-if-missing --track fails from detached HEAD without start-point' '
+ test_when_finished "
+ git switch main || :
+ git branch -D detached-target || :
+ " &&
+ git branch detached-target main &&
+ git switch --detach main &&
+ test_must_fail git switch --create-if-missing detached-target --track 2>stderr &&
+ test_grep "cannot set up tracking information; starting point '\''HEAD'\'' is not a branch" stderr
+'
+
test_expect_success 'switch back when temporarily detached and checked out elsewhere ' '
test_when_finished "
git worktree remove wt1 ||:
diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh
index 55dc9eabfc..e782c39c5e 100755
--- a/t/t9902-completion.sh
+++ b/t/t9902-completion.sh
@@ -2588,6 +2588,7 @@ test_expect_success 'double dash "git checkout"' '
--quiet Z
--detach Z
--track Z
+ --create-if-missing=Z
--orphan=Z
--ours Z
--theirs Z
base-commit: 0fae78c9d55efe705877ea537fe42c59164ccd94
--
gitgitgadget
^ permalink raw reply related
* How does GitGitGadget generate range-diffs, was Re: [PATCH v2 0/6] Support hashing objects larger than 4GB on Windows
From: Johannes Schindelin @ 2026-06-17 10:39 UTC (permalink / raw)
To: Junio C Hamano
Cc: Johannes Schindelin via GitGitGadget, git, Philip Oakley,
Patrick Steinhardt
In-Reply-To: <xmqqfr2m4gd1.fsf@gitster.g>
Hi Junio,
On Wed, 17 Jun 2026, Junio C Hamano wrote:
> "Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com>
> writes:
>
> > Range-diff vs v1:
> >
> > 1: 84e1cd0aa0 = 1: 9c01bac407 hash-object: demonstrate a >4GB/LLP64 problem
> > 2: 809d83e46f ! 2: aa5859c14f object-file.c: use size_t for header lengths
> > @@ Commit message
>
> By the way, how is range-diff driven via GGG? After applying these
> patches on the same base commit, my "git range-diff v1...v2" invocation
> punts on matching step 2 and I do not get a comparison like this unless
> I give --creation-factor=<large number> from the command line.
GitGitGadget is using range-diff to compare between iterations of
essentially the same patches, therefore it encourages `range-diff` to try
harder to look for matches via `--creation-factor=95`:
https://github.com/gitgitgadget/gitgitgadget/blob/bf9140eef184/lib/patch-series.ts#L722
The full details how the magic number "95" was determined is in the commit
https://github.com/gitgitgadget/gitgitgadget/commit/2605f72f92bb0ff63f4db91eaf91969749568dd7
(essentially, I played with a couple of values and known hard-cases of
actual, real-world patch series iterations and 95 was the best
compromise).
Ciao,
Johannes
^ permalink raw reply
* Re: [PATCH v5 0/2] graph: indent visual roots in graph
From: Chandra Pratap @ 2026-06-17 10:33 UTC (permalink / raw)
To: Pablo Sabater
Cc: git, ayu.chandekar, christian.couder, gitster, jltobler,
karthik.188, peff, phillip.wood, siddharthasthana31
In-Reply-To: <20260613-ps-pre-commit-indent-v5-0-8d308efea63d@gmail.com>
On Sun, 14 Jun 2026 at 00:39, Pablo Sabater <pabloosabaterr@gmail.com> wrote:
>
> When rendering a graph, if the history contains multiple "visual roots",
> actual roots or commits that look like roots (i.e. have their parents
> filtered out) can end up being vertically adjacent to unrelated commits,
> falsely appearing to be related.
>
> A fix for this issue was already attempted [1] a while ago.
>
> This series adds indentation to the visual root commits, so they cannot be
> vertically adjacent anymore making it easier to identify them.
>
> before indentation:
>
> * A
> * B1
> * B2
> * C1
> * C2
>
> after indentation:
>
> * A
> * B1
> \
> * B2
> * C1
> * C2
>
> Indents the visual root commits that have still commits to show after them, and
> if they have children it connects them with an edge at a new row.
>
> If there are multiple visual roots adjacent in history, the indentation starts
> with the second one, avoiding redundant indentation of the first one and cascades
> after the second.
>
> * A
> * B
> * C
> * D1
> * D2
>
> This series first commit is a cleanup that brings a common function from t4215
> and t6016 to a graph functions file which they both use, so the new test file
> for indentation, t4218, can use it as well.
>
> The lookahead used to set the cascading and avoid extra indentation is not
> completely reliable, as the walker goes through the commits it simplifies the
> history of the current commit and its parents, but it doesn't simplify it
> for the next unrelated or the grandparents. When the walker simplifies the
> history, it removes filtered commits from the history and sets its flags.
> When the next commit is an unrelated commit and its parents will be filtered
> out, for the lookahead the commit is still a child of, it cannot know that the
> next commit once simplified (advancing the walker) it will become a visual root.
> This makes the lookahead fail, failing to set the cascading and starting it
> with the first visual root, carrying an extra indent for the cascade.
>
> given:
>
> * A unrelated (visual root)
> * B child of C
> * C visual root WILL BE FILTERED OUT
> * D unrelated (visual root)
>
> the actual output is:
>
> * A
> * B
> * D
>
> A test has been added to t4218 and a NEEDSWORK to the lookahead function to
> document this edge case but I'm not that familiar with revision.c. Maybe there's
> a better way to make the lookahead more reliable.
It's slightly disappointing that we couldn't find a way to fix this
after all, but at least the bug is non-breaking and the added
NEEDSWORK properly documents the issue for someone else
to tackle in the future.
Other than that, this version looks fine to me.
> [1]: https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/
>
> V4 DIFF:
>
> - Fixed test to be shown as expected by unsetting COMMIT_GRAPH
>
> Signed-off-by: Pablo Sabater <pabloosabaterr@gmail.com>
> ---
> Pablo Sabater (2):
> lib-log-graph: move check_graph function
> graph: indent visual root in graph
>
> graph.c | 262 ++++++++++++++++
> t/lib-log-graph.sh | 5 +
> t/meson.build | 1 +
> t/t4215-log-skewed-merges.sh | 33 +-
> t/t4218-log-graph-indentation.sh | 467 +++++++++++++++++++++++++++++
> t/t6016-rev-list-graph-simplify-history.sh | 25 +-
> 6 files changed, 759 insertions(+), 34 deletions(-)
> ---
> base-commit: 3e65291872de10c3f0bf05ea8c24187e7a71ebf0
> change-id: 20260612-ps-pre-commit-indent-39ca72816382
>
> Best regards,
> --
> Pablo Sabater <pabloosabaterr@gmail.com>
^ permalink raw reply
* Re: [PATCH GSoC RFC v12 12/12] cat-file: make remote-object-info allow-list dynamic
From: Chandra Pratap @ 2026-06-17 10:16 UTC (permalink / raw)
To: Pablo Sabater
Cc: eric.peijian, calvinwan, chriscool, git, jltobler, jonathantanmy,
karthik.188, toon
In-Reply-To: <CAN5EUNQHSd=0z26iG0gk24TEtgg1n8CC+H9bkqRACyErNgLxEA@mail.gmail.com>
On Tue, 9 Jun 2026 at 23:04, Pablo Sabater <pabloosabaterr@gmail.com> wrote:
> [snip]
> > > diff --git a/fetch-object-info.c b/fetch-object-info.c
> > > index 51a898430d..425929a269 100644
> > > --- a/fetch-object-info.c
> > > +++ b/fetch-object-info.c
> > > @@ -39,6 +39,12 @@ int fetch_object_info(const enum protocol_version version, struct object_info_ar
> > > case protocol_v2:
> > > if (!server_supports_v2("object-info"))
> > > die(_("object-info capability is not enabled on the server"));
> > > +
> > > + for (int i = args->object_info_options->nr - 1; i >= 0; i--)
> >
> > Isn't args->object_info_options->nr of type size_t? We should probably
> > do something
> > like:
> >
> > for (size_t i = 0; i < args->args->object_info_options->nr; i++)
> >
> > instead.
>
> Hi!
>
> void unsorted_string_list_delete_item(struct string_list *list, int i,
> int free_util)
> {
> if (list->strdup_strings)
> free(list->items[i].string);
> if (free_util)
> free(list->items[i].util);
> list->items[i] = list->items[list->nr-1];
> list->nr--;
> }
>
>
> I made it backwards because of "list->items[i] = list->items[list->nr
> - 1];" If we made it from 0..nr and we delete the first element, for
> the next iteration, the last element is at [0] but we are on [1] and
> that swapped element never gets evaluated.
Makes sense now.
> About size_t, yes, it is size_t but because we go backwards 0 - 1
> would fail, also unsorted_string_list_delete_item() signature has "int
> i". The options that can be on that list will be a small number so
> there should be no problem, should I cast it explicitly?
Yes, I think explicit casting with a short comment explaining why it is
fine to do so will be much better.
Thanks,
Chandra.
^ permalink raw reply
* [PATCH v2 5/5] builtin/refs: add "rename" subcommand
From: Patrick Steinhardt @ 2026-06-17 10:16 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano
In-Reply-To: <20260617-pks-refs-writing-subcommands-v2-0-07f3d18336f9@pks.im>
Add a "rename" subcommand to git-refs(1) with the syntax:
$ git refs rename <oldref> <newref>
It renames <oldref> together with its reflog to <newref>; even when used
on a local branch ref, the current value and the reflog of the ref are
the only things that are renamed. Document it and redirect casual users
to "git branch -m" if that is what they wanted to do.
Co-authored-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-refs.adoc | 6 ++
builtin/refs.c | 49 +++++++++++++++++
t/meson.build | 1 +
t/t1467-refs-rename.sh | 131 ++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 187 insertions(+)
diff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc
index e6a3528349..ce278c59bf 100644
--- a/Documentation/git-refs.adoc
+++ b/Documentation/git-refs.adoc
@@ -23,6 +23,7 @@ git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude
git refs create [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value>
git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]
git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]
+git refs rename [--message=<reason>] <old-ref> <new-ref>
DESCRIPTION
-----------
@@ -71,6 +72,11 @@ update::
`<new-value>` deletes the branch, whereas an all-zeroes `<old-value>`
ensures that the branch does not yet exist.
+rename::
+ Rename the reference `<oldref>` to `<newref>`. The old reference must
+ exist and the new reference must not yet exist, and both must have a
+ well-formed name (see linkgit:git-check-ref-format[1]).
+
OPTIONS
-------
diff --git a/builtin/refs.c b/builtin/refs.c
index 92e62fd5df..c7aa1a327f 100644
--- a/builtin/refs.c
+++ b/builtin/refs.c
@@ -30,6 +30,9 @@
#define REFS_UPDATE_USAGE \
N_("git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]")
+#define REFS_RENAME_USAGE \
+ N_("git refs rename [--message=<reason>] <old-ref> <new-ref>")
+
static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,
struct repository *repo)
{
@@ -327,6 +330,50 @@ static int cmd_refs_update(int argc, const char **argv, const char *prefix,
return ret;
}
+static int cmd_refs_rename(int argc, const char **argv, const char *prefix,
+ struct repository *repo)
+{
+ static char const * const refs_rename_usage[] = {
+ REFS_RENAME_USAGE,
+ NULL
+ };
+ const char *message = NULL;
+ struct option opts[] = {
+ OPT_STRING(0, "message", &message, N_("reason"),
+ N_("reason of the update")),
+ OPT_END(),
+ };
+ const char *oldref, *newref;
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, opts, refs_rename_usage, 0);
+ if (argc != 2)
+ usage(_("rename requires old and new reference name"));
+ if (message && !*message)
+ die(_("refusing to perform update with empty message"));
+
+ repo_config(repo, git_default_config, NULL);
+
+ oldref = argv[0];
+ newref = argv[1];
+
+ if (check_refname_format(oldref, 0))
+ die(_("invalid ref format: '%s'"), oldref);
+ if (check_refname_format(newref, 0))
+ die(_("invalid ref format: '%s'"), newref);
+
+ if (!refs_ref_exists(get_main_ref_store(repo), oldref))
+ die(_("reference does not exist: '%s'"), oldref);
+ if (refs_ref_exists(get_main_ref_store(repo), newref))
+ die(_("reference already exists: '%s'"), newref);
+
+ ret = refs_rename_ref(get_main_ref_store(repo), oldref, newref, message);
+
+ if (ret < 0)
+ ret = 1;
+ return ret;
+}
+
int cmd_refs(int argc,
const char **argv,
const char *prefix,
@@ -341,6 +388,7 @@ int cmd_refs(int argc,
REFS_CREATE_USAGE,
REFS_DELETE_USAGE,
REFS_UPDATE_USAGE,
+ REFS_RENAME_USAGE,
NULL,
};
parse_opt_subcommand_fn *fn = NULL;
@@ -353,6 +401,7 @@ int cmd_refs(int argc,
OPT_SUBCOMMAND("create", &fn, cmd_refs_create),
OPT_SUBCOMMAND("delete", &fn, cmd_refs_delete),
OPT_SUBCOMMAND("update", &fn, cmd_refs_update),
+ OPT_SUBCOMMAND("rename", &fn, cmd_refs_rename),
OPT_END(),
};
diff --git a/t/meson.build b/t/meson.build
index 541e6f919c..a39fd8c4c4 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -226,6 +226,7 @@ integration_tests = [
't1464-refs-delete.sh',
't1465-refs-update.sh',
't1466-refs-create.sh',
+ 't1467-refs-rename.sh',
't1500-rev-parse.sh',
't1501-work-tree.sh',
't1502-rev-parse-parseopt.sh',
diff --git a/t/t1467-refs-rename.sh b/t/t1467-refs-rename.sh
new file mode 100755
index 0000000000..f80d58e0f4
--- /dev/null
+++ b/t/t1467-refs-rename.sh
@@ -0,0 +1,131 @@
+#!/bin/sh
+
+test_description='git refs rename'
+
+. ./test-lib.sh
+
+setup_repo () {
+ git init "$1" &&
+ test_commit -C "$1" A &&
+ test_commit -C "$1" B
+}
+
+test_ref_matches () {
+ git rev-parse "$1" >expect &&
+ echo "$2" >actual &&
+ test_cmp expect actual
+}
+
+test_expect_success 'rename an existing reference' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs update refs/heads/foo $A &&
+ git refs rename refs/heads/foo refs/heads/bar &&
+ test_must_fail git refs exists refs/heads/foo &&
+ test_ref_matches refs/heads/bar $A
+ )
+'
+
+test_expect_success 'rename moves the reflog along with the reference' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs update --message="rename me" refs/heads/foo $A &&
+ git refs rename refs/heads/foo refs/heads/bar &&
+ git reflog show refs/heads/bar >reflog &&
+ test_grep "rename me" reflog &&
+ test_must_fail git reflog exists refs/heads/foo
+ )
+'
+
+test_expect_success 'rename with message records reason in reflog' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs update refs/heads/foo $A &&
+ git refs rename --message="rename reason" refs/heads/foo refs/heads/bar &&
+ git reflog show refs/heads/bar >actual &&
+ test_grep "rename reason" actual
+ )
+'
+
+test_expect_success 'rename a nonexistent reference fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ test_must_fail git refs rename refs/heads/foo refs/heads/bar 2>err &&
+ test_grep "reference does not exist" err
+ )
+'
+
+test_expect_success 'rename to an existing reference fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git refs update refs/heads/foo $A &&
+ git refs update refs/heads/bar $B &&
+ test_must_fail git refs rename refs/heads/foo refs/heads/bar 2>err &&
+ test_grep "reference already exists" err
+ )
+'
+
+test_expect_success 'rename with empty message fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs update refs/heads/foo $A &&
+ test_must_fail git refs rename --message= refs/heads/foo refs/heads/bar 2>err &&
+ test_grep "empty message" err
+ )
+'
+
+test_expect_success 'rename with invalid old reference name fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ test_must_fail git refs rename "refs/heads/foo..bar" refs/heads/bar 2>err &&
+ test_grep "invalid ref format" err
+ )
+'
+
+test_expect_success 'rename with invalid new reference name fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs update refs/heads/foo $A &&
+ test_must_fail git refs rename refs/heads/foo "refs/heads/bar..baz" 2>err &&
+ test_grep "invalid ref format" err
+ )
+'
+
+test_expect_success 'rename with too few arguments fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ test_must_fail git -C repo refs rename refs/heads/foo 2>err &&
+ test_grep "requires old and new reference name" err
+'
+
+test_expect_success 'rename with too many arguments fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ test_must_fail git -C repo refs rename refs/heads/foo refs/heads/bar refs/heads/baz 2>err &&
+ test_grep "requires old and new reference name" err
+'
+
+test_done
--
2.55.0.rc0.786.g65d90a0328.dirty
^ permalink raw reply related
* [PATCH v2 4/5] builtin/refs: add "create" subcommand
From: Patrick Steinhardt @ 2026-06-17 10:16 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano
In-Reply-To: <20260617-pks-refs-writing-subcommands-v2-0-07f3d18336f9@pks.im>
The "update" subcommand cannot only update an existing reference, but it
can also create new branches and delete existing branches by specifying
the all-zeroes object ID as either old or new value. Despite that, we
already have the "delete" subcommand as a handy shortcut so that a user
can easily delete a branch. This relieves them of needing to understand
the more arcane uses of the "update" command, and of counting the number
of zeroes they need to pass.
But while we have a "delete" subcommand, we don't have an equivalent
that would allow the user to create a new branch, which creates a
certain asymmetry.
Add a new "create" subcommand to plug this gap.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-refs.adoc | 5 ++
builtin/refs.c | 52 +++++++++++++++
t/meson.build | 1 +
t/t1466-refs-create.sh | 151 ++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 209 insertions(+)
diff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc
index 6475bdcc62..e6a3528349 100644
--- a/Documentation/git-refs.adoc
+++ b/Documentation/git-refs.adoc
@@ -20,6 +20,7 @@ git refs list [--count=<count>] [--shell|--perl|--python|--tcl]
[ --stdin | (<pattern>...)]
git refs exists <ref>
git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]
+git refs create [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value>
git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]
git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]
@@ -53,6 +54,10 @@ optimize::
usage. This subcommand is an alias for linkgit:git-pack-refs[1] and
offers identical functionality.
+create::
+ Create the given reference, which must not already exist, pointing at
+ `<new-value>`.
+
delete::
Delete the given reference. This subcommand mirrors `git update-ref -d`
(see linkgit:git-update-ref[1]). When `<old-value>` is given, the
diff --git a/builtin/refs.c b/builtin/refs.c
index 08453ae1c8..92e62fd5df 100644
--- a/builtin/refs.c
+++ b/builtin/refs.c
@@ -21,6 +21,9 @@
#define REFS_OPTIMIZE_USAGE \
N_("git refs optimize " PACK_REFS_OPTS)
+#define REFS_CREATE_USAGE \
+ N_("git refs create [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value>")
+
#define REFS_DELETE_USAGE \
N_("git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]")
@@ -181,6 +184,53 @@ static int cmd_refs_optimize(int argc, const char **argv, const char *prefix,
return pack_refs_core(argc, argv, prefix, repo, refs_optimize_usage);
}
+static int cmd_refs_create(int argc, const char **argv, const char *prefix,
+ struct repository *repo)
+{
+ static char const * const refs_create_usage[] = {
+ REFS_CREATE_USAGE,
+ NULL
+ };
+ const char *message = NULL;
+ unsigned flags = 0;
+ struct option opts[] = {
+ OPT_STRING(0, "message", &message, N_("reason"),
+ N_("reason of the update")),
+ OPT_BIT(0 ,"no-deref", &flags,
+ N_("update <refname> not the one it points to"),
+ REF_NO_DEREF),
+ OPT_BIT(0, "create-reflog", &flags, N_("create a reflog"),
+ REF_FORCE_CREATE_REFLOG),
+ OPT_END(),
+ };
+ struct object_id newoid;
+ const char *refname;
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, opts, refs_create_usage, 0);
+ if (argc != 2)
+ usage(_("create requires reference name and an object ID"));
+
+ if (message && !*message)
+ die(_("refusing to perform update with empty message"));
+
+ repo_config(repo, git_default_config, NULL);
+
+ refname = argv[0];
+ if (repo_get_oid_with_flags(repo, argv[1], &newoid, GET_OID_SKIP_AMBIGUITY_CHECK))
+ die(_("invalid object ID: '%s'"), argv[1]);
+ if (is_null_oid(&newoid))
+ die(_("cannot create reference with null old object ID"));
+
+ ret = refs_update_ref(get_main_ref_store(repo), message, refname,
+ &newoid, null_oid(repo->hash_algo), flags,
+ UPDATE_REFS_MSG_ON_ERR);
+
+ if (ret < 0)
+ ret = 1;
+ return ret;
+}
+
static int cmd_refs_delete(int argc, const char **argv, const char *prefix,
struct repository *repo)
{
@@ -288,6 +338,7 @@ int cmd_refs(int argc,
"git refs list " COMMON_USAGE_FOR_EACH_REF,
REFS_EXISTS_USAGE,
REFS_OPTIMIZE_USAGE,
+ REFS_CREATE_USAGE,
REFS_DELETE_USAGE,
REFS_UPDATE_USAGE,
NULL,
@@ -299,6 +350,7 @@ int cmd_refs(int argc,
OPT_SUBCOMMAND("list", &fn, cmd_refs_list),
OPT_SUBCOMMAND("exists", &fn, cmd_refs_exists),
OPT_SUBCOMMAND("optimize", &fn, cmd_refs_optimize),
+ OPT_SUBCOMMAND("create", &fn, cmd_refs_create),
OPT_SUBCOMMAND("delete", &fn, cmd_refs_delete),
OPT_SUBCOMMAND("update", &fn, cmd_refs_update),
OPT_END(),
diff --git a/t/meson.build b/t/meson.build
index 2063962dab..541e6f919c 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -225,6 +225,7 @@ integration_tests = [
't1463-refs-optimize.sh',
't1464-refs-delete.sh',
't1465-refs-update.sh',
+ 't1466-refs-create.sh',
't1500-rev-parse.sh',
't1501-work-tree.sh',
't1502-rev-parse-parseopt.sh',
diff --git a/t/t1466-refs-create.sh b/t/t1466-refs-create.sh
new file mode 100755
index 0000000000..85c8bd6ea2
--- /dev/null
+++ b/t/t1466-refs-create.sh
@@ -0,0 +1,151 @@
+#!/bin/sh
+
+test_description='git refs create'
+
+. ./test-lib.sh
+
+setup_repo () {
+ git init "$1" &&
+ test_commit -C "$1" A &&
+ test_commit -C "$1" B
+}
+
+test_ref_matches () {
+ git rev-parse "$1" >expect &&
+ echo "$2" >actual &&
+ test_cmp expect actual
+}
+
+test_expect_success 'create a new reference' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs create refs/heads/foo $A &&
+ test_ref_matches refs/heads/foo "$A"
+ )
+'
+
+test_expect_success 'create fails when the reference already exists' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git refs create refs/heads/foo $A &&
+ test_must_fail git refs create refs/heads/foo $B 2>err &&
+ test_grep "reference already exists" err &&
+ test_ref_matches refs/heads/foo "$A"
+ )
+'
+
+test_expect_success 'create with null new value fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ test_must_fail git refs create refs/heads/foo $ZERO_OID 2>err &&
+ test_grep "null old object ID" err &&
+ test_must_fail git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'create with invalid new value fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ test_must_fail git refs create refs/heads/foo invalid-oid 2>err &&
+ test_grep "invalid object ID" err &&
+ test_must_fail git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'create does not create a reflog by default' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs create refs/foo $A &&
+ test_must_fail git reflog exists refs/foo
+ )
+'
+
+test_expect_success 'create creates a reflog with --create-reflog' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs create --create-reflog refs/foo $A &&
+ git reflog exists refs/foo
+ )
+'
+
+test_expect_success 'create with message records reason in reflog' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs create --message="create reason" refs/heads/foo $A &&
+ git reflog show refs/heads/foo >actual &&
+ test_grep "create reason$" actual
+ )
+'
+
+test_expect_success 'create with symref target creates target reference' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git symbolic-ref refs/heads/symref refs/heads/target &&
+ git refs create refs/heads/symref $A &&
+ git reflog exists refs/heads/target
+ )
+'
+
+test_expect_success 'create with symref target and --no-deref refuses to create reference' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git symbolic-ref refs/heads/symref refs/heads/target &&
+ test_must_fail git refs create --no-deref refs/heads/symref $A 2>err &&
+ test_grep "dangling symref already exists" err &&
+ test_must_fail git reflog exists refs/heads/target
+ )
+'
+
+test_expect_success 'create with empty message fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ test_must_fail git refs create --message= refs/heads/foo $A 2>err &&
+ test_grep "empty message" err &&
+ test_must_fail git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'create without arguments fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ test_must_fail git -C repo refs create 2>err &&
+ test_grep "requires reference name" err
+'
+
+test_expect_success 'create with too many arguments fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ test_must_fail git -C repo refs create refs/heads/foo a b 2>err &&
+ test_grep "requires reference name" err
+'
+
+test_done
--
2.55.0.rc0.786.g65d90a0328.dirty
^ permalink raw reply related
* [PATCH v2 3/5] builtin/refs: add "update" subcommand
From: Patrick Steinhardt @ 2026-06-17 10:16 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano
In-Reply-To: <20260617-pks-refs-writing-subcommands-v2-0-07f3d18336f9@pks.im>
Add a new "update" subcommand which mirrors `git update-ref <refname>
<oldoid> <newoid>`. This follows the same reasoning as the preceding
commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-refs.adoc | 12 ++
builtin/refs.c | 55 +++++++++
t/meson.build | 1 +
t/t1465-refs-update.sh | 268 ++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 336 insertions(+)
diff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc
index 2633934463..6475bdcc62 100644
--- a/Documentation/git-refs.adoc
+++ b/Documentation/git-refs.adoc
@@ -21,6 +21,7 @@ git refs list [--count=<count>] [--shell|--perl|--python|--tcl]
git refs exists <ref>
git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]
git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]
+git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]
DESCRIPTION
-----------
@@ -58,6 +59,13 @@ delete::
reference is only deleted after verifying that it currently contains
`<old-value>`.
+update::
+ Update the given reference to point at `<new-value>`. If `<old-value>`
+ is given, the reference is only updated after verifying that it
+ currently contains `<old-value>`. As a special case, an all-zeroes
+ `<new-value>` deletes the branch, whereas an all-zeroes `<old-value>`
+ ensures that the branch does not yet exist.
+
OPTIONS
-------
@@ -99,6 +107,10 @@ include::pack-refs-options.adoc[]
The following options are specific to commands which write references:
+`--create-reflog`::
+ Create a reflog for the reference even if one would not ordinarily be
+ created.
+
`--message=<reason>`::
Use the given <reason> string for the reflog entry associated with the
update. An empty message is rejected.
diff --git a/builtin/refs.c b/builtin/refs.c
index edb7d61663..08453ae1c8 100644
--- a/builtin/refs.c
+++ b/builtin/refs.c
@@ -24,6 +24,9 @@
#define REFS_DELETE_USAGE \
N_("git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]")
+#define REFS_UPDATE_USAGE \
+ N_("git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]")
+
static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,
struct repository *repo)
{
@@ -224,6 +227,56 @@ static int cmd_refs_delete(int argc, const char **argv, const char *prefix,
return ret;
}
+static int cmd_refs_update(int argc, const char **argv, const char *prefix,
+ struct repository *repo)
+{
+ static char const * const refs_update_usage[] = {
+ REFS_UPDATE_USAGE,
+ NULL
+ };
+ const char *message = NULL;
+ unsigned flags = 0;
+ struct option opts[] = {
+ OPT_STRING(0, "message", &message, N_("reason"),
+ N_("reason of the update")),
+ OPT_BIT(0 ,"no-deref", &flags,
+ N_("update <refname> not the one it points to"),
+ REF_NO_DEREF),
+ OPT_BIT(0, "create-reflog", &flags, N_("create a reflog"),
+ REF_FORCE_CREATE_REFLOG),
+ OPT_END(),
+ };
+ struct object_id newoid, oldoid;
+ const char *refname;
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, opts, refs_update_usage, 0);
+ if (argc < 2 || argc > 3)
+ usage(_("update requires reference name, new value and an optional old value"));
+
+ if (message && !*message)
+ die(_("refusing to perform update with empty message"));
+
+ repo_config(repo, git_default_config, NULL);
+
+ refname = argv[0];
+ if (repo_get_oid_with_flags(repo, argv[1], &newoid,
+ GET_OID_SKIP_AMBIGUITY_CHECK))
+ die(_("invalid new object ID: '%s'"), argv[1]);
+ if (argc == 3 &&
+ repo_get_oid_with_flags(repo, argv[2], &oldoid,
+ GET_OID_SKIP_AMBIGUITY_CHECK))
+ die(_("invalid old object ID: '%s'"), argv[2]);
+
+ ret = refs_update_ref(get_main_ref_store(repo), message, refname,
+ &newoid, argc == 3 ? &oldoid : NULL, flags,
+ UPDATE_REFS_MSG_ON_ERR);
+
+ if (ret < 0)
+ ret = 1;
+ return ret;
+}
+
int cmd_refs(int argc,
const char **argv,
const char *prefix,
@@ -236,6 +289,7 @@ int cmd_refs(int argc,
REFS_EXISTS_USAGE,
REFS_OPTIMIZE_USAGE,
REFS_DELETE_USAGE,
+ REFS_UPDATE_USAGE,
NULL,
};
parse_opt_subcommand_fn *fn = NULL;
@@ -246,6 +300,7 @@ int cmd_refs(int argc,
OPT_SUBCOMMAND("exists", &fn, cmd_refs_exists),
OPT_SUBCOMMAND("optimize", &fn, cmd_refs_optimize),
OPT_SUBCOMMAND("delete", &fn, cmd_refs_delete),
+ OPT_SUBCOMMAND("update", &fn, cmd_refs_update),
OPT_END(),
};
diff --git a/t/meson.build b/t/meson.build
index 1ccf08a3b5..2063962dab 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -224,6 +224,7 @@ integration_tests = [
't1462-refs-exists.sh',
't1463-refs-optimize.sh',
't1464-refs-delete.sh',
+ 't1465-refs-update.sh',
't1500-rev-parse.sh',
't1501-work-tree.sh',
't1502-rev-parse-parseopt.sh',
diff --git a/t/t1465-refs-update.sh b/t/t1465-refs-update.sh
new file mode 100755
index 0000000000..a9becdda99
--- /dev/null
+++ b/t/t1465-refs-update.sh
@@ -0,0 +1,268 @@
+#!/bin/sh
+
+test_description='git refs update'
+
+. ./test-lib.sh
+
+setup_repo () {
+ git init "$1" &&
+ test_commit -C "$1" A &&
+ test_commit -C "$1" B
+}
+
+test_ref_matches () {
+ git rev-parse "$1" >expect &&
+ echo "$2" >actual &&
+ test_cmp expect actual
+}
+
+test_expect_success 'update creates a new reference' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs update refs/heads/foo $A &&
+ test_ref_matches refs/heads/foo "$A"
+ )
+'
+
+test_expect_success 'update an existing reference without oldvalue' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git refs update refs/heads/foo $A &&
+ git refs update refs/heads/foo $B &&
+ test_ref_matches refs/heads/foo $B
+ )
+'
+
+test_expect_success 'update with matching oldvalue' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git refs update refs/heads/foo $A &&
+ git refs update refs/heads/foo $B $A &&
+ test_ref_matches refs/heads/foo $B
+ )
+'
+
+test_expect_success 'update with stale oldvalue fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git refs update refs/heads/foo $A &&
+ test_must_fail git refs update refs/heads/foo $B $B 2>err &&
+ test_grep " but expected " err &&
+ test_ref_matches refs/heads/foo $A
+ )
+'
+
+test_expect_success 'update can create a new branch with oldvalue' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs update refs/heads/foo $A $ZERO_OID 2>err &&
+ test_ref_matches refs/heads/foo $A
+ )
+'
+
+test_expect_success 'update can create a new branch without oldvalue' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs update refs/heads/foo $A 2>err &&
+ test_ref_matches refs/heads/foo $A
+ )
+'
+
+test_expect_success 'update refuses to create preexisting branch' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git refs update refs/heads/foo $A &&
+ test_must_fail git refs update refs/heads/foo $B $ZERO_OID 2>err &&
+ test_grep "reference already exists" err &&
+ test_ref_matches refs/heads/foo $A
+ )
+'
+
+test_expect_success 'update can delete a branch with oldvalue' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs update refs/heads/foo $A 2>err &&
+ git refs update refs/heads/foo $ZERO_OID $A 2>err &&
+ test_must_fail git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'update can delete a branch without oldvalue' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs update refs/heads/foo $A 2>err &&
+ git refs update refs/heads/foo $ZERO_OID 2>err &&
+ test_must_fail git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'update refuses to delete a branch with mismatching value' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git refs update refs/heads/foo $A 2>err &&
+ test_must_fail git refs update refs/heads/foo $ZERO_OID $B 2>err &&
+ test_grep " but expected " err &&
+ git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'update refuses to create preexisting branch' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git refs update refs/heads/foo $A &&
+ test_must_fail git refs update refs/heads/foo $B $ZERO_OID 2>err &&
+ test_grep "reference already exists" err &&
+ test_ref_matches refs/heads/foo $A
+ )
+'
+
+
+test_expect_success 'update with invalid new value fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ test_must_fail git refs update refs/heads/foo invalid-oid 2>err &&
+ test_grep "invalid new object ID" err &&
+ test_must_fail git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'update with invalid old value fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git refs update refs/heads/foo $A &&
+ test_must_fail git refs update refs/heads/foo $B invalid-oid 2>err &&
+ test_grep "invalid old object ID" err &&
+ test_ref_matches refs/heads/foo $A
+ )
+'
+
+test_expect_success 'update --no-deref rewrites the symref itself' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git refs update refs/heads/foo $A &&
+ git symbolic-ref refs/heads/symref refs/heads/foo &&
+ git refs update --no-deref refs/heads/symref $B &&
+ test_must_fail git symbolic-ref refs/heads/symref &&
+ test_ref_matches refs/heads/symref $B &&
+ test_ref_matches refs/heads/foo $A
+ )
+'
+
+test_expect_success 'update does not create a reflog by default' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs update refs/foo $A &&
+ test_must_fail git reflog exists refs/foo
+ )
+'
+
+test_expect_success 'update creates a reflog with --create-reflog' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git refs update --create-reflog refs/foo $A &&
+ git reflog exists refs/foo
+ )
+'
+
+test_expect_success 'update with message records reason in reflog' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git refs update refs/heads/foo $A &&
+ git refs update --message=update-reason refs/heads/foo $B &&
+ git reflog show refs/heads/foo >actual &&
+ test_grep "update-reason$" actual
+ )
+'
+
+test_expect_success 'update with empty message fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git refs update refs/heads/foo $A &&
+ test_must_fail git refs update --message= refs/heads/foo $B 2>err &&
+ test_grep "empty message" err
+ )
+'
+
+test_expect_success 'update with too few arguments fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ test_must_fail git -C repo refs update refs/heads/foo 2>err &&
+ test_grep "requires reference name, new value" err
+'
+
+test_expect_success 'update with too many arguments fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ test_must_fail git refs update refs/heads/foo $A $B extra 2>err &&
+ test_grep "requires reference name, new value" err
+ )
+'
+
+test_done
--
2.55.0.rc0.786.g65d90a0328.dirty
^ permalink raw reply related
* [PATCH v2 2/5] builtin/refs: add "delete" subcommand
From: Patrick Steinhardt @ 2026-06-17 10:15 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano
In-Reply-To: <20260617-pks-refs-writing-subcommands-v2-0-07f3d18336f9@pks.im>
Reference-related functionality in Git is currently spread across many
different commands: git-update-ref(1), git-for-each-ref(1),
git-show-ref(1), git-pack-refs(1) and git-symbolic-ref(1). This makes it
hard for users to discover what functionality we have available to work
with references.
We have thus started to consolidate this functionality into git-refs(1),
which is a toolbox of everything related to references. Until now, the
command doesn't handle functionality of git-update-ref(1).
Fix this gap by introducing a new "delete" subcommand, which is the
equivalent of `git update-ref -d`.
Note that we're intentionally not using a generic "write" subcommand
with a "-d" flag. This is rather harder to discover, and subcommands
that are implmented as flags tend to be hard to reason about in the code
as we'd have to handle mutually-exclusive flags that stem from the other
subcommand-like modes.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-refs.adoc | 17 ++++++
builtin/refs.c | 51 +++++++++++++++++
t/meson.build | 1 +
t/t1464-refs-delete.sh | 130 ++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 199 insertions(+)
diff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc
index fa33680cc7..2633934463 100644
--- a/Documentation/git-refs.adoc
+++ b/Documentation/git-refs.adoc
@@ -20,6 +20,7 @@ git refs list [--count=<count>] [--shell|--perl|--python|--tcl]
[ --stdin | (<pattern>...)]
git refs exists <ref>
git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]
+git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]
DESCRIPTION
-----------
@@ -51,6 +52,12 @@ optimize::
usage. This subcommand is an alias for linkgit:git-pack-refs[1] and
offers identical functionality.
+delete::
+ Delete the given reference. This subcommand mirrors `git update-ref -d`
+ (see linkgit:git-update-ref[1]). When `<old-value>` is given, the
+ reference is only deleted after verifying that it currently contains
+ `<old-value>`.
+
OPTIONS
-------
@@ -90,6 +97,16 @@ The following options are specific to 'git refs optimize':
include::pack-refs-options.adoc[]
+The following options are specific to commands which write references:
+
+`--message=<reason>`::
+ Use the given <reason> string for the reflog entry associated with the
+ update. An empty message is rejected.
+
+`--no-deref`::
+ Operate on <ref> itself rather than the reference it points to via a
+ symbolic ref.
+
KNOWN LIMITATIONS
-----------------
diff --git a/builtin/refs.c b/builtin/refs.c
index f0faabf45a..edb7d61663 100644
--- a/builtin/refs.c
+++ b/builtin/refs.c
@@ -21,6 +21,9 @@
#define REFS_OPTIMIZE_USAGE \
N_("git refs optimize " PACK_REFS_OPTS)
+#define REFS_DELETE_USAGE \
+ N_("git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]")
+
static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,
struct repository *repo)
{
@@ -175,6 +178,52 @@ static int cmd_refs_optimize(int argc, const char **argv, const char *prefix,
return pack_refs_core(argc, argv, prefix, repo, refs_optimize_usage);
}
+static int cmd_refs_delete(int argc, const char **argv, const char *prefix,
+ struct repository *repo)
+{
+ static char const * const refs_delete_usage[] = {
+ REFS_DELETE_USAGE,
+ NULL
+ };
+ const char *message = NULL;
+ unsigned flags = 0;
+ struct option opts[] = {
+ OPT_STRING(0, "message", &message, N_("reason"),
+ N_("reason of the update")),
+ OPT_BIT(0 ,"no-deref", &flags,
+ N_("update <refname> not the one it points to"),
+ REF_NO_DEREF),
+ OPT_END(),
+ };
+ struct object_id oldoid;
+ const char *refname;
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, opts, refs_delete_usage, 0);
+ if (argc < 1 || argc > 2)
+ usage(_("delete requires reference name and an optional old object ID"));
+
+ if (message && !*message)
+ die(_("refusing to perform update with empty message"));
+
+ repo_config(repo, git_default_config, NULL);
+
+ refname = argv[0];
+ if (argc == 2) {
+ if (repo_get_oid_with_flags(repo, argv[1], &oldoid, GET_OID_SKIP_AMBIGUITY_CHECK))
+ die(_("invalid old object ID: '%s'"), argv[1]);
+ if (is_null_oid(&oldoid))
+ die(_("cannot delete reference with null old object ID"));
+ }
+
+ ret = refs_delete_ref(get_main_ref_store(repo), message, refname,
+ argc == 2 ? &oldoid : NULL, flags);
+
+ if (ret < 0)
+ ret = 1;
+ return ret;
+}
+
int cmd_refs(int argc,
const char **argv,
const char *prefix,
@@ -186,6 +235,7 @@ int cmd_refs(int argc,
"git refs list " COMMON_USAGE_FOR_EACH_REF,
REFS_EXISTS_USAGE,
REFS_OPTIMIZE_USAGE,
+ REFS_DELETE_USAGE,
NULL,
};
parse_opt_subcommand_fn *fn = NULL;
@@ -195,6 +245,7 @@ int cmd_refs(int argc,
OPT_SUBCOMMAND("list", &fn, cmd_refs_list),
OPT_SUBCOMMAND("exists", &fn, cmd_refs_exists),
OPT_SUBCOMMAND("optimize", &fn, cmd_refs_optimize),
+ OPT_SUBCOMMAND("delete", &fn, cmd_refs_delete),
OPT_END(),
};
diff --git a/t/meson.build b/t/meson.build
index c5832fee05..1ccf08a3b5 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -223,6 +223,7 @@ integration_tests = [
't1461-refs-list.sh',
't1462-refs-exists.sh',
't1463-refs-optimize.sh',
+ 't1464-refs-delete.sh',
't1500-rev-parse.sh',
't1501-work-tree.sh',
't1502-rev-parse-parseopt.sh',
diff --git a/t/t1464-refs-delete.sh b/t/t1464-refs-delete.sh
new file mode 100755
index 0000000000..efff7d0574
--- /dev/null
+++ b/t/t1464-refs-delete.sh
@@ -0,0 +1,130 @@
+#!/bin/sh
+
+test_description='git refs delete'
+
+. ./test-lib.sh
+
+setup_repo () {
+ git init "$1" &&
+ test_commit -C "$1" A &&
+ test_commit -C "$1" B
+}
+
+test_expect_success 'delete without oldvalue verification' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ A=$(git -C repo rev-parse A) &&
+ git -C repo update-ref refs/heads/foo $A &&
+ git -C repo refs delete refs/heads/foo &&
+ test_must_fail git -C repo show-ref --verify -q refs/heads/foo
+'
+
+test_expect_success 'delete with matching oldvalue' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git update-ref refs/heads/foo $A &&
+ git refs delete refs/heads/foo $A &&
+ test_must_fail git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'delete with stale oldvalue fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ B=$(git rev-parse B) &&
+ git update-ref refs/heads/foo $A &&
+ test_must_fail git refs delete refs/heads/foo $B 2>err &&
+ test_grep " but expected " err &&
+ git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'delete with null oldvalue fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git update-ref refs/heads/foo $A &&
+ test_must_fail git refs delete refs/heads/foo $ZERO_OID 2>err &&
+ test_grep "null old object ID" err &&
+ git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'delete with invalid oldvalue fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git update-ref refs/heads/foo $A &&
+ test_must_fail git refs delete refs/heads/foo invalid-oid 2>err &&
+ test_grep "invalid old object ID" err &&
+ git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'delete symref with --no-deref leaves target intact' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git update-ref refs/heads/foo $A &&
+ git symbolic-ref refs/heads/symref refs/heads/foo &&
+ git refs delete --no-deref refs/heads/symref &&
+ test_must_fail git refs exists refs/heads/symref &&
+ git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'delete with message records reason in reflog' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git update-ref refs/heads/foo $A &&
+ git symbolic-ref HEAD refs/heads/foo &&
+ git refs delete --message=delete-reason refs/heads/foo &&
+ test_must_fail git refs exists refs/heads/foo &&
+ test-tool ref-store main for-each-reflog-ent HEAD >actual &&
+ test_grep "delete-reason$" actual
+ )
+'
+
+test_expect_success 'delete with empty message fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ (
+ cd repo &&
+ A=$(git rev-parse A) &&
+ git update-ref refs/heads/foo $A &&
+ test_must_fail git refs delete --message= refs/heads/foo 2>err &&
+ test_grep "empty message" err &&
+ git refs exists refs/heads/foo
+ )
+'
+
+test_expect_success 'delete without arguments fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ test_must_fail git -C repo refs delete 2>err &&
+ test_grep "requires reference name" err
+'
+
+test_expect_success 'delete with too many arguments fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
+ test_must_fail git refs delete one two three 2>err &&
+ test_grep "requires reference name" err
+'
+
+test_done
--
2.55.0.rc0.786.g65d90a0328.dirty
^ permalink raw reply related
* [PATCH v2 1/5] builtin/refs: drop `the_repository`
From: Patrick Steinhardt @ 2026-06-17 10:15 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano
In-Reply-To: <20260617-pks-refs-writing-subcommands-v2-0-07f3d18336f9@pks.im>
We still have a couple of uses of `the_repository` in "builtin/refs.c".
All of those are trivial to convert though as the command always
requires a repository to exist.
Convert them to use the passed-in repository and drop
`USE_THE_REPOSITORY_VARIABLE`.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
builtin/refs.c | 15 +++++++--------
1 file changed, 7 insertions(+), 8 deletions(-)
diff --git a/builtin/refs.c b/builtin/refs.c
index e3125bc61b..f0faabf45a 100644
--- a/builtin/refs.c
+++ b/builtin/refs.c
@@ -1,4 +1,3 @@
-#define USE_THE_REPOSITORY_VARIABLE
#include "builtin.h"
#include "config.h"
#include "fsck.h"
@@ -23,7 +22,7 @@
N_("git refs optimize " PACK_REFS_OPTS)
static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,
- struct repository *repo UNUSED)
+ struct repository *repo)
{
const char * const migrate_usage[] = {
REFS_MIGRATE_USAGE,
@@ -59,13 +58,13 @@ static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,
goto out;
}
- if (the_repository->ref_storage_format == format) {
+ if (repo->ref_storage_format == format) {
err = error(_("repository already uses '%s' format"),
ref_storage_format_to_name(format));
goto out;
}
- if (repo_migrate_ref_storage_format(the_repository, format, flags, &errbuf) < 0) {
+ if (repo_migrate_ref_storage_format(repo, format, flags, &errbuf) < 0) {
err = error("%s", errbuf.buf);
goto out;
}
@@ -99,8 +98,8 @@ static int cmd_refs_verify(int argc, const char **argv, const char *prefix,
if (argc)
usage(_("'git refs verify' takes no arguments"));
- repo_config(the_repository, git_fsck_config, &fsck_refs_options);
- prepare_repo_settings(the_repository);
+ repo_config(repo, git_fsck_config, &fsck_refs_options);
+ prepare_repo_settings(repo);
worktrees = get_worktrees_without_reading_head();
for (size_t i = 0; worktrees[i]; i++)
@@ -124,7 +123,7 @@ static int cmd_refs_list(int argc, const char **argv, const char *prefix,
}
static int cmd_refs_exists(int argc, const char **argv, const char *prefix,
- struct repository *repo UNUSED)
+ struct repository *repo)
{
struct strbuf unused_referent = STRBUF_INIT;
struct object_id unused_oid;
@@ -145,7 +144,7 @@ static int cmd_refs_exists(int argc, const char **argv, const char *prefix,
die(_("'git refs exists' requires a reference"));
ref = *argv++;
- if (refs_read_raw_ref(get_main_ref_store(the_repository), ref,
+ if (refs_read_raw_ref(get_main_ref_store(repo), ref,
&unused_oid, &unused_referent, &unused_type,
&failure_errno)) {
if (failure_errno == ENOENT || failure_errno == EISDIR) {
--
2.55.0.rc0.786.g65d90a0328.dirty
^ permalink raw reply related
* [PATCH v2 0/5] builtin/refs: add ability to write references
From: Patrick Steinhardt @ 2026-06-17 10:15 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano
In-Reply-To: <20260616-pks-refs-writing-subcommands-v1-0-9f5219b6109d@pks.im>
Hi,
Reference-related functionality in Git is currently spread across many
different commands: git-update-ref(1), git-for-each-ref(1),
git-show-ref(1), git-pack-refs(1) and git-symbolic-ref(1). This makes it
hard for users to discover what functionality we have available to work
with references.
We have thus started to consolidate this functionality into git-refs(1),
which is a toolbox of everything related to references. Until now, the
command doesn't handle functionality of git-update-ref(1).
This patch series backfills most of the functionality by introducing
three new commands:
- `git refs delete` to delete references. This is the equivalent of
`git update-ref -d`.
- `git refs update` to update references. This is the equivalent of
`git update-ref <refname> <oldvalue> <newvalue>`.
- `git refs rename` to rename a reference, including its reflog. This
does not have an equivalent in git-update-ref(1), but is inspired by
and supersedes [1].
Changes in v2:
- Add a new "create" subcommand.
- Consistently quote in error messages.
- Consistently use `<old-value>` in the synopsis.
- Don't return negative exit codes.
- Improve documentation of "update" subcommand to mention that you can
create and delete branches.
- Add tests to verify that we can use "update" to do this, both in
racy and raceless ways.
- Add missing calls to `repo_config()`.
- Drop useless `GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME` variable.
- Link to v1: https://patch.msgid.link/20260616-pks-refs-writing-subcommands-v1-0-9f5219b6109d@pks.im
Thanks!
Patrick
[1]: <xmqqv7brz9ba.fsf@gitster.g>
---
Patrick Steinhardt (5):
builtin/refs: drop `the_repository`
builtin/refs: add "delete" subcommand
builtin/refs: add "update" subcommand
builtin/refs: add "create" subcommand
builtin/refs: add "rename" subcommand
Documentation/git-refs.adoc | 40 +++++++
builtin/refs.c | 222 ++++++++++++++++++++++++++++++++++--
t/meson.build | 4 +
t/t1464-refs-delete.sh | 130 +++++++++++++++++++++
t/t1465-refs-update.sh | 268 ++++++++++++++++++++++++++++++++++++++++++++
t/t1466-refs-create.sh | 151 +++++++++++++++++++++++++
t/t1467-refs-rename.sh | 131 ++++++++++++++++++++++
7 files changed, 938 insertions(+), 8 deletions(-)
Range-diff versus v1:
1: cfbc247e81 = 1: 6d0c5bd06f builtin/refs: drop `the_repository`
2: f5f33e5c5b ! 2: db55d87116 builtin/refs: add "delete" subcommand
@@ Documentation/git-refs.adoc: git refs list [--count=<count>] [--shell|--perl|--p
[ --stdin | (<pattern>...)]
git refs exists <ref>
git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]
-+git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]
++git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]
DESCRIPTION
-----------
@@ Documentation/git-refs.adoc: optimize::
+delete::
+ Delete the given reference. This subcommand mirrors `git update-ref -d`
-+ (see linkgit:git-update-ref[1]). When `<oldvalue>` is given, the
++ (see linkgit:git-update-ref[1]). When `<old-value>` is given, the
+ reference is only deleted after verifying that it currently contains
-+ `<oldvalue>`.
++ `<old-value>`.
+
OPTIONS
-------
@@ builtin/refs.c
N_("git refs optimize " PACK_REFS_OPTS)
+#define REFS_DELETE_USAGE \
-+ N_("git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]")
++ N_("git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]")
+
static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,
struct repository *repo)
@@ builtin/refs.c: static int cmd_refs_optimize(int argc, const char **argv, const
+ };
+ struct object_id oldoid;
+ const char *refname;
++ int ret;
+
+ argc = parse_options(argc, argv, prefix, opts, refs_delete_usage, 0);
+ if (argc < 1 || argc > 2)
@@ builtin/refs.c: static int cmd_refs_optimize(int argc, const char **argv, const
+ if (repo_get_oid_with_flags(repo, argv[1], &oldoid, GET_OID_SKIP_AMBIGUITY_CHECK))
+ die(_("invalid old object ID: '%s'"), argv[1]);
+ if (is_null_oid(&oldoid))
-+ die(_("cannot delete object with null old object ID"));
++ die(_("cannot delete reference with null old object ID"));
+ }
+
-+ return refs_delete_ref(get_main_ref_store(repo), message, refname,
-+ argc == 2 ? &oldoid : NULL, flags);
++ ret = refs_delete_ref(get_main_ref_store(repo), message, refname,
++ argc == 2 ? &oldoid : NULL, flags);
++
++ if (ret < 0)
++ ret = 1;
++ return ret;
+}
+
int cmd_refs(int argc,
@@ t/t1464-refs-delete.sh (new)
+
+test_description='git refs delete'
+
-+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
-+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
-+
+. ./test-lib.sh
+
+setup_repo () {
3: 1fc1bed619 ! 3: 85f07a2cb0 builtin/refs: add "update" subcommand
@@ Documentation/git-refs.adoc
@@ Documentation/git-refs.adoc: git refs list [--count=<count>] [--shell|--perl|--python|--tcl]
git refs exists <ref>
git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]
- git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]
+ git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]
+git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]
DESCRIPTION
-----------
@@ Documentation/git-refs.adoc: delete::
reference is only deleted after verifying that it currently contains
- `<oldvalue>`.
+ `<old-value>`.
+update::
-+ Update the given reference to point at `<new-value>`. This subcommand
-+ mirrors `git update-ref` (see linkgit:git-update-ref[1]). When
-+ `<old-value>` is given, the reference is only updated after verifying
-+ that it currently contains `<old-value>`.
++ Update the given reference to point at `<new-value>`. If `<old-value>`
++ is given, the reference is only updated after verifying that it
++ currently contains `<old-value>`. As a special case, an all-zeroes
++ `<new-value>` deletes the branch, whereas an all-zeroes `<old-value>`
++ ensures that the branch does not yet exist.
+
OPTIONS
-------
+@@ Documentation/git-refs.adoc: include::pack-refs-options.adoc[]
+
+ The following options are specific to commands which write references:
+
++`--create-reflog`::
++ Create a reflog for the reference even if one would not ordinarily be
++ created.
++
+ `--message=<reason>`::
+ Use the given <reason> string for the reflog entry associated with the
+ update. An empty message is rejected.
## builtin/refs.c ##
@@
#define REFS_DELETE_USAGE \
- N_("git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]")
+ N_("git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]")
+#define REFS_UPDATE_USAGE \
+ N_("git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]")
@@ builtin/refs.c
struct repository *repo)
{
@@ builtin/refs.c: static int cmd_refs_delete(int argc, const char **argv, const char *prefix,
- argc == 2 ? &oldoid : NULL, flags);
+ return ret;
}
+static int cmd_refs_update(int argc, const char **argv, const char *prefix,
@@ builtin/refs.c: static int cmd_refs_delete(int argc, const char **argv, const ch
+ };
+ struct object_id newoid, oldoid;
+ const char *refname;
++ int ret;
+
+ argc = parse_options(argc, argv, prefix, opts, refs_update_usage, 0);
+ if (argc < 2 || argc > 3)
@@ builtin/refs.c: static int cmd_refs_delete(int argc, const char **argv, const ch
+ refname = argv[0];
+ if (repo_get_oid_with_flags(repo, argv[1], &newoid,
+ GET_OID_SKIP_AMBIGUITY_CHECK))
-+ die(_("invalid new object ID: %s"), argv[1]);
++ die(_("invalid new object ID: '%s'"), argv[1]);
+ if (argc == 3 &&
+ repo_get_oid_with_flags(repo, argv[2], &oldoid,
+ GET_OID_SKIP_AMBIGUITY_CHECK))
-+ die(_("invalid old object ID: %s"), argv[2]);
++ die(_("invalid old object ID: '%s'"), argv[2]);
+
-+ return refs_update_ref(get_main_ref_store(repo), message, refname,
-+ &newoid, argc == 3 ? &oldoid : NULL, flags,
-+ UPDATE_REFS_DIE_ON_ERR);
++ ret = refs_update_ref(get_main_ref_store(repo), message, refname,
++ &newoid, argc == 3 ? &oldoid : NULL, flags,
++ UPDATE_REFS_MSG_ON_ERR);
++
++ if (ret < 0)
++ ret = 1;
++ return ret;
+}
+
int cmd_refs(int argc,
@@ t/t1465-refs-update.sh (new)
+ )
+'
+
++test_expect_success 'update can create a new branch with oldvalue' '
++ test_when_finished "rm -rf repo" &&
++ setup_repo repo &&
++ (
++ cd repo &&
++ A=$(git rev-parse A) &&
++ git refs update refs/heads/foo $A $ZERO_OID 2>err &&
++ test_ref_matches refs/heads/foo $A
++ )
++'
++
++test_expect_success 'update can create a new branch without oldvalue' '
++ test_when_finished "rm -rf repo" &&
++ setup_repo repo &&
++ (
++ cd repo &&
++ A=$(git rev-parse A) &&
++ git refs update refs/heads/foo $A 2>err &&
++ test_ref_matches refs/heads/foo $A
++ )
++'
++
++test_expect_success 'update refuses to create preexisting branch' '
++ test_when_finished "rm -rf repo" &&
++ setup_repo repo &&
++ (
++ cd repo &&
++ A=$(git rev-parse A) &&
++ B=$(git rev-parse B) &&
++ git refs update refs/heads/foo $A &&
++ test_must_fail git refs update refs/heads/foo $B $ZERO_OID 2>err &&
++ test_grep "reference already exists" err &&
++ test_ref_matches refs/heads/foo $A
++ )
++'
++
++test_expect_success 'update can delete a branch with oldvalue' '
++ test_when_finished "rm -rf repo" &&
++ setup_repo repo &&
++ (
++ cd repo &&
++ A=$(git rev-parse A) &&
++ git refs update refs/heads/foo $A 2>err &&
++ git refs update refs/heads/foo $ZERO_OID $A 2>err &&
++ test_must_fail git refs exists refs/heads/foo
++ )
++'
++
++test_expect_success 'update can delete a branch without oldvalue' '
++ test_when_finished "rm -rf repo" &&
++ setup_repo repo &&
++ (
++ cd repo &&
++ A=$(git rev-parse A) &&
++ git refs update refs/heads/foo $A 2>err &&
++ git refs update refs/heads/foo $ZERO_OID 2>err &&
++ test_must_fail git refs exists refs/heads/foo
++ )
++'
++
++test_expect_success 'update refuses to delete a branch with mismatching value' '
++ test_when_finished "rm -rf repo" &&
++ setup_repo repo &&
++ (
++ cd repo &&
++ A=$(git rev-parse A) &&
++ B=$(git rev-parse B) &&
++ git refs update refs/heads/foo $A 2>err &&
++ test_must_fail git refs update refs/heads/foo $ZERO_OID $B 2>err &&
++ test_grep " but expected " err &&
++ git refs exists refs/heads/foo
++ )
++'
++
++test_expect_success 'update refuses to create preexisting branch' '
++ test_when_finished "rm -rf repo" &&
++ setup_repo repo &&
++ (
++ cd repo &&
++ A=$(git rev-parse A) &&
++ B=$(git rev-parse B) &&
++ git refs update refs/heads/foo $A &&
++ test_must_fail git refs update refs/heads/foo $B $ZERO_OID 2>err &&
++ test_grep "reference already exists" err &&
++ test_ref_matches refs/heads/foo $A
++ )
++'
++
++
+test_expect_success 'update with invalid new value fails' '
+ test_when_finished "rm -rf repo" &&
+ setup_repo repo &&
-: ---------- > 4: 03036ef730 builtin/refs: add "create" subcommand
4: aadedb14e1 ! 5: 65f0ee4f03 builtin/refs: add "rename" subcommand
@@ Commit message
Signed-off-by: Patrick Steinhardt <ps@pks.im>
## Documentation/git-refs.adoc ##
-@@ Documentation/git-refs.adoc: git refs exists <ref>
- git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]
- git refs delete [--message=<reason>] [--no-deref] <ref> [<oldvalue>]
+@@ Documentation/git-refs.adoc: git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude
+ git refs create [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value>
+ git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]
git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]
-+git refs rename [--message=<reason>] <oldref> <newref>
++git refs rename [--message=<reason>] <old-ref> <new-ref>
DESCRIPTION
-----------
@@ Documentation/git-refs.adoc: update::
- `<old-value>` is given, the reference is only updated after verifying
- that it currently contains `<old-value>`.
+ `<new-value>` deletes the branch, whereas an all-zeroes `<old-value>`
+ ensures that the branch does not yet exist.
+rename::
+ Rename the reference `<oldref>` to `<newref>`. The old reference must
@@ Documentation/git-refs.adoc: update::
OPTIONS
-------
-@@ Documentation/git-refs.adoc: include::pack-refs-options.adoc[]
-
- The following options are specific to commands which write references:
-
-+`--create-reflog`::
-+ Create a reflog for the reference even if one would not ordinarily be
-+ created.
-+
- `--message=<reason>`::
- Use the given <reason> string for the reflog entry associated with the
- update. An empty message is rejected.
## builtin/refs.c ##
@@
@@ builtin/refs.c
N_("git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]")
+#define REFS_RENAME_USAGE \
-+ N_("git refs rename [--message=<reason>] <oldref> <newref>")
++ N_("git refs rename [--message=<reason>] <old-ref> <new-ref>")
+
static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,
struct repository *repo)
{
@@ builtin/refs.c: static int cmd_refs_update(int argc, const char **argv, const char *prefix,
- UPDATE_REFS_DIE_ON_ERR);
+ return ret;
}
+static int cmd_refs_rename(int argc, const char **argv, const char *prefix,
@@ builtin/refs.c: static int cmd_refs_update(int argc, const char **argv, const ch
+ OPT_END(),
+ };
+ const char *oldref, *newref;
++ int ret;
+
+ argc = parse_options(argc, argv, prefix, opts, refs_rename_usage, 0);
+ if (argc != 2)
@@ builtin/refs.c: static int cmd_refs_update(int argc, const char **argv, const ch
+ if (message && !*message)
+ die(_("refusing to perform update with empty message"));
+
++ repo_config(repo, git_default_config, NULL);
++
+ oldref = argv[0];
+ newref = argv[1];
+
+ if (check_refname_format(oldref, 0))
-+ die(_("invalid ref format: %s"), oldref);
++ die(_("invalid ref format: '%s'"), oldref);
+ if (check_refname_format(newref, 0))
-+ die(_("invalid ref format: %s"), newref);
++ die(_("invalid ref format: '%s'"), newref);
+
+ if (!refs_ref_exists(get_main_ref_store(repo), oldref))
+ die(_("reference does not exist: '%s'"), oldref);
+ if (refs_ref_exists(get_main_ref_store(repo), newref))
+ die(_("reference already exists: '%s'"), newref);
+
-+ return refs_rename_ref(get_main_ref_store(repo), oldref, newref, message);
++ ret = refs_rename_ref(get_main_ref_store(repo), oldref, newref, message);
++
++ if (ret < 0)
++ ret = 1;
++ return ret;
+}
+
int cmd_refs(int argc,
const char **argv,
const char *prefix,
@@ builtin/refs.c: int cmd_refs(int argc,
- REFS_OPTIMIZE_USAGE,
+ REFS_CREATE_USAGE,
REFS_DELETE_USAGE,
REFS_UPDATE_USAGE,
+ REFS_RENAME_USAGE,
@@ builtin/refs.c: int cmd_refs(int argc,
};
parse_opt_subcommand_fn *fn = NULL;
@@ builtin/refs.c: int cmd_refs(int argc,
- OPT_SUBCOMMAND("optimize", &fn, cmd_refs_optimize),
+ OPT_SUBCOMMAND("create", &fn, cmd_refs_create),
OPT_SUBCOMMAND("delete", &fn, cmd_refs_delete),
OPT_SUBCOMMAND("update", &fn, cmd_refs_update),
+ OPT_SUBCOMMAND("rename", &fn, cmd_refs_rename),
@@ builtin/refs.c: int cmd_refs(int argc,
## t/meson.build ##
@@ t/meson.build: integration_tests = [
- 't1463-refs-optimize.sh',
't1464-refs-delete.sh',
't1465-refs-update.sh',
-+ 't1466-refs-rename.sh',
+ 't1466-refs-create.sh',
++ 't1467-refs-rename.sh',
't1500-rev-parse.sh',
't1501-work-tree.sh',
't1502-rev-parse-parseopt.sh',
- ## t/t1466-refs-rename.sh (new) ##
+ ## t/t1467-refs-rename.sh (new) ##
@@
+#!/bin/sh
+
---
base-commit: 700432b2ba22603a0bcb71475c9c333d17c9b0d1
change-id: 20260616-pks-refs-writing-subcommands-7a77be5bda9b
^ permalink raw reply
* [PATCH] osxkeychain: fix build with Rust
From: Johannes Schindelin via GitGitGadget @ 2026-06-17 10:11 UTC (permalink / raw)
To: git; +Cc: Johannes Schindelin, Johannes Schindelin
From: Johannes Schindelin <johannes.schindelin@gmx.de>
Without NO_RUST defined, the varint encoder/decoder lives in the
RUST_LIB, which needs to be linked. Symptom:
cc [... -o contrib/credential/osxkeychain/git-credential-osxkeychain [...]
Undefined symbols for architecture x86_64:
"_decode_varint", referenced from:
_read_untracked_extension in libgit.a[x86_64][63](dir.o)
_read_untracked_extension in libgit.a[x86_64][63](dir.o)
_read_one_dir in libgit.a[x86_64][63](dir.o)
_read_one_dir in libgit.a[x86_64][63](dir.o)
_load_cache_entry_block in libgit.a[x86_64][174](read-cache.o)
"_encode_varint", referenced from:
_write_untracked_extension in libgit.a[x86_64][63](dir.o)
_write_untracked_extension in libgit.a[x86_64][63](dir.o)
_write_untracked_extension in libgit.a[x86_64][63](dir.o)
_write_one_dir in libgit.a[x86_64][63](dir.o)
_write_one_dir in libgit.a[x86_64][63](dir.o)
_do_write_index in libgit.a[x86_64][174](read-cache.o)
ld: symbol(s) not found for architecture x86_64
While it is curious why these functions are needed at all (osxkeychain
does not read or write the index), the compile error is a real problem.
Instead of trying to play games to add `GITLIBS` while filtering out
`common-main.o`, replace the `$(LIB_FILE) $(EXTLIBS)` construct with the
much shorter `$(LIBS)` construct that _already_ filters out
`common-main.o` and adds the Rust library when needed.
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
osxkeychain: fix build with Rust
I ran into this when trying to build Microsoft Git v2.55.0-rc0. This
seems to be similar in spirit to
https://lore.kernel.org/git/pull.2288.git.git.1778001976709.gitgitgadget@gmail.com/
but the latter seems not to have gained traction. This build failure is
a hard regression in v2.55.0, though.
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2154%2Fdscho%2Fosxkeychain-vs-rust-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2154/dscho/osxkeychain-vs-rust-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/2154
Makefile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Makefile b/Makefile
index 0976a69b4c..1cec251f43 100644
--- a/Makefile
+++ b/Makefile
@@ -4074,7 +4074,7 @@ contrib/libgit-sys/libgitpub.a: $(LIBGIT_HIDDEN_EXPORT)
contrib/credential/osxkeychain/git-credential-osxkeychain: contrib/credential/osxkeychain/git-credential-osxkeychain.o $(LIB_FILE) GIT-LDFLAGS
$(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) \
- $(filter %.o,$^) $(LIB_FILE) $(EXTLIBS) -framework Security -framework CoreFoundation
+ $(filter %.o,$^) $(LIBS) -framework Security -framework CoreFoundation
contrib/credential/osxkeychain/git-credential-osxkeychain.o: contrib/credential/osxkeychain/git-credential-osxkeychain.c GIT-CFLAGS
$(QUIET_LINK)$(CC) -o $@ -c $(dep_args) $(compdb_args) $(ALL_CFLAGS) $(EXTRA_CPPFLAGS) $<
base-commit: 0fae78c9d55efe705877ea537fe42c59164ccd94
--
gitgitgadget
^ permalink raw reply related
* Re: [PATCH v15 0/7] branch: delete-merged
From: Phillip Wood @ 2026-06-17 10:01 UTC (permalink / raw)
To: Harald Nordgren via GitGitGadget, git
Cc: Kristoffer Haugsbakk, Johannes Sixt, Phillip Wood,
Harald Nordgren
In-Reply-To: <pull.2285.v15.git.git.1781542042.gitgitgadget@gmail.com>
Hi Harald
Our SubmittingPatches documentation recommends waiting for the
discussion to settle before sending a new version. When you know someone
is going send more comments on a series it is a good idea to wait for
them before sending a new version to avoid too much churn on the list
which makes it hard for people to keep up. I'm not going to read this
version in detail because I know another version will be needed but I
did spot a couple of things in the summary below.
On 15/06/2026 17:47, Harald Nordgren via GitGitGadget wrote:
> * Renamed --prune-merged to --delete-merged throughout. Not necessarily
> final, but something to advance the discussion.
> * --delete-merged now silently skips not-yet-merged branches instead of
> warning.
Good
> * --forked now accepts a bare remote name (e.g. origin) for the branch
> origin/HEAD points at using DWIM.
The range-diff below does not show any changes to the implementation,
only the Documentation and tests
> * Initialized the delete_branches() flag locals where declared. Only force
> stays deferred.
Not changing force sounds like a bad idea. The whole point of unpacking
the flags at the start of the function is to avoid accidental
regressions. Unpacking the flags into separate variables means the rest
of the function does not need to know that the function arguments have
changed.
Thanks
Phillip
> * delete_branches()/check_branch_commit() doc and code cleanups: redundant
> branch NULL checks dropped, ref_array candidates = { 0 }, a BUG() for the
> unreachable non-branch ref, and reworked --delete-merged doc wording.
> * Broadened the --forked tests (local commits for realism, remote add -f,
> --forked <pattern> <branch> coverage), renamed the misleading trunk
> fixture, and replaced the misnamed detached branch with git checkout
> --detach.
>
> Harald Nordgren (7):
> branch: add --forked filter for --list mode
> branch: convert delete_branches() to a flags argument
> branch: let delete_branches skip unmerged branches on bulk refusal
> branch: prepare delete_branches for a bulk caller
> branch: add --delete-merged <branch>
> branch: add branch.<name>.deleteMerged opt-out
> branch: add --dry-run for --delete-merged
>
> Documentation/config/branch.adoc | 7 +
> Documentation/git-branch.adoc | 43 +++-
> builtin/branch.c | 184 ++++++++++++---
> ref-filter.c | 70 ++++++
> ref-filter.h | 10 +
> t/t3200-branch.sh | 387 +++++++++++++++++++++++++++++++
> 6 files changed, 673 insertions(+), 28 deletions(-)
>
>
> base-commit: ea97ad8d017de0c9037451a78008a0fd60abea0c
> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2285%2FHaraldNordgren%2Ffetch-prune-local-branches-v15
> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2285/HaraldNordgren/fetch-prune-local-branches-v15
> Pull-Request: https://github.com/git/git/pull/2285
>
> Range-diff vs v14:
>
> 1: 7383872f4b ! 1: da741b5ea7 branch: add --forked filter for --list mode
> @@ Commit message
>
> 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
> + can be a ref (e.g. "origin/main", "master"), a remote name like
> + "origin" for the branch its origin/HEAD points at, 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:
> @@ Commit message
> 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
> + This is the building block for --delete-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: superproject's "origin/main", but tracks the subm
> +`--forked <branch>`::
> + Only list branches whose configured upstream matches
> + _<branch>_. The argument can be a ref (e.g. `origin/main`,
> -+ `master`) or a shell-style glob (e.g. `'origin/*'`). The
> -+ option can be repeated to widen the filter. Implies `--list`.
> ++ `master`), a remote name like `origin` for the branch its
> ++ `origin/HEAD` points at, or a shell-style glob (e.g.
> ++ `'origin/*'`). The option can be repeated to widen the
> ++ filter. Implies `--list`.
> +
> `--points-at <object>`::
> Only list branches of _<object>_.
> @@ t/t3200-branch.sh: test_expect_success 'errors if given a bad branch name' '
> + git -C forked-other branch foreign other-base &&
> +
> + git clone forked-upstream forked &&
> -+ git -C forked remote add other ../forked-other &&
> -+ git -C forked fetch other &&
> ++ git -C forked remote add -f other ../forked-other &&
> ++ git -C forked remote set-head origin one &&
> + 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 &&
> -+ git -C forked branch --track local-trunk local-base
> ++ git -C forked branch --track local-onbase local-base &&
> ++
> ++ git -C forked checkout local-one &&
> ++ test_commit -C forked --no-tag local-one-work local-one.t &&
> ++ git -C forked checkout local-foreign &&
> ++ test_commit -C forked --no-tag local-foreign-work local-foreign.t &&
> ++ git -C forked checkout --detach
> +'
> +
> +test_expect_success '--forked <upstream-tracking-branch> filters by upstream' '
> @@ t/t3200-branch.sh: test_expect_success 'errors if given a bad branch name' '
> +
> +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 &&
> ++ echo local-onbase >expect &&
> + test_cmp expect actual
> +'
> +
> @@ t/t3200-branch.sh: test_expect_success 'errors if given a bad branch name' '
> + git -C forked branch --forked local-base --forked "other/*" --format="%(refname:short)" >actual &&
> + cat >expect <<-\EOF &&
> + local-foreign
> -+ local-trunk
> ++ local-onbase
> + EOF
> + test_cmp expect actual
> +'
> @@ t/t3200-branch.sh: test_expect_success 'errors if given a bad branch name' '
> +'
> +
> +test_expect_success '--forked composes with --no-merged' '
> -+ test_when_finished "git -C forked checkout detached" &&
> ++ test_when_finished "git -C forked checkout --detach" &&
> + git -C forked checkout local-one &&
> + test_commit -C forked local-only &&
> + git -C forked branch --forked "origin/*" --no-merged origin/one \
> @@ t/t3200-branch.sh: test_expect_success 'errors if given a bad branch name' '
> + test_must_fail git -C forked branch --forked 2>err &&
> + test_grep "requires a value" err
> +'
> ++
> ++test_expect_success '--forked <remote> uses the branch <remote>/HEAD points at' '
> ++ git -C forked branch --forked origin --format="%(refname:short)" >actual &&
> ++ echo local-one >expect &&
> ++ test_cmp expect actual
> ++'
> ++
> ++test_expect_success '--forked narrows a <pattern> argument' '
> ++ git -C forked branch --forked "origin/*" "local-*" \
> ++ --format="%(refname:short)" >actual &&
> ++ cat >expect <<-\EOF &&
> ++ local-one
> ++ local-two
> ++ EOF
> ++ test_cmp expect actual
> ++'
> +
> test_done
> 2: 7ef9502e01 ! 2: 91c35f10cc branch: let delete_branches warn instead of error on bulk refusal
> @@ Metadata
> Author: Harald Nordgren <haraldnordgren@gmail.com>
>
> ## Commit message ##
> - branch: let delete_branches warn instead of error on bulk refusal
> + branch: convert delete_branches() to a flags argument
>
> - Add a warn-only mode to delete_branches() and check_branch_commit()
> - so a bulk caller can report branches that are not fully merged as a
> - short warning and carry on, rather than erroring with the longer
> - "use 'git branch -D'" advice that the plain "git branch -d" path
> - emits. Existing callers are unaffected.
> + delete_branches() and check_branch_commit() take a pair of int
> + booleans (force and quiet) that the next commits would grow further.
> + Replace them with a single "unsigned int flags" argument and an
> + enum, splitting the bits back into named bool locals so the body
> + keeps reading the same named values.
> +
> + No change in behavior.
>
> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
>
> @@ builtin/branch.c: static int branch_merged(int kind, const char *name,
> +enum delete_branch_flags {
> + DELETE_BRANCH_FORCE = (1 << 0),
> + DELETE_BRANCH_QUIET = (1 << 1),
> -+ DELETE_BRANCH_WARN_ONLY = (1 << 2),
> +};
> +
> static int check_branch_commit(const char *branchname, const char *refname,
> @@ builtin/branch.c: static int branch_merged(int kind, const char *name,
> - int kinds, int force)
> + int kinds, unsigned int flags)
> {
> -+ int force = flags & DELETE_BRANCH_FORCE;
> ++ bool force = flags & DELETE_BRANCH_FORCE;
> struct commit *rev = lookup_commit_reference(the_repository, oid);
> if (!force && !rev) {
> error(_("couldn't look up commit object for '%s'"), refname);
> - return -1;
> - }
> - if (!force && !branch_merged(kinds, branchname, rev, head_rev)) {
> -- error(_("the branch '%s' is not fully merged"), branchname);
> -- advise_if_enabled(ADVICE_FORCE_DELETE_BRANCH,
> -- _("If you are sure you want to delete it, "
> -- "run 'git branch -D %s'"), branchname);
> -+ if (flags & DELETE_BRANCH_WARN_ONLY) {
> -+ warning(_("the branch '%s' is not fully merged"),
> -+ branchname);
> -+ } else {
> -+ error(_("the branch '%s' is not fully merged"),
> -+ branchname);
> -+ advise_if_enabled(ADVICE_FORCE_DELETE_BRANCH,
> -+ _("If you are sure you want to delete it, "
> -+ "run 'git branch -D %s'"), branchname);
> -+ }
> - return -1;
> - }
> - return 0;
> @@ builtin/branch.c: static void delete_branch_config(const char *branchname)
> strbuf_release(&buf);
> }
> @@ builtin/branch.c: static int delete_branches(int argc, const char **argv, int fo
> int i;
> int ret = 0;
> int remote_branch = 0;
> -+ int force, quiet;
> ++ bool force;
> ++ bool quiet = flags & DELETE_BRANCH_QUIET;
> struct strbuf bname = STRBUF_INIT;
> enum interpret_branch_kind allowed_interpret;
> struct string_list refs_to_delete = STRING_LIST_INIT_DUP;
> @@ builtin/branch.c: static int delete_branches(int argc, const char **argv, int fo
> branch_name_pos = strcspn(fmt, "%");
>
> + force = flags & DELETE_BRANCH_FORCE;
> -+ quiet = flags & DELETE_BRANCH_QUIET;
> +
> if (!force)
> head_rev = lookup_commit_reference(the_repository, &head_oid);
> @@ builtin/branch.c: static int delete_branches(int argc, const char **argv, int fo
> + if (!(ref_flags & (REF_ISSYMREF|REF_ISBROKEN)) &&
> check_branch_commit(bname.buf, name, &oid, head_rev, kinds,
> - force)) {
> -- ret = 1;
> + flags)) {
> -+ if (!(flags & DELETE_BRANCH_WARN_ONLY))
> -+ ret = 1;
> + ret = 1;
> goto next;
> }
>
> -: ---------- > 3: e101dd2886 branch: let delete_branches skip unmerged branches on bulk refusal
> 3: 259113e304 ! 4: 6c3534901a branch: prepare delete_branches for a bulk caller
> @@ Commit message
> branch: prepare delete_branches for a bulk caller
>
> Teach delete_branches() two new modes for the upcoming
> - --prune-merged: one that asks only whether a branch is merged into
> + --delete-merged: one that asks only whether a branch is merged into
> its upstream, without falling back to HEAD when there is no
> upstream, and one that rehearses the deletions without removing any
> ref. Existing callers keep their current behavior.
> @@ builtin/branch.c: static int branch_merged(int kind, const char *name,
> @@ builtin/branch.c: enum delete_branch_flags {
> DELETE_BRANCH_FORCE = (1 << 0),
> DELETE_BRANCH_QUIET = (1 << 1),
> - DELETE_BRANCH_WARN_ONLY = (1 << 2),
> + DELETE_BRANCH_SKIP_UNMERGED = (1 << 2),
> + DELETE_BRANCH_NO_HEAD_FALLBACK = (1 << 3),
> + DELETE_BRANCH_DRY_RUN = (1 << 4),
> };
>
> static int check_branch_commit(const char *branchname, const char *refname,
> @@ builtin/branch.c: static int delete_branches(int argc, const char **argv, int kinds,
> - int i;
> - int ret = 0;
> - int remote_branch = 0;
> -- int force, quiet;
> -+ int force, quiet, dry_run, no_head_fallback;
> + bool force;
> + bool quiet = flags & DELETE_BRANCH_QUIET;
> + bool skip_unmerged = flags & DELETE_BRANCH_SKIP_UNMERGED;
> ++ bool dry_run = flags & DELETE_BRANCH_DRY_RUN;
> ++ bool no_head_fallback = flags & DELETE_BRANCH_NO_HEAD_FALLBACK;
> struct strbuf bname = STRBUF_INIT;
> enum interpret_branch_kind allowed_interpret;
> struct string_list refs_to_delete = STRING_LIST_INIT_DUP;
> @@ builtin/branch.c: static int delete_branches(int argc, const char **argv, int kinds,
>
> force = flags & DELETE_BRANCH_FORCE;
> - quiet = flags & DELETE_BRANCH_QUIET;
> -+ dry_run = flags & DELETE_BRANCH_DRY_RUN;
> -+ no_head_fallback = flags & DELETE_BRANCH_NO_HEAD_FALLBACK;
>
> - if (!force)
> + if (!force && !no_head_fallback)
> 4: 9924373da0 ! 5: 5899013b8f branch: add --prune-merged <branch>
> @@ Metadata
> Author: Harald Nordgren <haraldnordgren@gmail.com>
>
> ## Commit message ##
> - branch: add --prune-merged <branch>
> + branch: add --delete-merged <branch>
>
> - git branch --prune-merged <branch>...
> + git branch --delete-merged <branch>...
>
> 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,
> + 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.
> + Three kinds of branches are not deleted:
>
> - 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 checked out in any worktree
> + * any branch whose upstream remote-tracking branch no longer
> + exists, 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.
> + after a pull it just looks "fully merged", so it is kept. 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.
> + A branch whose work is not yet merged into its upstream is silently
> + skipped, so one unmerged topic does not abort the whole sweep.
>
> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
>
> @@ Documentation/git-branch.adoc: 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>...
> ++git branch --delete-merged <branch>...
>
> DESCRIPTION
> -----------
> @@ Documentation/git-branch.adoc: This option is only applicable in non-verbose mod
> Print the name of the current branch. In detached `HEAD` state,
> nothing is printed.
>
> -+`--prune-merged <branch>...`::
> ++`--delete-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*'`.
> ++ --delete-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.
> ++A branch is not deleted when:
> ++
> -+A branch is left alone if any of the following holds:
> -+its upstream no longer resolves locally; it is checked out in any
> -+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".
> ++--
> ++* its upstream remote-tracking branch no longer exists,
> ++* it is checked out in any worktree, or
> ++* its push destination (`<branch>@{push}`) equals its upstream
> ++ (`<branch>@{upstream}`), so it cannot be distinguished from a
> ++ branch that just looks "fully merged" right after a pull.
> ++--
> ++
> -+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.
> ++A branch whose work has not yet been merged into its upstream is
> ++silently skipped. Delete it with `git branch -D` if you want to
> ++remove it anyway.
> +
> `-v`::
> `-vv`::
> @@ builtin/branch.c: 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>..."),
> ++ N_("git branch [<options>] --delete-merged <branch>..."),
> NULL
> };
>
> @@ builtin/branch.c: static int parse_opt_forked(const struct option *opt, const ch
> return 0;
> }
>
> -+static int prune_merged_branches(int argc, const char **argv,
> ++static int delete_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 ref_array candidates = { 0 };
> + struct strvec deletable = STRVEC_INIT;
> + int i, ret = 0;
> +
> + if (!argc)
> -+ die(_("--prune-merged requires at least one <branch>"));
> ++ die(_("--delete-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));
> + filter_refs(&candidates, &filter, filter.kind);
> +
> + for (i = 0; i < candidates.nr; i++) {
> @@ builtin/branch.c: static int parse_opt_forked(const struct option *opt, const ch
> + const char *upstream, *push;
> +
> + if (!skip_prefix(full_name, "refs/heads/", &short_name))
> -+ continue;
> ++ BUG("filter returned non-branch ref '%s'", full_name);
> + if (branch_checked_out(full_name))
> + continue;
> +
> + branch = branch_get(short_name);
> -+ upstream = branch ? branch_get_upstream(branch, NULL) : NULL;
> ++ upstream = branch_get_upstream(branch, NULL);
> + if (!upstream || !refs_ref_exists(refs, upstream))
> + continue;
> -+ push = branch ? branch_get_push(branch, NULL) : NULL;
> ++ push = branch_get_push(branch, NULL);
> + if (!push || !strcmp(push, upstream))
> + continue;
> +
> @@ builtin/branch.c: static int parse_opt_forked(const struct option *opt, const ch
> + if (deletable.nr)
> + ret = delete_branches(deletable.nr, deletable.v,
> + FILTER_REFS_BRANCHES,
> -+ DELETE_BRANCH_WARN_ONLY |
> ++ DELETE_BRANCH_SKIP_UNMERGED |
> + DELETE_BRANCH_NO_HEAD_FALLBACK |
> + (quiet ? DELETE_BRANCH_QUIET : 0));
> +
> @@ builtin/branch.c: int cmd_branch(int argc,
> /* possible actions */
> int delete = 0, rename = 0, copy = 0, list = 0,
> unset_upstream = 0, show_current = 0, edit_description = 0;
> -+ int prune_merged = 0;
> ++ int delete_merged = 0;
> const char *new_upstream = NULL;
> int noncreate_actions = 0;
> /* possible options */
> @@ builtin/branch.c: int cmd_branch(int argc,
> OPT_BOOL(0, "create-reflog", &reflog, N_("create the branch's reflog")),
> OPT_BOOL(0, "edit-description", &edit_description,
> N_("edit the description for the branch")),
> -+ OPT_BOOL(0, "prune-merged", &prune_merged,
> -+ N_("delete local branches whose upstream matches <branch> and is merged")),
> ++ OPT_BOOL(0, "delete-merged", &delete_merged,
> ++ N_("delete local branches whose upstream matches <branch> and are merged")),
> OPT__FORCE(&force, N_("force creation, move/rename, deletion"), PARSE_OPT_NOCOMPLETE),
> OPT_MERGED(&filter, N_("print only branches that are merged")),
> OPT_NO_MERGED(&filter, N_("print only branches that are not merged")),
> @@ builtin/branch.c: int cmd_branch(int argc,
>
> if (!delete && !rename && !copy && !edit_description && !new_upstream &&
> - !show_current && !unset_upstream && argc == 0)
> -+ !show_current && !unset_upstream && !prune_merged &&
> ++ !show_current && !unset_upstream && !delete_merged &&
> + argc == 0)
> list = 1;
>
> @@ builtin/branch.c: int cmd_branch(int argc,
> noncreate_actions = !!delete + !!rename + !!copy + !!new_upstream +
> !!show_current + !!list + !!edit_description +
> - !!unset_upstream;
> -+ !!unset_upstream + !!prune_merged;
> ++ !!unset_upstream + !!delete_merged;
> if (noncreate_actions > 1)
> usage_with_options(builtin_branch_usage, options);
>
> @@ builtin/branch.c: int cmd_branch(int argc,
> (delete > 1 ? DELETE_BRANCH_FORCE : 0) |
> (quiet ? DELETE_BRANCH_QUIET : 0));
> goto out;
> -+ } else if (prune_merged) {
> -+ ret = prune_merged_branches(argc, argv, quiet);
> ++ } else if (delete_merged) {
> ++ ret = delete_merged_branches(argc, argv, quiet);
> + goto out;
> } else if (show_current) {
> print_current_branch_name();
> ret = 0;
>
> ## t/t3200-branch.sh ##
> -@@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
> - test_grep "requires a value" err
> +@@ t/t3200-branch.sh: test_expect_success '--forked narrows a <pattern> argument' '
> + test_cmp expect actual
> '
>
> -+test_expect_success '--prune-merged: setup' '
> ++test_expect_success '--delete-merged: setup' '
> + test_create_repo pm-upstream &&
> + test_commit -C pm-upstream base &&
> + git -C pm-upstream checkout -b next &&
> @@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
> + test_create_repo pm-fork
> +'
> +
> -+test_expect_success '--prune-merged deletes branches integrated into upstream' '
> ++test_expect_success '--delete-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 &&
> @@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
> + 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/*" &&
> ++ git -C pm-merged branch --delete-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_expect_success '--delete-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 &&
> @@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
> + 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 &&
> ++ git -C pm-literal branch --delete-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_expect_success '--delete-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 &&
> @@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
> + 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 &&
> ++ git -C pm-union branch --delete-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_expect_success '--delete-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 checkout -b mainline &&
> + git -C pm-local branch one one-commit &&
> -+ git -C pm-local branch --set-upstream-to=trunk one &&
> ++ git -C pm-local branch --set-upstream-to=mainline one &&
> + git -C pm-local merge --ff-only one-commit &&
> +
> -+ git -C pm-local branch --prune-merged trunk &&
> ++ git -C pm-local branch --delete-merged mainline &&
> +
> + 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_expect_success '--delete-merged silently skips 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 &&
> @@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
> + 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 branch --delete-merged "origin/*" 2>err &&
> ++ test_grep ! "not fully merged" err &&
> + git -C pm-unmerged rev-parse --verify refs/heads/wip
> +'
> +
> -+test_expect_success '--prune-merged is silent about not-merged-to-HEAD' '
> ++test_expect_success '--delete-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 &&
> @@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
> + 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 &&
> ++ git -C pm-nohead branch --delete-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_expect_success '--delete-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 &&
> @@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
> + 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 branch --delete-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_expect_success '--delete-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 &&
> @@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
> + 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 branch --delete-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_expect_success '--delete-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 branch --delete-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_expect_success '--delete-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 &&
> @@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
> + 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 branch --delete-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_expect_success '--delete-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 &&
> @@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
> + 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/*" &&
> ++ git -C pm-push-diff branch --delete-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_expect_success '--delete-merged requires at least one <branch>' '
> ++ test_must_fail git -C forked branch --delete-merged 2>err &&
> + test_grep "requires at least one <branch>" err
> +'
> +
> -+test_expect_success '--prune-merged takes positional <branch> arguments' '
> ++test_expect_success '--delete-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 &&
> @@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
> + 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 &&
> ++ git -C pm-positional branch --delete-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
> 5: d691d5051b ! 6: 72aaca0666 branch: add branch.<name>.pruneMerged opt-out
> @@ Metadata
> Author: Harald Nordgren <haraldnordgren@gmail.com>
>
> ## Commit message ##
> - branch: add branch.<name>.pruneMerged opt-out
> + branch: add branch.<name>.deleteMerged opt-out
>
> - Setting branch.<name>.pruneMerged=false exempts that branch from
> - "git branch --prune-merged", which is useful for a topic you want
> + Setting branch.<name>.deleteMerged=false exempts that branch from
> + "git branch --delete-merged", which is useful for a topic you want
> to keep developing after an early round of it has been merged
> upstream. Unless --quiet is given, each skip is reported so the
> user knows why their topic was kept.
> @@ Documentation/config/branch.adoc: for details).
> automatically added to the `format-patch` cover letter or
> `request-pull` summary.
> +
> -+`branch.<name>.pruneMerged`::
> ++`branch.<name>.deleteMerged`::
> + If set to `false`, branch _<name>_ is exempt from
> -+ `git branch --prune-merged`. Useful for a topic branch you
> ++ `git branch --delete-merged`. Useful for a topic branch you
> + intend to develop further after an initial round has been
> + merged upstream. Defaults to true. Explicit deletion via
> + `git branch -d` is unaffected.
>
> ## Documentation/git-branch.adoc ##
> -@@ Documentation/git-branch.adoc: the upstream refs refreshed.
> +@@ Documentation/git-branch.adoc: A branch is not deleted when:
> +
> - A branch is left alone if any of the following holds:
> - its upstream no longer resolves locally; it is checked out in any
> --worktree; or its push destination (`<branch>@{push}`) equals its
> -+worktree; 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".
> -+from a freshly pulled trunk that just looks "fully merged"; or
> -+`branch.<name>.pruneMerged` is set to `false`.
> + --
> + * its upstream remote-tracking branch no longer exists,
> +-* it is checked out in any worktree, or
> ++* it is checked out in any worktree,
> + * its push destination (`<branch>@{push}`) equals its upstream
> + (`<branch>@{upstream}`), so it cannot be distinguished from a
> +- branch that just looks "fully merged" right after a pull.
> ++ branch that just looks "fully merged" right after a pull, or
> ++* `branch.<name>.deleteMerged` is set to `false`.
> + --
> +
> - Branches refused by the "fully merged" safety check are listed as
> - warnings and skipped; pass them to `git branch -D` explicitly if
> + A branch whose work has not yet been merged into its upstream is
>
> ## builtin/branch.c ##
> -@@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv,
> +@@ builtin/branch.c: static int delete_merged_branches(int argc, const char **argv,
> const char *short_name;
> struct branch *branch;
> const char *upstream, *push;
> @@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv,
> + int opt_out;
>
> if (!skip_prefix(full_name, "refs/heads/", &short_name))
> - continue;
> -@@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv,
> + BUG("filter returned non-branch ref '%s'", full_name);
> +@@ builtin/branch.c: static int delete_merged_branches(int argc, const char **argv,
> if (!push || !strcmp(push, upstream))
> continue;
>
> -+ strbuf_addf(&key, "branch.%s.prunemerged", short_name);
> ++ strbuf_addf(&key, "branch.%s.deletemerged", short_name);
> + if (!repo_config_get_bool(the_repository, key.buf, &opt_out) &&
> + !opt_out) {
> + if (!quiet)
> + fprintf(stderr,
> -+ _("Skipping '%s' (branch.%s.pruneMerged is false)\n"),
> ++ _("Skipping '%s' (branch.%s.deleteMerged is false)\n"),
> + short_name, short_name);
> + strbuf_release(&key);
> + continue;
> @@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv,
>
>
> ## t/t3200-branch.sh ##
> -@@ t/t3200-branch.sh: test_expect_success '--prune-merged takes positional <branch> arguments' '
> +@@ t/t3200-branch.sh: test_expect_success '--delete-merged takes positional <branch> arguments' '
> test_must_fail git -C pm-positional rev-parse --verify refs/heads/two
> '
>
> -+test_expect_success '--prune-merged honours branch.<name>.pruneMerged=false' '
> ++test_expect_success '--delete-merged honours branch.<name>.deleteMerged=false' '
> + test_when_finished "rm -rf pm-optout" &&
> + git clone pm-upstream pm-optout &&
> + git -C pm-optout remote add fork ../pm-fork &&
> @@ t/t3200-branch.sh: test_expect_success '--prune-merged takes positional <branch>
> + git -C pm-optout branch --set-upstream-to=origin/next one &&
> + git -C pm-optout branch two two-commit &&
> + git -C pm-optout branch --set-upstream-to=origin/next two &&
> -+ test_config -C pm-optout branch.one.pruneMerged false &&
> ++ test_config -C pm-optout branch.one.deleteMerged false &&
> +
> -+ git -C pm-optout branch --prune-merged "origin/*" 2>err &&
> ++ git -C pm-optout branch --delete-merged "origin/*" 2>err &&
> +
> + git -C pm-optout rev-parse --verify refs/heads/one &&
> + test_must_fail git -C pm-optout rev-parse --verify refs/heads/two &&
> + test_grep "Skipping .one." err
> +'
> +
> -+test_expect_success 'branch -d still deletes a pruneMerged=false branch' '
> ++test_expect_success 'branch -d still deletes a deleteMerged=false branch' '
> + test_when_finished "rm -rf pm-optout-d" &&
> + git clone pm-upstream pm-optout-d &&
> + git -C pm-optout-d branch one one-commit &&
> + git -C pm-optout-d branch --set-upstream-to=origin/next one &&
> -+ test_config -C pm-optout-d branch.one.pruneMerged false &&
> ++ test_config -C pm-optout-d branch.one.deleteMerged false &&
> +
> + git -C pm-optout-d branch -d one &&
> + test_must_fail git -C pm-optout-d rev-parse --verify refs/heads/one
> 6: ede8c61729 ! 7: 7b2b01b988 branch: add --dry-run for --prune-merged
> @@ Metadata
> Author: Harald Nordgren <haraldnordgren@gmail.com>
>
> ## Commit message ##
> - branch: add --dry-run for --prune-merged
> + branch: add --dry-run for --delete-merged
>
> - With --dry-run, --prune-merged prints the local branches it would
> + With --dry-run, --delete-merged prints the local branches it would
> delete, one "Would delete branch <name>" line each, and exits
> without touching any ref. The same filtering applies, so the output
> is exactly the set that the real run would delete.
>
> - --dry-run is only meaningful together with --prune-merged and is
> + --dry-run is only meaningful together with --delete-merged and is
> rejected otherwise.
>
> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
> @@ Documentation/git-branch.adoc: 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>...
> -+git branch [--dry-run] --prune-merged <branch>...
> +-git branch --delete-merged <branch>...
> ++git branch [--dry-run] --delete-merged <branch>...
>
> DESCRIPTION
> -----------
> -@@ Documentation/git-branch.adoc: 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.
> +@@ Documentation/git-branch.adoc: A branch whose work has not yet been merged into its upstream is
> + silently skipped. Delete it with `git branch -D` if you want to
> + remove it anyway.
>
> +`--dry-run`::
> -+ With `--prune-merged`, print which branches would be
> ++ With `--delete-merged`, print which branches would be
> + deleted and exit without touching any ref. Useful for
> + sanity-checking a wide pattern like `'origin/*'` before
> + committing to the deletion.
> @@ builtin/branch.c
> @@ builtin/branch.c: static int parse_opt_forked(const struct option *opt, const char *arg, int unset
> }
>
> - static int prune_merged_branches(int argc, const char **argv,
> + static int delete_merged_branches(int argc, const char **argv,
> - int quiet)
> + int quiet, int dry_run)
> {
> struct ref_store *refs = get_main_ref_store(the_repository);
> struct ref_filter filter = REF_FILTER_INIT;
> -@@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv,
> +@@ builtin/branch.c: static int delete_merged_branches(int argc, const char **argv,
> FILTER_REFS_BRANCHES,
> - DELETE_BRANCH_WARN_ONLY |
> + DELETE_BRANCH_SKIP_UNMERGED |
> DELETE_BRANCH_NO_HEAD_FALLBACK |
> - (quiet ? DELETE_BRANCH_QUIET : 0));
> + (quiet ? DELETE_BRANCH_QUIET : 0) |
> @@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv,
> @@ builtin/branch.c: int cmd_branch(int argc,
> int delete = 0, rename = 0, copy = 0, list = 0,
> unset_upstream = 0, show_current = 0, edit_description = 0;
> - int prune_merged = 0;
> + int delete_merged = 0;
> + int dry_run = 0;
> const char *new_upstream = NULL;
> int noncreate_actions = 0;
> /* possible options */
> @@ builtin/branch.c: int cmd_branch(int argc,
> N_("edit the description for the branch")),
> - OPT_BOOL(0, "prune-merged", &prune_merged,
> - N_("delete local branches whose upstream matches <branch> and is merged")),
> + OPT_BOOL(0, "delete-merged", &delete_merged,
> + N_("delete local branches whose upstream matches <branch> and are merged")),
> + OPT_BOOL(0, "dry-run", &dry_run,
> -+ N_("with --prune-merged, only print which branches would be deleted")),
> ++ N_("with --delete-merged, only print which branches would be deleted")),
> OPT__FORCE(&force, N_("force creation, move/rename, deletion"), PARSE_OPT_NOCOMPLETE),
> OPT_MERGED(&filter, N_("print only branches that are merged")),
> OPT_NO_MERGED(&filter, N_("print only branches that are not merged")),
> @@ builtin/branch.c: int cmd_branch(int argc,
> if (noncreate_actions > 1)
> usage_with_options(builtin_branch_usage, options);
>
> -+ if (dry_run && !prune_merged)
> -+ die(_("--dry-run requires --prune-merged"));
> ++ if (dry_run && !delete_merged)
> ++ die(_("--dry-run requires --delete-merged"));
> +
> if (recurse_submodules_explicit) {
> if (!submodule_propagate_branches)
> @@ builtin/branch.c: int cmd_branch(int argc,
> @@ builtin/branch.c: int cmd_branch(int argc,
> (quiet ? DELETE_BRANCH_QUIET : 0));
> goto out;
> - } else if (prune_merged) {
> -- ret = prune_merged_branches(argc, argv, quiet);
> -+ ret = prune_merged_branches(argc, argv, quiet, dry_run);
> + } else if (delete_merged) {
> +- ret = delete_merged_branches(argc, argv, quiet);
> ++ ret = delete_merged_branches(argc, argv, quiet, dry_run);
> goto out;
> } else if (show_current) {
> print_current_branch_name();
>
> ## t/t3200-branch.sh ##
> -@@ t/t3200-branch.sh: test_expect_success 'branch -d still deletes a pruneMerged=false branch' '
> +@@ t/t3200-branch.sh: test_expect_success 'branch -d still deletes a deleteMerged=false branch' '
> test_must_fail git -C pm-optout-d rev-parse --verify refs/heads/one
> '
>
> -+test_expect_success '--prune-merged --dry-run lists but does not delete' '
> ++test_expect_success '--delete-merged --dry-run lists but does not delete' '
> + test_when_finished "rm -rf pm-dry" &&
> + git clone pm-upstream pm-dry &&
> + git -C pm-dry remote add fork ../pm-fork &&
> @@ t/t3200-branch.sh: test_expect_success 'branch -d still deletes a pruneMerged=fa
> + git -C pm-dry branch two two-commit &&
> + git -C pm-dry branch --set-upstream-to=origin/next two &&
> +
> -+ git -C pm-dry branch --dry-run --prune-merged "origin/*" >actual &&
> ++ git -C pm-dry branch --dry-run --delete-merged "origin/*" >actual &&
> + test_grep "Would delete branch one " actual &&
> + test_grep "Would delete branch two " actual &&
> +
> @@ t/t3200-branch.sh: test_expect_success 'branch -d still deletes a pruneMerged=fa
> + git -C pm-dry rev-parse --verify refs/heads/two
> +'
> +
> -+test_expect_success '--prune-merged --dry-run only lists branches the live run would delete' '
> ++test_expect_success '--delete-merged --dry-run only lists branches the live run would delete' '
> + test_when_finished "rm -rf pm-dry-mixed" &&
> + git clone pm-upstream pm-dry-mixed &&
> + git -C pm-dry-mixed remote add fork ../pm-fork &&
> @@ t/t3200-branch.sh: test_expect_success 'branch -d still deletes a pruneMerged=fa
> + git -C pm-dry-mixed branch merged one-commit &&
> + git -C pm-dry-mixed branch --set-upstream-to=origin/next merged &&
> +
> -+ git -C pm-dry-mixed branch --dry-run --prune-merged "origin/*" >out &&
> ++ git -C pm-dry-mixed branch --dry-run --delete-merged "origin/*" >out &&
> + test_grep "Would delete branch merged" out &&
> + test_grep ! "Would delete branch wip" out &&
> + git -C pm-dry-mixed rev-parse --verify refs/heads/wip &&
> + git -C pm-dry-mixed rev-parse --verify refs/heads/merged
> +'
> +
> -+test_expect_success '--dry-run without --prune-merged is rejected' '
> ++test_expect_success '--dry-run without --delete-merged is rejected' '
> + test_must_fail git -C forked branch --dry-run 2>err &&
> -+ test_grep "requires --prune-merged" err
> ++ test_grep "requires --delete-merged" err
> +'
> +
> test_done
>
^ permalink raw reply
* Re: [PATCH] rebase: mention --abort alongside --continue
From: Phillip Wood @ 2026-06-17 9:52 UTC (permalink / raw)
To: Junio C Hamano, Phillip Wood
Cc: Harald Nordgren via GitGitGadget, git, Harald Nordgren
In-Reply-To: <xmqqpl1q2xw5.fsf@gitster.g>
On 16/06/2026 18:33, Junio C Hamano wrote:
> Phillip Wood <phillip.wood123@gmail.com> writes:
>
>> Hi Harald
>>
>> On 15/06/2026 20:19, Harald Nordgren via GitGitGadget wrote:
>>> From: Harald Nordgren <haraldnordgren@gmail.com>
>>>
>>> The warning shown when an "exec" step fails and the "git status"
>>> advice while splitting or editing a commit pointed users at "git
>>> rebase --continue" but not "--abort". Mention it in both, matching
>>> the conflict case.
>>
>> I'm not sure that the "failed exec" and "conflicts" cases are equivalent
>> though. If you have some nasty conflict that you don't want to resolve
>> then aborting and trying another approach such is incrementally rebasing
>> is the only option. If an exec command fails then it likely means that a
>> test has failed or some something similar which is minor inconvenience
>> which needs fixing before continuing - it seems very unlikely that the
>> user would want to abort the rebase.
>
> It is very true that users who know what they are doing and got into
> such conflicts are opted to go into such a situation tnat it is
> unlikely that they would appreciate a choice to abort.
That's not quite what I was trying to say which was that aborting in the
case of conflicts is more likely than in the case of a failed exec.
> But given that for any system, everybody starts as a newbie, it may
> be assuring to always give "here is a way out" option when they get
> in a nasty confusing situation. Discouraging the way to use the
> tool that can lead to confusing situation by guiding them with BCP
> workflows would help, but they always get into pitfall.
>
> The patch adds new message into the existing message to suggest how
> to move forward, but as a training wheel option, it may not be a bad
> thing to offer "--abort" as an extra hint, separate from the
> existing warning() message.
So if I've understood we'd print a message explaining what's happened
and how to continue followed by a hint about aborting. The message would
depend on what problem caused the rebase to stop, but the hint would be
the same in each case. That sounds fine to me.
Thanks
Phillip
^ permalink raw reply
* Re: [PATCH v2 0/2] rebase: add --squash to fold a range into its first commit
From: Phillip Wood @ 2026-06-17 9:48 UTC (permalink / raw)
To: Harald Nordgren, Phillip Wood
Cc: Harald Nordgren via GitGitGadget, git, D. Ben Knoble,
Patrick Steinhardt, Junio C Hamano
In-Reply-To: <CAHwyqnV_pt1fEhUGPyGtXrJAwhjpQHOyX9juHRv_88T2md554Q@mail.gmail.com>
On 17/06/2026 10:11, Harald Nordgren wrote:
> On Tue, Jun 16, 2026 at 12:10 PM Phillip Wood <phillip.wood123@gmail.com> wrote:
>> On 15/06/2026 09:37, Harald Nordgren via GitGitGadget wrote:
>>> Rename to rebase --squash.
>>
>> Please include the original cover letter as well so people who have not
>> read the previous version know what the series is about.
>
> So you mean this one, should that be included in each version, and
> append each subsequent one:
>
> ```
> Adds `git rebase --autosquash --fixup [<upstream>]` to fold a range of
> commits into its oldest one, reusing that commit's message.
Yes, see
https://lore.kernel.org/20260615-b4-pks-history-drop-v6-0-2e329e536d78@pks.im
for an example and the "Cover Letter" section of
Documentation/SubmittingPatches
Thanks
Phillip
>
> Related idea: https://github.com/gitgitgadget/git/issues/1135
> ```
>
> Or make each message a full cover letter instead of just a diff?
>
>
>
>
> Harald
>
^ permalink raw reply
* Re: [PATCH 0/2] rebase: add --fixup to fold a range into its oldest commit
From: Harald Nordgren @ 2026-06-17 9:30 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: D. Ben Knoble, Junio C Hamano, Harald Nordgren via GitGitGadget,
git
In-Reply-To: <ajEKf-jCIDVPQCeO@pks.im>
> Yes, it does fit into git-history(1), and I do indeed already have plans
> to implement such a command going forward. I wouldn't mind at all though
> if somebody else beat me to it, I want to implement at least one more
> command before I get to this.
What I like about 'git rebase --squash' is that when upstream is set
up, it understands the commit range automatically, whereas history
feels more removed from the current upstream. Maybe I'm wrong about
that.
Harald
^ permalink raw reply
* Re: [PATCH v2 0/2] rebase: add --squash to fold a range into its first commit
From: Harald Nordgren @ 2026-06-17 9:11 UTC (permalink / raw)
To: Phillip Wood
Cc: Harald Nordgren via GitGitGadget, git, D. Ben Knoble,
Patrick Steinhardt, Junio C Hamano
In-Reply-To: <d55b6600-50f3-4e81-a6bf-d270cd7abd2d@gmail.com>
On Tue, Jun 16, 2026 at 12:10 PM Phillip Wood <phillip.wood123@gmail.com> wrote:
>
> Hi Harald
>
> On 15/06/2026 09:37, Harald Nordgren via GitGitGadget wrote:
> > Rename to rebase --squash.
>
> Please include the original cover letter as well so people who have not
> read the previous version know what the series is about.
So you mean this one, should that be included in each version, and
append each subsequent one:
```
Adds `git rebase --autosquash --fixup [<upstream>]` to fold a range of
commits into its oldest one, reusing that commit's message.
Related idea: https://github.com/gitgitgadget/git/issues/1135
```
Or make each message a full cover letter instead of just a diff?
Harald
^ permalink raw reply
* Re: [PATCH] rebase: mention --abort alongside --continue
From: Harald Nordgren @ 2026-06-17 8:56 UTC (permalink / raw)
To: Junio C Hamano; +Cc: Phillip Wood, Harald Nordgren via GitGitGadget, git
In-Reply-To: <xmqqpl1q2xw5.fsf@gitster.g>
For the record, I have gotten into ill-fated rebases, and then it
would have been nice to have that message there. Sometimes you realize
after starting that you chose the wrong upstream, etc.
Harald
^ 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