* [PATCH] SubmittingPatches: address design critiques
From: Junio C Hamano @ 2026-06-17 16:06 UTC (permalink / raw)
To: git
Contributors sometimes fail to answer fundamental design or
viability comments from reviewers and submit subsequent rounds
without addressing them. When design decisions are resolved on the
mailing list, the final justification should be recorded in the
commit messages.
Instruct authors to be particularly mindful of critiques regarding
high-level design or viability, to defend their choices on the list,
and to accompany new iterations with clearer explanations in the cover
letter, responses, and revised commit messages. Also instruct them to
explicitly document the resolution of these concerns in the commit
message body to keep the historical record complete.
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
Documentation/SubmittingPatches | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/Documentation/SubmittingPatches b/Documentation/SubmittingPatches
index 176567738d..bfe3745a54 100644
--- a/Documentation/SubmittingPatches
+++ b/Documentation/SubmittingPatches
@@ -51,6 +51,21 @@ area.
respond to them with "Reply-All" on the mailing list, while taking
them into account while preparing an updated set of patches.
+
+You would want to be particularly mindful of critiques regarding the
+high-level design or viability of your proposal (e.g., questioning
+whether the feature is worth implementing, or if the chosen approach
+is appropriate). You want to defend your design decisions on the list
+first, because you do not want to spend too much effort in the
+implementation if the design is not yet solid.
++
+Also, make sure that any new version is accompanied by a much clearer
+explanation and justification (in the cover letter, your responses,
+and in the revised commit messages). Aim to make the reviewers say
+"it is now clear why we may want to do this with the updated version".
++
+Topics that fail to address fundamental design critiques without
+resolution will not be considered ready for merging.
++
It is often beneficial to allow some time for reviewers to provide
feedback before sending a new version, rather than sending an updated
series immediately after receiving a review. This helps collect broader
@@ -322,6 +337,10 @@ The body should provide a meaningful commit message, which:
. alternate solutions considered but discarded, if any.
+. the resolution of design or viability concerns raised by the
+ community during the review, if any, ensuring the historical record
+ explains why the chosen approach was accepted over alternatives.
+
[[present-tense]]
The problem statement that describes the status quo is written in the
present tense. Write "The code does X when it is given input Y",
--
2.55.0-rc1-92-ge545aa9d3e
^ permalink raw reply related
* [PATCH 2/2] config: use repo_get_ignore_case() to access core.ignorecase
From: Tian Yuchen @ 2026-06-17 15:49 UTC (permalink / raw)
To: git
Cc: ps, phillip.wood123, johannes.schindelin, stolee, Tian Yuchen,
Christian Couder, Ayush Chandekar, Olamide Caleb Bello
In-Reply-To: <20260617154929.564498-1-cat@malon.dev>
Replace the accesses to the global 'ignore_case' variable with
calls to 'repo_get_ignore_case(the_repository)'. This step eliminates
the 'ignore_case' global state.
Note on compat/win32/path-utils.c:
To eliminate the global state, several helper functions
(e.g. 'win32_fspathncmp()') now read from
'repo_get_ignore_case(the_repository)'. While this introduces
dependency on 'repository.h' into the 'compat/', it avoids massive
refactoring of the signatures across the codebase.
Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Ayush Chandekar <ayu.chandekar@gmail.com>
Mentored-by: Olamide Caleb Bello <belkid98@gmail.com>
Signed-off-by: Tian Yuchen <cat@malon.dev>
---
apply.c | 2 +-
builtin/fetch.c | 2 +-
builtin/mv.c | 2 +-
compat/win32/path-utils.c | 3 ++-
dir.c | 18 +++++++++---------
environment.c | 3 +--
environment.h | 1 -
fsmonitor.c | 2 +-
name-hash.c | 6 +++---
read-cache.c | 6 +++---
refs/files-backend.c | 4 ++--
submodule.c | 2 +-
t/helper/test-lazy-init-name-hash.c | 2 +-
unpack-trees.c | 2 +-
14 files changed, 27 insertions(+), 28 deletions(-)
diff --git a/apply.c b/apply.c
index 249248d4f2..53309b9a09 100644
--- a/apply.c
+++ b/apply.c
@@ -4008,7 +4008,7 @@ static int path_is_beyond_symlink_1(struct apply_state *state, struct strbuf *na
struct cache_entry *ce;
ce = index_file_exists(state->repo->index, name->buf,
- name->len, ignore_case);
+ name->len, repo_get_ignore_case(the_repository));
if (ce && S_ISLNK(ce->ce_mode))
return 1;
} else {
diff --git a/builtin/fetch.c b/builtin/fetch.c
index e4e8a72ed9..67c7df0f3c 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1819,7 +1819,7 @@ static void ref_transaction_rejection_handler(const char *refname,
{
struct ref_rejection_data *data = cb_data;
- if (err == REF_TRANSACTION_ERROR_CASE_CONFLICT && ignore_case &&
+ if (err == REF_TRANSACTION_ERROR_CASE_CONFLICT && repo_get_ignore_case(the_repository) &&
!data->case_sensitive_msg_shown) {
error(_("You're on a case-insensitive filesystem, and the remote you are\n"
"trying to fetch from has references that only differ in casing. It\n"
diff --git a/builtin/mv.c b/builtin/mv.c
index 948b330639..0f6f060004 100644
--- a/builtin/mv.c
+++ b/builtin/mv.c
@@ -419,7 +419,7 @@ int cmd_mv(int argc,
goto act_on_entry;
}
if (lstat(dst, &st) == 0 &&
- (!ignore_case || strcasecmp(src, dst))) {
+ (!repo_get_ignore_case(the_repository) || strcasecmp(src, dst))) {
bad = _("destination exists");
if (force) {
/*
diff --git a/compat/win32/path-utils.c b/compat/win32/path-utils.c
index 966ef779b9..4edb033e20 100644
--- a/compat/win32/path-utils.c
+++ b/compat/win32/path-utils.c
@@ -2,6 +2,7 @@
#include "../../git-compat-util.h"
#include "../../environment.h"
+#include "../../repository.h"
int win32_has_dos_drive_prefix(const char *path)
{
@@ -75,7 +76,7 @@ int win32_fspathncmp(const char *a, const char *b, size_t count)
} else if (is_dir_sep(*b))
return +1;
- diff = ignore_case ?
+ diff = repo_get_ignore_case(the_repository) ?
(unsigned char)tolower(*a) - (int)(unsigned char)tolower(*b) :
(unsigned char)*a - (int)(unsigned char)*b;
if (diff)
diff --git a/dir.c b/dir.c
index 33c81c256e..7116d65cad 100644
--- a/dir.c
+++ b/dir.c
@@ -126,7 +126,7 @@ int count_slashes(const char *s)
int git_fspathcmp(const char *a, const char *b)
{
- return ignore_case ? strcasecmp(a, b) : strcmp(a, b);
+ return repo_get_ignore_case(the_repository) ? strcasecmp(a, b) : strcmp(a, b);
}
int fspatheq(const char *a, const char *b)
@@ -136,7 +136,7 @@ int fspatheq(const char *a, const char *b)
int git_fspathncmp(const char *a, const char *b, size_t count)
{
- return ignore_case ? strncasecmp(a, b, count) : strncmp(a, b, count);
+ return repo_get_ignore_case(the_repository) ? strncasecmp(a, b, count) : strncmp(a, b, count);
}
int paths_collide(const char *a, const char *b)
@@ -153,7 +153,7 @@ int paths_collide(const char *a, const char *b)
unsigned int fspathhash(const char *str)
{
- return ignore_case ? strihash(str) : strhash(str);
+ return repo_get_ignore_case(the_repository) ? strihash(str) : strhash(str);
}
int git_fnmatch(const struct pathspec_item *item,
@@ -202,7 +202,7 @@ static int fnmatch_icase_mem(const char *pattern, int patternlen,
use_str = str_buf.buf;
}
- if (ignore_case)
+ if (repo_get_ignore_case(the_repository))
flags |= WM_CASEFOLD;
match_status = wildmatch(use_pat, use_str, flags);
@@ -1851,7 +1851,7 @@ static struct dir_entry *dir_add_name(struct dir_struct *dir,
struct index_state *istate,
const char *pathname, int len)
{
- if (index_file_exists(istate, pathname, len, ignore_case))
+ if (index_file_exists(istate, pathname, len, repo_get_ignore_case(the_repository)))
return NULL;
ALLOC_GROW(dir->entries, dir->nr+1, dir->internal.alloc);
@@ -1888,7 +1888,7 @@ static enum exist_status directory_exists_in_index_icase(struct index_state *ist
if (index_dir_exists(istate, dirname, len))
return index_directory;
- ce = index_file_exists(istate, dirname, len, ignore_case);
+ ce = index_file_exists(istate, dirname, len, repo_get_ignore_case(the_repository));
if (ce && S_ISGITLINK(ce->ce_mode))
return index_gitdir;
@@ -1907,7 +1907,7 @@ static enum exist_status directory_exists_in_index(struct index_state *istate,
{
int pos;
- if (ignore_case)
+ if (repo_get_ignore_case(the_repository))
return directory_exists_in_index_icase(istate, dirname, len);
pos = index_name_pos(istate, dirname, len);
@@ -2447,7 +2447,7 @@ static enum path_treatment treat_path(struct dir_struct *dir,
/* Always exclude indexed files */
has_path_in_index = !!index_file_exists(istate, path->buf, path->len,
- ignore_case);
+ repo_get_ignore_case(the_repository));
if (dtype != DT_DIR && has_path_in_index)
return path_none;
@@ -3201,7 +3201,7 @@ static int cmp_icase(char a, char b)
{
if (a == b)
return 0;
- if (ignore_case)
+ if (repo_get_ignore_case(the_repository))
return toupper(a) - toupper(b);
return a - b;
}
diff --git a/environment.c b/environment.c
index c568d3b6fb..1f548b357c 100644
--- a/environment.c
+++ b/environment.c
@@ -46,7 +46,6 @@ int trust_ctime = 1;
int check_stat = 1;
int has_symlinks = 1;
int minimum_abbrev = 4, default_abbrev = -1;
-int ignore_case;
int assume_unchanged;
int is_bare_repository_cfg = -1; /* unspecified */
int warn_on_object_refname_ambiguity = 1;
@@ -342,7 +341,7 @@ int git_default_core_config(const char *var, const char *value,
}
if (!strcmp(var, "core.ignorecase")) {
- ignore_case = git_config_bool(var, value);
+ cfg->ignore_case = git_config_bool(var, value);
return 0;
}
diff --git a/environment.h b/environment.h
index 9e3d94fb80..66fdb1ed20 100644
--- a/environment.h
+++ b/environment.h
@@ -171,7 +171,6 @@ extern int trust_ctime;
extern int check_stat;
extern int has_symlinks;
extern int minimum_abbrev, default_abbrev;
-extern int ignore_case;
extern int assume_unchanged;
extern int warn_on_object_refname_ambiguity;
extern char *apply_default_whitespace;
diff --git a/fsmonitor.c b/fsmonitor.c
index d07dc18967..5376e1987a 100644
--- a/fsmonitor.c
+++ b/fsmonitor.c
@@ -453,7 +453,7 @@ static void fsmonitor_refresh_callback(struct index_state *istate, char *name)
* case-insensitive file system, try again using the name-hash
* and dir-name-hash.
*/
- if (!nr_in_cone && ignore_case) {
+ if (!nr_in_cone && repo_get_ignore_case(the_repository)) {
nr_in_cone = handle_using_name_hash_icase(istate, name);
if (!nr_in_cone)
nr_in_cone = handle_using_dir_name_hash_icase(
diff --git a/name-hash.c b/name-hash.c
index b91e276267..6bb2ecdd05 100644
--- a/name-hash.c
+++ b/name-hash.c
@@ -126,7 +126,7 @@ static void hash_index_entry(struct index_state *istate, struct cache_entry *ce)
hashmap_add(&istate->name_hash, &ce->ent);
}
- if (ignore_case)
+ if (repo_get_ignore_case(the_repository))
add_dir_entry(istate, ce);
}
@@ -207,7 +207,7 @@ static int lookup_lazy_params(struct index_state *istate)
* code to build the "istate->name_hash". We don't
* need the complexity here.
*/
- if (!ignore_case)
+ if (!repo_get_ignore_case(the_repository))
return 0;
nr_cpus = online_cpus();
@@ -651,7 +651,7 @@ void remove_name_hash(struct index_state *istate, struct cache_entry *ce)
ce->ce_flags &= ~CE_HASHED;
hashmap_remove(&istate->name_hash, &ce->ent, ce);
- if (ignore_case)
+ if (repo_get_ignore_case(the_repository))
remove_dir_entry(istate, ce);
}
diff --git a/read-cache.c b/read-cache.c
index 21829102ae..1409ac00b4 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -760,12 +760,12 @@ int add_to_index(struct index_state *istate, const char *path, struct stat *st,
* case of the file being added to the repository matches (is folded into) the existing
* entry's directory case.
*/
- if (ignore_case) {
+ if (repo_get_ignore_case(the_repository)) {
adjust_dirname_case(istate, ce->name);
}
if (!(flags & ADD_CACHE_RENORMALIZE)) {
alias = index_file_exists(istate, ce->name,
- ce_namelen(ce), ignore_case);
+ ce_namelen(ce), repo_get_ignore_case(the_repository));
if (alias &&
!ce_stage(alias) &&
!ie_match_stat(istate, alias, st, ce_option)) {
@@ -786,7 +786,7 @@ int add_to_index(struct index_state *istate, const char *path, struct stat *st,
} else
set_object_name_for_intent_to_add_entry(ce);
- if (ignore_case && alias && different_name(ce, alias))
+ if (repo_get_ignore_case(the_repository) && alias && different_name(ce, alias))
ce = create_alias_ce(istate, ce, alias);
ce->ce_flags |= CE_ADDED;
diff --git a/refs/files-backend.c b/refs/files-backend.c
index a4c7858787..6d89d9817a 100644
--- a/refs/files-backend.c
+++ b/refs/files-backend.c
@@ -806,7 +806,7 @@ static enum ref_transaction_error lock_raw_ref(struct files_ref_store *refs,
} else {
unable_to_lock_message(ref_file.buf, myerr, err);
if (myerr == EEXIST) {
- if (ignore_case &&
+ if (repo_get_ignore_case(the_repository) &&
transaction_has_case_conflicting_update(transaction, update)) {
/*
* In case-insensitive filesystems, ensure that conflicts within a
@@ -920,7 +920,7 @@ static enum ref_transaction_error lock_raw_ref(struct files_ref_store *refs,
* conflicts between 'foo' and 'Foo/bar'. So let's lowercase
* the refname.
*/
- if (ignore_case) {
+ if (repo_get_ignore_case(the_repository)) {
struct strbuf lower = STRBUF_INIT;
strbuf_addstr(&lower, refname);
diff --git a/submodule.c b/submodule.c
index a939ff5072..32af85d967 100644
--- a/submodule.c
+++ b/submodule.c
@@ -2389,7 +2389,7 @@ static int validate_submodule_encoded_git_dir(char *git_dir, const char *submodu
/* Prevent conflicts on case-folding filesystems */
repo_config_get_bool(the_repository, "core.ignorecase", &config_ignorecase);
- if (ignore_case || config_ignorecase) {
+ if (repo_get_ignore_case(the_repository) || config_ignorecase) {
bool suffixes_match = !strcmp(last_submodule_name, submodule_name);
return check_casefolding_conflict(git_dir, submodule_name,
suffixes_match);
diff --git a/t/helper/test-lazy-init-name-hash.c b/t/helper/test-lazy-init-name-hash.c
index e542985c94..43cead6d7d 100644
--- a/t/helper/test-lazy-init-name-hash.c
+++ b/t/helper/test-lazy-init-name-hash.c
@@ -218,7 +218,7 @@ int cmd__lazy_init_name_hash(int argc, const char **argv)
/*
* istate->dir_hash is only created when ignore_case is set.
*/
- ignore_case = 1;
+ repo_config_values(the_repository)->ignore_case = 1;
if (dump) {
if (perf || analyze > 0)
diff --git a/unpack-trees.c b/unpack-trees.c
index 998a1e6dc7..330c5c0172 100644
--- a/unpack-trees.c
+++ b/unpack-trees.c
@@ -2428,7 +2428,7 @@ static int check_ok_to_remove(const char *name, int len, int dtype,
*
* Ignore that lstat() if it matches.
*/
- if (ignore_case && icase_exists(o, name, len, st))
+ if (repo_get_ignore_case(the_repository) && icase_exists(o, name, len, st))
return 0;
if (o->internal.dir &&
--
2.43.0
^ permalink raw reply related
* [PATCH 1/2] environment: move ignore_case into repo_config_values
From: Tian Yuchen @ 2026-06-17 15:49 UTC (permalink / raw)
To: git
Cc: ps, phillip.wood123, johannes.schindelin, stolee, Tian Yuchen,
Christian Couder, Ayush Chandekar, Olamide Caleb Bello
In-Reply-To: <20260617154929.564498-1-cat@malon.dev>
The 'core.ignorecase' configuration which is stored as the
global variable 'ignore_case' acts as a core filesystem
capability flag.
Move this global variable into 'struct repo_config_values' to tie it
to the specific repository instance it was read from. This reduces
global state and aligns with the ongoing libification effort.
Note that the newly introduced getter, 'repo_get_ignore_case()',
intentionally avoids checking 'repo->gitdir'. This could safely
accommodates early dynamic probing of the filesystem during
'git init' or clone operations, where the 'gitdir' might not be fully
initialized but the filesystem capability must be recorded.
Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Ayush Chandekar <ayu.chandekar@gmail.com>
Mentored-by: Olamide Caleb Bello <belkid98@gmail.com>
Signed-off-by: Tian Yuchen <cat@malon.dev>
---
environment.c | 8 ++++++++
environment.h | 8 ++++++++
2 files changed, 16 insertions(+)
diff --git a/environment.c b/environment.c
index fc3ed8bb1c..c568d3b6fb 100644
--- a/environment.c
+++ b/environment.c
@@ -142,6 +142,13 @@ int is_bare_repository(void)
return is_bare_repository_cfg && !repo_get_work_tree(the_repository);
}
+int repo_get_ignore_case(struct repository *repo)
+{
+ if (repo)
+ return repo_config_values(repo)->ignore_case;
+ return 0;
+}
+
int have_git_dir(void)
{
return startup_info->have_repository
@@ -720,5 +727,6 @@ void repo_config_values_init(struct repo_config_values *cfg)
{
cfg->attributes_file = NULL;
cfg->apply_sparse_checkout = 0;
+ cfg->ignore_case = 0;
cfg->branch_track = BRANCH_TRACK_REMOTE;
}
diff --git a/environment.h b/environment.h
index 9eb97b3869..9e3d94fb80 100644
--- a/environment.h
+++ b/environment.h
@@ -91,6 +91,7 @@ struct repo_config_values {
/* section "core" config values */
char *attributes_file;
int apply_sparse_checkout;
+ int ignore_case;
/* section "branch" config values */
enum branch_track branch_track;
@@ -123,6 +124,13 @@ int git_default_config(const char *, const char *,
int git_default_core_config(const char *var, const char *value,
const struct config_context *ctx, void *cb);
+/*
+ * Getter for the `ignore_case` field of `struct repo_config_values`.
+ * It intentionally avoids checking `repo->gitdir` to allow early dynamic
+ * probing during `git init` or clone.
+ */
+int repo_get_ignore_case(struct repository *repo);
+
void repo_config_values_init(struct repo_config_values *cfg);
/*
--
2.43.0
^ permalink raw reply related
* [PATCH 0/2] environment: move ignore_case into repo_config_values
From: Tian Yuchen @ 2026-06-17 15:49 UTC (permalink / raw)
To: git; +Cc: ps, phillip.wood123, johannes.schindelin, stolee, Tian Yuchen
The 'core.ignorecase' configuration, stored as the global variable
'ignore_case', acts as a core filesystem capability flag.
This series continues the ongoing libification effort by moving
this global variable into struct 'repo_config_values', tying it
to the specific repository instance it was read from. This allows
us to encapsulate the configuration without altering its
eager-parsing behavior.
The getter function 'repo_get_ignore_case()' is introduced so
that we can safely retrieve the configuration value whilst
maintaining the correct fallback logic.
RFC Questions:
environment.h --- Is the fallback logic for repo_get_ignore_case()
correct? I am unsure whether gitdir should be used here, since it
might not be ready when we access it in the early stage of
initialization (e.g. git init / git clone).
dir.c --- Performance overhead?
compat/win32/path-utils.c --- Is it appropriate to include the
repository.h header file?
Related materials:
[1] In this patch to migrate protect_hfs and protect_ntfs, the approach
of introducing getters has been endorsed.
[2] Derrick Stolee's previous attempt. The reasons for the failure are
also mentioned in [1].
Thanks!
Mentored-by: Christian Couder christian.couder@gmail.com
Mentored-by: Ayush Chandekar ayu.chandekar@gmail.com
Mentored-by: Olamide Caleb Bello belkid98@gmail.com
Signed-off-by: Tian Yuchen cat@malon.dev
[1] https://lore.kernel.org/git/20260606143412.15443-1-cat@malon.dev/
[2] https://lore.kernel.org/git/2b4198c09cb6c04c60608d19072d419503dfe5df.1685716421.git.gitgitgadget@gmail.com/
Tian Yuchen (2):
environment: move ignore_case into repo_config_values
config: use repo_get_ignore_case() to access core.ignorecase
apply.c | 2 +-
builtin/fetch.c | 2 +-
builtin/mv.c | 2 +-
compat/win32/path-utils.c | 3 ++-
dir.c | 18 +++++++++---------
environment.c | 11 +++++++++--
environment.h | 9 ++++++++-
fsmonitor.c | 2 +-
name-hash.c | 6 +++---
read-cache.c | 6 +++---
refs/files-backend.c | 4 ++--
submodule.c | 2 +-
t/helper/test-lazy-init-name-hash.c | 2 +-
unpack-trees.c | 2 +-
14 files changed, 43 insertions(+), 28 deletions(-)
--
2.43.0
^ permalink raw reply
* [PATCH] completion: zsh: support completion after "git -C <path>"
From: Lutz Lengemann via GitGitGadget @ 2026-06-17 15:30 UTC (permalink / raw)
To: git; +Cc: Lutz Lengemann, Lutz Lengemann
From: Lutz Lengemann <lutz@lengemann.net>
The zsh completion wrapper (__git_zsh_main) did not handle the global -C
option, so "git -C <path> <command> <TAB>" offered nothing and could not
complete a command's arguments.
Three things are needed to make it work, all scoped to -C:
- Add -C to the _arguments specification, so completion no longer stops
at it.
- Advance __git_cmd_idx past any leading "-C <path>" options. The index
is hard-coded to 1, i.e. the command is assumed to be the first
argument; with -C present the command sits two words later for each
-C, so the bash helpers otherwise look at the wrong word and produce
nothing.
- Collect the -C paths into __git_C_args, as __git_main does. The bash
helpers run git to resolve aliases and list refs; without the -C
paths they run in the current directory, so completion fails whenever
the cwd is not the target repository or the command is an alias.
With these, "git -C <path> <command> <TAB>" completes the command, its
options and its arguments, including outside the repository, through
aliases, and with repeated -C options.
Signed-off-by: Lutz Lengemann <lutz@lengemann.net>
---
completion: zsh: support completion after "git -C "
This patch is intentionally scoped to -C, but the underlying problem is
more general. The zsh wrapper hard-codes __git_cmd_idx=1, i.e. it
assumes the command is always the first argument. That assumption breaks
argument completion after any global option that precedes the command,
not just -C — e.g. --git-dir, --work-tree, --namespace, -c, and
-p/--paginate. After those, git <opt> <command> <TAB> currently
completes the command name but not its arguments.
The same approach generalizes cleanly: instead of skipping only leading
-C options, walk all leading global options and their arguments to
locate the command and its true index (mirroring the option scan in
__git_main in git-completion.bash), while collecting -C into
__git_C_args and --git-dir into __git_dir as today.
I kept this revision narrow for reviewability and because git -C is the
case where I miss the completion, but I'm happy to extend it to cover
the other global options in a follow-up (or fold it into this patch) if
that's preferred.
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2155%2Fmobilutz%2Fzsh-complete-global-C-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2155/mobilutz/zsh-complete-global-C-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/2155
contrib/completion/git-completion.zsh | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/contrib/completion/git-completion.zsh b/contrib/completion/git-completion.zsh
index c32186a977..323049be8b 100644
--- a/contrib/completion/git-completion.zsh
+++ b/contrib/completion/git-completion.zsh
@@ -227,6 +227,7 @@ __git_zsh_main ()
'(-p --paginate --no-pager)'{-p,--paginate}'[pipe all output into ''less'']' \
'(-p --paginate)--no-pager[do not pipe git output into a pager]' \
'--git-dir=-[set the path to the repository]: :_directories' \
+ '*-C[run as if git was started in <path>]: :_directories' \
'--bare[treat the repository as a bare repository]' \
'(- :)--version[prints the git suite version]' \
'--exec-path=-[path to where your core git programs are installed]:: :_directories' \
@@ -252,6 +253,14 @@ __git_zsh_main ()
;;
(arg)
local command="${words[1]}" __git_dir __git_cmd_idx=1
+ local -a __git_C_args
+ local -i i=2
+
+ while [[ ${orig_words[i]} == -C ]]; do
+ __git_C_args+=(-C ${orig_words[i+1]})
+ (( __git_cmd_idx += 2 ))
+ (( i += 2 ))
+ done
if (( $+opt_args[--bare] )); then
__git_dir='.'
base-commit: 0fae78c9d55efe705877ea537fe42c59164ccd94
--
gitgitgadget
^ permalink raw reply related
* Re: [PATCH v3 00/17] odb: make packed object source a proper `struct odb_source`
From: Justin Tobler @ 2026-06-17 15:02 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git, Karthik Nayak
In-Reply-To: <20260617-pks-odb-source-packed-v3-0-b5c7583cd795@pks.im>
On 26/06/17 08:39AM, Patrick Steinhardt wrote:
> 5: 8eb3cb17a1 ! 5: c9b1e1da26 odb/source-packed: start converting to a proper `struct odb_source`
> @@ Commit message
> odb_source`, as it's missing all of the callback implementations. These
> will be wired up in subsequent commits.
>
> + Further note that we're also registering a `chdir_notify` callback to
> + reparent our path. This wasn't previously necessary (and still isn't at
> + this point in time) because all paths are taken from the owning "files"
> + source, and that source already handles the reparenting for us. But a
> + subsequent commit will change that so that we're using the path of the
> + "packed" source, and once that happens we'll need it to be updated when
> + changing the working directory.
Ah ok, the "file" ODB source already has a `chdir_notify` callback
registered to handle this which is why we could get away with using the
path taken from the parent. Make sense. The explaination here is very
helpful.
This version of the series looks good to me.
Thanks
-Justin
^ permalink raw reply
* Re: [PATCH v15 0/7] branch: delete-merged
From: Phillip Wood @ 2026-06-17 14:21 UTC (permalink / raw)
To: Harald Nordgren
Cc: Harald Nordgren via GitGitGadget, git, Kristoffer Haugsbakk,
Johannes Sixt
In-Reply-To: <CAHwyqnXRo=P5Zihs6s7Uh8CrYCO7mjyeZ5nAv9JqYbGH0RE72g@mail.gmail.com>
On 17/06/2026 12:17, Harald Nordgren wrote:
> On Wed, Jun 17, 2026 at 12:01 PM Phillip Wood <phillip.wood123@gmail.com> wrote:
> >> 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.
Right but you sent that version a few hours after I'd posted a partial
review which concluded by saying I'd finish it the next day. If you send
a new version when you are waiting for further comments it clutters the
list because you know you're going to have to post another revision when
you get the rest of the comments. Anyone reviewing the interim version
is wasting their time. When you receive review comments, by all means
start thinking about them and updating your local copy but please don't
post a new version until the discussion on the previous version has
settled down.
>> 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.
Oh, are we passing flags on to another function? If so I'd missed that
and it does complicate things as we don't want two sources of truth.
Thanks
Phillip
>
> Harald
^ permalink raw reply
* Re: [PATCH] sequencer: Skip copying notes for commits that disappear during rebase
From: Uwe Kleine-König @ 2026-06-17 13:58 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git, Phillip Wood
In-Reply-To: <xmqqzf0txpu4.fsf@gitster.g>
[-- Attachment #1: Type: text/plain, Size: 5911 bytes --]
Hello Junio,
On Wed, Jun 17, 2026 at 06:24:03AM -0700, Junio C Hamano wrote:
> Uwe Kleine-König <u.kleine-koenig@baylibre.com> writes:
>
> > Note that Phillip also suggested to integrete the test into
> > t3400-rebase.sh . IMHO it doesn't matter much if this is considered a
> > rebase test or a notes test. I kept it where I have it because I'm lazy
> > and failed to understand the git history created in that test.
>
> I do not think his suggestion was about "is this rebase or notes?"
> at all. It was a lot more about "let's not add a new test script
> that does only one thing, when there is already a script that covers
> the same command and the same option for the command". In fact,
> around 3400.28 there are test pieces that rebases commits that have
> notes.
OK, sounds fair.
> > sequencer.c | 20 ++++++++++----------
> > t/meson.build | 1 +
> > t/t3322-notes-rebase.sh | 37 +++++++++++++++++++++++++++++++++++++
> > 3 files changed, 48 insertions(+), 10 deletions(-)
> > create mode 100755 t/t3322-notes-rebase.sh
>
> We need some documentation updates to describe that the users can
> lose notes by doing a rebase and under what condition, no?
Well, the current state is that we're not losing notes, but that we
attach it to commits that most of the time are completely unrelated to
the commit the note was initially attached to. (i.e. in general it's not
attached to the commit that made the currently picked commit empty.) So
essentially the notes are lost, too, but also add confusion to where
they happen to get attached to.
> It is not yet clear to me if we want to _always_ discard a note from
> a commit that would become "empty" during a rebase session (in other
> words, a commit that becomes empty during a rebase is _always_ a
> sign that the change it brings in is _already_ in the new base of
> the rebase
Yeah, or in a patch that was picked before.
> and the necessary information the note wanted to carry to
> the target branch is there without need to _duplicate_ it by copying
> the note). But assuming that we want the behaviour, the code change
> to sequencer.c looks very reasonable to me, except for one thing that
> I am not clear about.
I think given the commit goes away, it's natural that the note goes
away, too. And to come back to your question above: I think it doesn't
need documentation, that if a commit disappears its notes go away, too.
But that might be subjective?!
> > diff --git a/sequencer.c b/sequencer.c
> > index 57855b0066ac..da2185a37c5d 100644
> > --- a/sequencer.c
> > +++ b/sequencer.c
> > ...
> > @@ -4965,7 +4965,7 @@ static int pick_one_commit(struct repository *r,
> > return error_with_patch(r, commit,
> > arg, item->arg_len, opts, res, !res);
> > }
> > - if (is_rebase_i(opts) && !res)
> > + if (is_rebase_i(opts) && !res && !dropped_commit)
> > record_in_rewritten(&item->commit->object.oid,
> > peek_command(todo_list, 1));
>
> If we have a sequence of commits where a commit that was *not*
> dropped is followed by a fixup commit that *is* dropped (e.g.,
> because it became empty/redundant), wouldn't it prevent the
> previously pending commit from being flushed to skip
> `record_in_rewritten` entirely for the dropped fixup commit?
>
> For example, if we have
>
> pick X (with note)
> fixup B (dropped because it is redundant)
> pick C
>
> 1. `pick X`: calls `record_in_rewritten(X, TODO_FIXUP)`. `X` is
> written to `pending`, but not flushed because the next insn is
> `TODO_FIXUP` (B).
>
> 2. `fixup B`: gets dropped. `dropped_commit` is 1 in the code above,
> so `record_in_rewritten` is skipped.
>
> 3. `pick C`: calls `record_in_rewritten(C, -1)`. `C` is written to
> `pending`. Since next insn is not a fixup, it flushes `pending`
> (which contains both `X` and `C`) to the commit created for `C`.
Huh, sounds possible. I wonder if that makes the change so complicated
that my time isn't well spend working on that given that I'm not used to
git's source code and it's better addressed by someone with deeper
knowledge. Sounds as if we need a state signaling "Current commit is
done".
> Wouldn't it map the note for `X` to rewritten `C`?
>
> > diff --git a/t/t3322-notes-rebase.sh b/t/t3322-notes-rebase.sh
> > new file mode 100755
> > index 000000000000..0eddde7f9961
> > --- /dev/null
> > +++ b/t/t3322-notes-rebase.sh
> > @@ -0,0 +1,37 @@
> > +#!/bin/sh
> > +
> > +test_description='Test notes on rebase'
> > +
> > +. ./test-lib.sh
> > +
> > +test_expect_success setup '
> > + git init &&
> > + git config notes.rewriteRef refs/notes/commits &&
> > + git version > version &&
> > + echo A > A &&
>
> Style. In our codebase, redirection operator sticks to the
> redirection target without SP in between, i.e.
>
> git version >version &&
> echo A >A &&
>
> > + git notes add -m "This is B" @ &&
>
> '@' is hard to read; when you refer to HEAD, please write HEAD.
>
>
> > +test_expect_success 'rebase B + C on top of BD' '
> > + git rebase @ master
> > +'
> > +
> > +test_expect_success 'assert there is no note on BD' '
> > + if git notes list branch >/tmp/lalaa; then return 1; fi
> > +'
>
> Do not step outside of $TRASH_DIRECTORY without a good reason.
Oh, that is a debug thing that shouldn't have made it into the patch.
> Style. In our codebase, shell scripts do not use ';' and written
> more like
>
> if git notes list branch >notes-list
> then
> return 1
> fi
>
> But more importantly, if you want to make sure the command makes a
> controlled exit (not crash), use
>
> test_must_fail git notes list branch
Ah, I really wondered if I'm missing something because it should be
easier to say "this command should fail".
Best regards
Uwe
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 488 bytes --]
^ permalink raw reply
* Re: [PATCH] sequencer: Skip copying notes for commits that disappear during rebase
From: Junio C Hamano @ 2026-06-17 13:24 UTC (permalink / raw)
To: Uwe Kleine-König; +Cc: git, Phillip Wood
In-Reply-To: <20260616174012.601651-2-u.kleine-koenig@baylibre.com>
Uwe Kleine-König <u.kleine-koenig@baylibre.com> writes:
> Note that Phillip also suggested to integrete the test into
> t3400-rebase.sh . IMHO it doesn't matter much if this is considered a
> rebase test or a notes test. I kept it where I have it because I'm lazy
> and failed to understand the git history created in that test.
I do not think his suggestion was about "is this rebase or notes?"
at all. It was a lot more about "let's not add a new test script
that does only one thing, when there is already a script that covers
the same command and the same option for the command". In fact,
around 3400.28 there are test pieces that rebases commits that have
notes.
> sequencer.c | 20 ++++++++++----------
> t/meson.build | 1 +
> t/t3322-notes-rebase.sh | 37 +++++++++++++++++++++++++++++++++++++
> 3 files changed, 48 insertions(+), 10 deletions(-)
> create mode 100755 t/t3322-notes-rebase.sh
We need some documentation updates to describe that the users can
lose notes by doing a rebase and under what condition, no?
It is not yet clear to me if we want to _always_ discard a note from
a commit that would become "empty" during a rebase session (in other
words, a commit that becomes empty during a rebase is _always_ a
sign that the change it brings in is _already_ in the new base of
the rebase and the necessary information the note wanted to carry to
the target branch is there without need to _duplicate_ it by copying
the note). But assuming that we want the behaviour, the code change
to sequencer.c looks very reasonable to me, except for one thing that
I am not clear about.
> diff --git a/sequencer.c b/sequencer.c
> index 57855b0066ac..da2185a37c5d 100644
> --- a/sequencer.c
> +++ b/sequencer.c
> ...
> @@ -4965,7 +4965,7 @@ static int pick_one_commit(struct repository *r,
> return error_with_patch(r, commit,
> arg, item->arg_len, opts, res, !res);
> }
> - if (is_rebase_i(opts) && !res)
> + if (is_rebase_i(opts) && !res && !dropped_commit)
> record_in_rewritten(&item->commit->object.oid,
> peek_command(todo_list, 1));
If we have a sequence of commits where a commit that was *not*
dropped is followed by a fixup commit that *is* dropped (e.g.,
because it became empty/redundant), wouldn't it prevent the
previously pending commit from being flushed to skip
`record_in_rewritten` entirely for the dropped fixup commit?
For example, if we have
pick X (with note)
fixup B (dropped because it is redundant)
pick C
1. `pick X`: calls `record_in_rewritten(X, TODO_FIXUP)`. `X` is
written to `pending`, but not flushed because the next insn is
`TODO_FIXUP` (B).
2. `fixup B`: gets dropped. `dropped_commit` is 1 in the code above,
so `record_in_rewritten` is skipped.
3. `pick C`: calls `record_in_rewritten(C, -1)`. `C` is written to
`pending`. Since next insn is not a fixup, it flushes `pending`
(which contains both `X` and `C`) to the commit created for `C`.
Wouldn't it map the note for `X` to rewritten `C`?
> diff --git a/t/t3322-notes-rebase.sh b/t/t3322-notes-rebase.sh
> new file mode 100755
> index 000000000000..0eddde7f9961
> --- /dev/null
> +++ b/t/t3322-notes-rebase.sh
> @@ -0,0 +1,37 @@
> +#!/bin/sh
> +
> +test_description='Test notes on rebase'
> +
> +. ./test-lib.sh
> +
> +test_expect_success setup '
> + git init &&
> + git config notes.rewriteRef refs/notes/commits &&
> + git version > version &&
> + echo A > A &&
Style. In our codebase, redirection operator sticks to the
redirection target without SP in between, i.e.
git version >version &&
echo A >A &&
> + git notes add -m "This is B" @ &&
'@' is hard to read; when you refer to HEAD, please write HEAD.
> +test_expect_success 'rebase B + C on top of BD' '
> + git rebase @ master
> +'
> +
> +test_expect_success 'assert there is no note on BD' '
> + if git notes list branch >/tmp/lalaa; then return 1; fi
> +'
Do not step outside of $TRASH_DIRECTORY without a good reason.
Style. In our codebase, shell scripts do not use ';' and written
more like
if git notes list branch >notes-list
then
return 1
fi
But more importantly, if you want to make sure the command makes a
controlled exit (not crash), use
test_must_fail git notes list branch
That will pass the test happily if "git notes list branch" makes a
controlled die() call (e.g., when there is no notes attached to that
commit, the command exits with 1), but still makes the test fail if
"git notes list branch" segfaults.
Again, we do not want to add a new test script that does only one
thing, when there is already a script that covers the same command
and the same option for the command.
Thanks.
^ permalink raw reply
* 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
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