checkout: introduce "--to-branch" option
cover checkout: introduce "--to-branch" option
Sometimes people expect to checkout to a commit (or a commit which tag reference to) and develop on its corresponding branch. This may take some steps:
$ git checkout v1.1 $ git log # search for the branch dev1 $ git checkout dev1
collapse the above steps into one step:
$ git checkout --to-branch v1.1 Switched to branch 'refs/heads/dev1'
This will help users switch to the unqiue branch quickly.
Thanks.
ZheNing Hu (2):
checkout: handling branch_info memory leak
checkout: introduce "--to-branch" option
Documentation/git-checkout.txt | 8 +++- builtin/checkout.c | 49 +++++++++++++++++++- t/t2018-checkout-branch.sh | 85 ++++++++++++++++++++++++++++++++++ t/t9902-completion.sh | 1 + 4 files changed, 140 insertions(+), 3 deletions(-)
base-commit: abe6bb3905392d5eb6b01fa6e54d7e784e0522aa
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1095%2Fadlternative%2Fcheckout-w-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1095/adlternative/checkout-w-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/1095
Signature · 1 line
gitgitgadget
PATCH 1/2 checkout: handling branch_info memory leak
From: ZheNing Hu <[email protected]>
On the checkout code path, we dynamically allocate memory for refname and path in struct branch_info, but did not free their memory at the end. So introduce branch_info_clear() which can free branch_info memory.
--- builtin/checkout.c | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-)
builtin/checkout.c +14 -2
@@ -103,6 +103,15 @@ struct branch_info {char *checkout; }; +static void branch_info_clear (struct branch_info *branch_info) { +if (branch_info->path) { +free ((char *)branch_info->path); + branch_info->path =NULL ; + } +if (branch_info->refname) +FREE_AND_NULL (branch_info->refname); +} +static int post_checkout_hook (struct commit *old_commit,struct commit *new_commit,int changed) {
@@ -1578,6 +1587,7 @@ static int checkout_main(int argc, const char **argv, const char *prefix, {struct branch_info new_branch_info;int parseopt_flags =0 ; +int ret =0 ;memset (&new_branch_info,0 ,sizeof (new_branch_info)); opts->overwrite_ignore =1 ;
@@ -1768,9 +1778,11 @@ static int checkout_main(int argc, const char **argv, const char *prefix,UNLEAK (opts);if (opts->patch_mode || opts->pathspec.nr) -return checkout_paths (opts, &new_branch_info); + ret =checkout_paths (opts, &new_branch_info);else -return checkout_branch (opts, &new_branch_info); + ret =checkout_branch (opts, &new_branch_info); +branch_info_clear (&new_branch_info); +return ret; }int cmd_checkout (int argc,const char **argv,const char *prefix)
Signature · 1 line
gitgitgadget
PATCH 2/2 checkout: introduce "--to-branch" option
In reply to ZheNing Hu via GitGitGadgetFrom: ZheNing Hu <[email protected]>
When we want checkout to a branch (e.g. dev1) which reference to a commit, but sometimes we only remember the tag (e.g. v1.1) on it, we will use git checkout v1.1 to find the commit first, git will be in the state of deatching HEAD, so we have to search the branches on the commit and checkout the branch we perfer. This will be a bit cumbersome.
Introduce "--to-branch" option, git checkout --to-branch <tag> and git checkout --to-branch <commit> will search all branches and find a unique branch reference to the commit (or the commit which the tag reference to) and checkout to it. If the commit have more than one branches, it will report error "here are more than one branch on commit".
--- Documentation/git-checkout.txt | 8 +++- builtin/checkout.c | 33 +++++++++++++ t/t2018-checkout-branch.sh | 85 ++++++++++++++++++++++++++++++++++ t/t9902-completion.sh | 1 + 4 files changed, 126 insertions(+), 1 deletion(-)
Documentation/git-checkout.txt +7 -1
@@ -10,7 +10,7 @@ SYNOPSIS [verse] '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] [--detach] [-w|--to-branch] <commit> 'git checkout' [-q] [-f] [-m] [[-b|-B|--orphan] <new_branch>] [<start_point>] 'git checkout' [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] [--] <pathspec>... 'git checkout' [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] --pathspec-from-file=<file> [--pathspec-file-nul]
@@ -210,6 +210,12 @@ variable. `<commit>` is not a branch name. See the "DETACHED HEAD" section below for details. +-w:: +--to-branch:: + Rather than checking out a commit to work on it, checkout out + to the unique branch on it. If there are multiple branches on + the commit, the checkout will fail. + --orphan <new_branch>:: Create a new 'orphan' branch, named `<new_branch>`, started from `<start_point>` and switch to it. The first commit made on this
builtin/checkout.c +33 -0
@@ -28,6 +28,7 @@#include "xdiff-interface.h" #include "entry.h" #include "parallel-checkout.h" +#include "../ref-filter.h" static const char *const checkout_usage[] = {N_ ("git checkout [<options>] <branch>" ),
@@ -70,6 +71,7 @@ struct checkout_opts {int empty_pathspec_ok;int checkout_index;int checkout_worktree; +int to_branch;const char *ignore_unmerged_opt;int ignore_unmerged;int pathspec_file_nul;
@@ -1512,6 +1514,35 @@ static int checkout_branch(struct checkout_opts *opts, (flag & REF_ISSYMREF) && is_null_oid(&rev))return switch_unborn_to_new_branch (opts); } +if (opts->to_branch) { +struct ref_filter filter; +struct ref_array array; +int i; +int count =0 ; +const char *unused_pattern =NULL ; + +memset (&array,0 ,sizeof (array)); +memset (&filter,0 ,sizeof (filter)); + filter.kind =FILTER_REFS_BRANCHES ; + filter.name_patterns = &unused_pattern; +filter_refs (&array, &filter, filter.kind); +for (i =0 ; i < array.nr; i++) { +if (oideq (&array.items[i]->objectname, &new_branch_info->oid)) { +if (count) +die (_ ("here are more than one branch on commit %s" ),oid_to_hex (&new_branch_info->oid)); + count++; +if (new_branch_info->refname) +free ((char *)new_branch_info->refname); + new_branch_info->refname =xstrdup (array.items[i]->refname); +if (new_branch_info->path) +free ((char *)new_branch_info->path); + new_branch_info->path =xstrdup (array.items[i]->refname); + new_branch_info->name = new_branch_info->path; + } + } +ref_array_clear (&array); + } +return switch_branches (opts, new_branch_info); }
@@ -1797,6 +1828,8 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)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)" )), +OPT_BOOL ('w' ,"to-branch" , &opts.to_branch, +N_ ("checkout to a branch from a commit or a tag" )),OPT_BOOL (0 ,"overlay" , &opts.overlay_mode,N_ ("use overlay mode (default)" )), OPT_END() };
t/t2018-checkout-branch.sh +85 -0
89 lines not on this page · raw message
t/t9902-completion.sh +1 -0
@@ -2021,6 +2021,7 @@ test_expect_success 'double dash "git checkout"' ' --orphan=Z --ours Z --theirs Z + --to-branch Z --merge Z --conflict=Z --patch Z
Signature · 1 line
gitgitgadget
Re: [PATCH 1/2] checkout: handling branch_info memory leak
In reply to ZheNing Hu via GitGitGadgetQuoted 6 lines · ZheNing Hu via GitGitGadget
From: ZheNing Hu <[email protected]>
On the checkout code path, we dynamically allocate memory for
refnameandpathinstruct branch_info, but did not free their memory at the end. So introducebranch_info_clear()which can freebranch_infomemory.
This (partially) duplicates my 9081a421a6d (checkout: fix "branch info" memory leaks, 2021-11-16) which is in "next" already.
Quoted 7 lines · ZheNing Hu via GitGitGadget
From: ZheNing Hu <[email protected]>
When we want checkout to a branch (e.g. dev1) which reference to a commit, but sometimes we only remember the tag (e.g. v1.1) on it, we will use
git checkout v1.1to find the commit first, git will be in the state of deatching HEAD, so we have to search the
s/deatching/detached/
It's possible, and likely faster, to use something like git log -1 v1.1 or git show <sha> to see which branches point to the tag or commit, as the branches are added as decoration to the output.
Quoted 1 line
branches on the commit and checkout the branch we perfer. This will
s/perfer/prefer/
Quoted 6 lines
be a bit cumbersome.
Introduce "--to-branch" option,
git checkout --to-branch <tag>andgit checkout --to-branch <commit>will search all branches and find a unique branch reference to the commit (or the commit which the tag reference to) and checkout to it. If the commit have more
s/have/has/
Quoted 1 line
than one branches, it will report error "here are more than one
s/branches/branch/
Quoted 1 line
branch on commit".
Maybe reporting something like "more than one branch point to commit XXX" would be a bit better.
Quoted 13 lines
Signed-off-by ZheNing Hu <[email protected]>--- Documentation/git-checkout.txt | 8 +++- builtin/checkout.c | 33 +++++++++++++ t/t2018-checkout-branch.sh | 85 ++++++++++++++++++++++++++++++++++ t/t9902-completion.sh | 1 + 4 files changed, 126 insertions(+), 1 deletion(-)Documentation/git-checkout.txt +0
-0
Documentation/git-checkout.txt SYNOPSIS · line 13
Quoted 5 lines
Documentation/git-checkout.txt +1
-1@@ -10,7 +10,7 @@ SYNOPSIS [verse] '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] [--detach] [-w|--to-branch] <commit>
It's a bit strange that --detach can be used along with the new option, as the purpose of the new option is to not detach. It makes one wonder what happens when both --detach and --to-branch are used.
I wonder if all the following lines:
git checkout [-q] [-f] [-m] [<branch>]
git checkout [-q] [-f] [-m] --detach [<branch>]
git checkout [-q] [-f] [-m] [--detach] <commit>could be replaced with just:
git checkout [-q] [-f] [-m] [--detach|--to-branch] <commitish>
Documentation/git-checkout.txt variable. · line 214
Quoted 9 lines
Documentation/git-checkout.txt +2
-0'git checkout' [-q] [-f] [-m] [[-b|-B|--orphan] <new_branch>] [<start_point>] 'git checkout' [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] [--] <pathspec>... 'git checkout' [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] --pathspec-from-file=<file> [--pathspec-file-nul]@@ -210,6 +210,12 @@ variable. `<commit>` is not a branch name. See the "DETACHED HEAD" section below for details. +-w:: +--to-branch::
Using a short option name like "-w" might not be a good idea at this point. Maybe if many people use the long option a lot, they will want a short option name, but we can add it then instead of using one of the few left right now.
Documentation/git-checkout.txt variable. · line 215
Quoted 1 line
Documentation/git-checkout.txt +1
-0+ Rather than checking out a commit to work on it, checkout out
"checking out a commit to work on it" might not describe well that it works when we pass a tag too and that we checkout the underlying commit in the detached HEAD mode by default.
Documentation/git-checkout.txt variable. · line 217
Quoted 2 lines
Documentation/git-checkout.txt +2
-0+ to the unique branch on it. If there are multiple branches on + the commit, the checkout will fail.
It might be a bit better to say that a branch "points to" a commit, rather than that it is "on" a commit.
builtin/checkout.c line 31
Quoted 12 lines
Documentation/git-checkout.txt +0
-0--orphan <new_branch>:: Create a new 'orphan' branch, named `<new_branch>`, started from `<start_point>` and switch to it. The first commit made on thisbuiltin/checkout.c +1
-0@@ -28,6 +28,7 @@#include "xdiff-interface.h" #include "entry.h" #include "parallel-checkout.h" +#include "../ref-filter.h"
Are you sure that "../" is needed above?
Quoted 31 lines
builtin/checkout.c +18
-0static const char *const checkout_usage[] = {N_ ("git checkout [<options>] <branch>" ),@@ -70,6 +71,7 @@ struct checkout_opts {int empty_pathspec_ok;int checkout_index;int checkout_worktree; +int to_branch;const char *ignore_unmerged_opt;int ignore_unmerged;int pathspec_file_nul;@@ -1512,6 +1514,35 @@ static int checkout_branch(struct checkout_opts *opts, (flag & REF_ISSYMREF) && is_null_oid(&rev))return switch_unborn_to_new_branch (opts); } +if (opts->to_branch) { +struct ref_filter filter; +struct ref_array array; +int i; +int count =0 ; +const char *unused_pattern =NULL ; + +memset (&array,0 ,sizeof (array)); +memset (&filter,0 ,sizeof (filter)); + filter.kind =FILTER_REFS_BRANCHES ; + filter.name_patterns = &unused_pattern; +filter_refs (&array, &filter, filter.kind); +for (i =0 ; i < array.nr; i++) { +if (oideq (&array.items[i]->objectname, &new_branch_info->oid)) { +if (count) +die (_ ("here are more than one branch on commit %s" ),oid_to_hex (&new_branch_info->oid)); + count++;
builtin/checkout.c static int checkout_branch(struct checkout_opts *opts, · line 1543
Quoted 10 lines
builtin/checkout.c +10
-0+if (new_branch_info->refname) +free ((char *)new_branch_info->refname); + new_branch_info->refname =xstrdup (array.items[i]->refname); +if (new_branch_info->path) +free ((char *)new_branch_info->path); + new_branch_info->path =xstrdup (array.items[i]->refname); + new_branch_info->name = new_branch_info->path; + } + } +ref_array_clear (&array);
It might be my personal taste, but I would find it cleaner and easier to understand if a separate function to find the branch we are looking for was called, instead of adding all the code here.
t/t2018-checkout-branch.sh
Quoted 18 lines
builtin/checkout.c +4
-0+ } +return switch_branches (opts, new_branch_info); }@@ -1797,6 +1828,8 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)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)" )), +OPT_BOOL ('w' ,"to-branch" , &opts.to_branch, +N_ ("checkout to a branch from a commit or a tag" )),OPT_BOOL (0 ,"overlay" , &opts.overlay_mode,N_ ("use overlay mode (default)" )), OPT_END() };t/t2018-checkout-branch.sh +0
-0
I plan to look at the tests after we decide how the new option relates to --detach.
Thanks!
Quoted 38 lines · ZheNing Hu via GitGitGadget
From: ZheNing Hu <[email protected]>
When we want checkout to a branch (e.g. dev1) which reference to a commit, but sometimes we only remember the tag (e.g. v1.1) on it, we will use
git checkout v1.1to find the commit first, git will be in the state of deatching HEAD, so we have to search the branches on the commit and checkout the branch we perfer. This will be a bit cumbersome.Introduce "--to-branch" option,
git checkout --to-branch <tag>andgit checkout --to-branch <commit>will search all branches and find a unique branch reference to the commit (or the commit which the tag reference to) and checkout to it. If the commit have more than one branches, it will report error "here are more than one branch on commit".Signed-off-by ZheNing Hu <[email protected]>--- Documentation/git-checkout.txt | 8 +++- builtin/checkout.c | 33 +++++++++++++ t/t2018-checkout-branch.sh | 85 ++++++++++++++++++++++++++++++++++ t/t9902-completion.sh | 1 + 4 files changed, 126 insertions(+), 1 deletion(-)Documentation/git-checkout.txt +1
-1@@ -10,7 +10,7 @@ SYNOPSIS [verse] '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] [--detach] [-w|--to-branch] <commit> 'git checkout' [-q] [-f] [-m] [[-b|-B|--orphan] <new_branch>] [<start_point>] 'git checkout' [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] [--] <pathspec>... 'git checkout' [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] --pathspec-from-file=<file> [--pathspec-file-nul]
Documentation/git-checkout.txt variable. · line 218
Quoted 9 lines
Documentation/git-checkout.txt +6
-0@@ -210,6 +210,12 @@ variable. `<commit>` is not a branch name. See the "DETACHED HEAD" section below for details. +-w:: +--to-branch:: + Rather than checking out a commit to work on it, checkout out + to the unique branch on it. If there are multiple branches on + the commit, the checkout will fail. +
So basically what this option implements is something that could be done as a shellscript of:
git_checkout_branch_from_oid () {
rev=$1
git for-each-ref --format='%(refname:strip=2)' --points-at $rev >/tmp/found
if test $(wc -l </tmp/found) -ne 1
then
echo "Goldilocks error in finding $rev: $(cat /tmp/found)"
return 1
fi
git checkout $found
}Which is not to say that it isn't useful, but that I think adding this to "git checkout" specifically is adding this to the wrong level. Isn't this useful to most things that parse revisions? I.e. wouldn't a better interface be via the peel syntax?
oid=$(git rev-parse HEAD)
git checkout $oid^{tobranch}Doing it that way would allow any arbitrary command that takes revisions now access to that, and we could have e.g. "^{tobranches}" too, so you could do:
git for-each-ref --format='%(refname:strip=2)' $oid^{tobranches}Or:
git log $oid^{tobranches}I think implementing that is a bit harder. It's peel_onion() in object-name.c. I think parse_branchname_arg() via get_oid_mb() is now only capable of filling in an OID for a given name, and then checking out that name comes as a separate step, and you can't just return e.g. "master".
But I don't think anything stops us from adjusting those functions a bit so that get_oid_with_context(() and friends could pass down say an optional "struct string_list *", and the "peel" could then be expanded to that.
Similar to how we have "git chekout -", and the "-" is understood by some commands, but not all (via some opt-in whose location I forget...).
Documentation/git-checkout.txt variable. · line 218
Quoted 6 lines · ZheNing Hu via GitGitGadget
+-w:: +--to-branch::Documentation/git-checkout.txt +4
-0+ Rather than checking out a commit to work on it, checkout out + to the unique branch on it. If there are multiple branches on + the commit, the checkout will fail. +
Did you mean unique branch that contains the commit?
Signature · 1 line
An old man doll... just what I always wanted! - Clara
Quoted 9 lines · ZheNing Hu via GitGitGadget
From: ZheNing Hu <[email protected]>
When we want checkout to a branch (e.g. dev1) which reference to a commit, but sometimes we only remember the tag (e.g. v1.1) on it, we will use
git checkout v1.1to find the commit first, git will be in the state of deatching HEAD, so we have to search the branches on the commit and checkout the branch we perfer. This will be a bit cumbersome.
Quoted 6 lines
Introduce "--to-branch" option,
git checkout --to-branch <tag>andgit checkout --to-branch <commit>will search all branches and find a unique branch reference to the commit (or the commit which the tag reference to) and checkout to it. If the commit have more than one branches, it will report error "here are more than one branch on commit".
Sorry, but the above explanation does not make any sense to me.
It is unclear if you mean "dev1" exactly point at the commit tagged as v1.1, or you want the branch "dev1" that is a descedanant of v1.1. Without telling that to the reader, the above explanation is useless.
And whether you meant the former or the latter, neither use case does not make much sense.
First, suppose you meant "checkout --to-branch v1.1" to find a branch whose tip exactly points at v1.1. You instead check out "dev1" branch, and work on it to advance its history. When you are done, you may go to another branch and work on something else.
But then what? When you need another topic that also needs to be later merge-able to v1.1, "checkout --to-branch v1.1" no longer will be able to find "dev1", because, well, you have already used it to build something else.
So, "--to-branch v1.1" that finds and checks out a branch whose tip exactly points at v1.1 would be pretty useless.
So let's correct the unwritten assumption and say "--to-branch v1.1" finds a branch that is descendant of the tag. It is like I have maint-2.33 branch to prepare for v2.33.1, v2.33.2,... maintenance releases and being able to find maint-2.33 by saying v2.33.2 (or v2.33.1) _might_ be convenient.
But that would only be true if there is only one single branch per family of tags (in the above example, v2.33.* tags). You cannot use the workflow where many topic branches run in parallel, and get merged to the integration branch(es) only after they are ready, because you need bugfix-1-for-v2.33, bugfix-2-for-v2.33,... branches all forked from v2.33.0 (or a commit with a later tag in the v2.33.* family) to cook these independent fixes that are destined for the maint-2.33 integration branch, so you cannot uniquely find maint-2.33 by saying v2.33.0 or v2.33.1 or whatever.
I also sense that the first paragraph of the proposed log message for this commit hints that the user needs a bit more studying of existing tools. When we know v1.1 but do not know if we already have branches that are based on it, we DO NOT do "git checkout v1.1". Instead the first thing we would do is "git branch --contains v1.1" (add "--no-merged main" to exclude the branches that have already graduated to 'main').
So, for this partcular topic, what I would recommend is *not* jump in and add a new feature, but to study what's available and build a workflow around the existing features.
Quoted 10 lines · Christian Couder <[email protected]> 于2021年12月10日周五 16:34写道:
Quoted 5 lines
[verse]
'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] [--detach] [-w|--to-branch] <commit>It's a bit strange that --detach can be used along with the new option, as the purpose of the new option is to not detach. It makes one wonder what happens when both --detach and --to-branch are used.
When both --detach and --to-branch are used, --detach will work... Of course, it should be reasonable to prevent them from being used at the same time.
Quoted 10 lines
I wonder if all the following lines:
git checkout [-q] [-f] [-m] [<branch>] git checkout [-q] [-f] [-m] --detach [<branch>] git checkout [-q] [-f] [-m] [--detach] <commit>could be replaced with just:
git checkout [-q] [-f] [-m] [--detach|--to-branch] <commitish>
Well, it will be much more concise.
Quoted 7 lines
Quoted 7 lines
'git checkout' [-q] [-f] [-m] [[-b|-B|--orphan] <new_branch>] [<start_point>] 'git checkout' [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] [--] <pathspec>... 'git checkout' [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] --pathspec-from-file=<file> [--pathspec-file-nul]Documentation/git-checkout.txt +0
-0@@ -210,6 +210,12 @@ variable. `<commit>` is not a branch name. See the "DETACHED HEAD" section below for details.
Quoted 8 lines
Documentation/git-checkout.txt variable. · line 214
Quoted 2 lines
Documentation/git-checkout.txt +2
-0+-w:: +--to-branch::Using a short option name like "-w" might not be a good idea at this point. Maybe if many people use the long option a lot, they will want a short option name, but we can add it then instead of using one of the few left right now.
Makes sense.
Quoted 6 lines
Documentation/git-checkout.txt variable. · line 215
Quoted 1 line
Documentation/git-checkout.txt +1
-0+ Rather than checking out a commit to work on it, checkout out"checking out a commit to work on it" might not describe well that it works when we pass a tag too and that we checkout the underlying commit in the detached HEAD mode by default.
Quoted 6 lines
Documentation/git-checkout.txt variable. · line 217
Quoted 2 lines
Documentation/git-checkout.txt +2
-0+ to the unique branch on it. If there are multiple branches on + the commit, the checkout will fail.It might be a bit better to say that a branch "points to" a commit, rather than that it is "on" a commit.
OK.
Quoted 37 lines
Quoted 37 lines
builtin/checkout.c +24
-0static const char *const checkout_usage[] = {N_ ("git checkout [<options>] <branch>" ),@@ -70,6 +71,7 @@ struct checkout_opts {int empty_pathspec_ok;int checkout_index;int checkout_worktree; +int to_branch;const char *ignore_unmerged_opt;int ignore_unmerged;int pathspec_file_nul;@@ -1512,6 +1514,35 @@ static int checkout_branch(struct checkout_opts *opts, (flag & REF_ISSYMREF) && is_null_oid(&rev))return switch_unborn_to_new_branch (opts); } +if (opts->to_branch) { +struct ref_filter filter; +struct ref_array array; +int i; +int count =0 ; +const char *unused_pattern =NULL ; + +memset (&array,0 ,sizeof (array)); +memset (&filter,0 ,sizeof (filter)); + filter.kind =FILTER_REFS_BRANCHES ; + filter.name_patterns = &unused_pattern; +filter_refs (&array, &filter, filter.kind); +for (i =0 ; i < array.nr; i++) { +if (oideq (&array.items[i]->objectname, &new_branch_info->oid)) { +if (count) +die (_ ("here are more than one branch on commit %s" ),oid_to_hex (&new_branch_info->oid)); + count++; +if (new_branch_info->refname) +free ((char *)new_branch_info->refname); + new_branch_info->refname =xstrdup (array.items[i]->refname); +if (new_branch_info->path) +free ((char *)new_branch_info->path); + new_branch_info->path =xstrdup (array.items[i]->refname);
Quoted 9 lines
builtin/checkout.c static int checkout_branch(struct checkout_opts *opts, · line 1543
Quoted 4 lines
builtin/checkout.c +4
-0+ new_branch_info->name = new_branch_info->path; + } + } +ref_array_clear (&array);It might be my personal taste, but I would find it cleaner and easier to understand if a separate function to find the branch we are looking for was called, instead of adding all the code here.
You are right.
Quoted 22 lines
t/t2018-checkout-branch.sh
Quoted 18 lines
builtin/checkout.c +4
-0+ } +return switch_branches (opts, new_branch_info); }@@ -1797,6 +1828,8 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)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)" )), +OPT_BOOL ('w' ,"to-branch" , &opts.to_branch, +N_ ("checkout to a branch from a commit or a tag" )),OPT_BOOL (0 ,"overlay" , &opts.overlay_mode,N_ ("use overlay mode (default)" )), OPT_END() };t/t2018-checkout-branch.sh +0
-0I plan to look at the tests after we decide how the new option relates to --detach.
Quoted 1 line
Thanks!
Thanks.
--
ZheNing Hu
Quoted 10 lines · Ævar Arnfjörð Bjarmason <[email protected]> 于2021年12月10日周五 17:24写道:
Documentation/git-checkout.txt variable. · line 218
Quoted 6 lines
+-w:: +--to-branch::Documentation/git-checkout.txt +4
-0+ Rather than checking out a commit to work on it, checkout out + to the unique branch on it. If there are multiple branches on + the commit, the checkout will fail. +So basically what this option implements is something that could be done as a shellscript of:
Quoted 11 lines
git_checkout_branch_from_oid () {
rev=$1
git for-each-ref --format='%(refname:strip=2)' --points-at $rev >/tmp/found
if test $(wc -l </tmp/found) -ne 1
then
echo "Goldilocks error in finding $rev: $(cat /tmp/found)"
return 1
fi
git checkout $found
}Yes, this is the effect I expect, and it can indeed be done through the shellscript.
Quoted 12 lines
Which is not to say that it isn't useful, but that I think adding this to "git checkout" specifically is adding this to the wrong level. Isn't this useful to most things that parse revisions? I.e. wouldn't a better interface be via the peel syntax?
oid=$(git rev-parse HEAD) git checkout $oid^{tobranch}Doing it that way would allow any arbitrary command that takes revisions now access to that, and we could have e.g. "^{tobranches}" too, so you could do:
Quoted 6 lines
git for-each-ref --format='%(refname:strip=2)' $oid^{tobranches}Or:
git log $oid^{tobranches}
Very good inspiration, putting "oid -> branches" in peel will be more elegant and useful.
Quoted 11 lines
I think implementing that is a bit harder. It's
peel_onion()inobject-name.c. I thinkparse_branchname_arg()viaget_oid_mb()is now only capable of filling in an OID for a given name, and then checking out that name comes as a separate step, and you can't just return e.g. "master".But I don't think anything stops us from adjusting those functions a bit so that get_oid_with_context(() and friends could pass down say an optional "struct
string_list*", and the "peel" could then be expanded to that.
I agree.
Quoted 2 lines
Similar to how we have "git chekout -", and the "-" is understood by some commands, but not all (via some opt-in whose location I forget...).
"-" is parsed as "@{-1}".
Thanks.
--
ZheNing Hu
Quoted 11 lines · Bagas Sanjaya <[email protected]> 于2021年12月10日周五 19:51写道:
Documentation/git-checkout.txt variable. · line 218
Quoted 6 lines · ZheNing Hu via GitGitGadget
+-w:: +--to-branch::Documentation/git-checkout.txt +4
-0+ Rather than checking out a commit to work on it, checkout out + to the unique branch on it. If there are multiple branches on + the commit, the checkout will fail. +Did you mean unique branch that contains the commit?
I mean the unique branch which points to the commit.
Quoted 2 lines
--
An old man doll... just what I always wanted! - Clara
--
ZheNing Hu
Quoted 19 lines · Junio C Hamano <[email protected]> 于2021年12月11日周六 06:14写道:
Quoted 9 lines · ZheNing Hu via GitGitGadget
From: ZheNing Hu <[email protected]>
When we want checkout to a branch (e.g. dev1) which reference to a commit, but sometimes we only remember the tag (e.g. v1.1) on it, we will use
git checkout v1.1to find the commit first, git will be in the state of deatching HEAD, so we have to search the branches on the commit and checkout the branch we perfer. This will be a bit cumbersome.Quoted 6 lines
Introduce "--to-branch" option,
git checkout --to-branch <tag>andgit checkout --to-branch <commit>will search all branches and find a unique branch reference to the commit (or the commit which the tag reference to) and checkout to it. If the commit have more than one branches, it will report error "here are more than one branch on commit".
Quoted 7 lines
Sorry, but the above explanation does not make any sense to me.
It is unclear if you mean "dev1" exactly point at the commit tagged as v1.1, or you want the branch "dev1" that is a descedanant of v1.1. Without telling that to the reader, the above explanation is useless.
I meant the former.
Quoted 8 lines
And whether you meant the former or the latter, neither use case does not make much sense.
First, suppose you meant "checkout --to-branch v1.1" to find a branch whose tip exactly points at v1.1. You instead check out "dev1" branch, and work on it to advance its history. When you are done, you may go to another branch and work on something else.
Quoted 8 lines
But then what? When you need another topic that also needs to be later merge-able to v1.1, "checkout --to-branch v1.1" no longer will be able to find "dev1", because, well, you have already used it to build something else.
So, "--to-branch v1.1" that finds and checks out a branch whose tip exactly points at v1.1 would be pretty useless.
Well, I didn't consider what you said before. I just want to find a shortcut for "oid -> branches" and "tag -> branches". And I can quickly use it to switch branches.
Quoted 6 lines
So let's correct the unwritten assumption and say "--to-branch v1.1" finds a branch that is descendant of the tag. It is like I have maint-2.33 branch to prepare for v2.33.1, v2.33.2,... maintenance releases and being able to find maint-2.33 by saying v2.33.2 (or v2.33.1) _might_ be convenient.
Quoted 10 lines
But that would only be true if there is only one single branch per family of tags (in the above example, v2.33.* tags). You cannot use the workflow where many topic branches run in parallel, and get merged to the integration branch(es) only after they are ready, because you need bugfix-1-for-v2.33, bugfix-2-for-v2.33,... branches all forked from v2.33.0 (or a commit with a later tag in the v2.33.* family) to cook these independent fixes that are destined for the maint-2.33 integration branch, so you cannot uniquely find maint-2.33 by saying v2.33.0 or v2.33.1 or whatever.
Well, in the case of many branches pointing to one commit, this "--to-branch" is not very useful.
Quoted 8 lines
I also sense that the first paragraph of the proposed log message for this commit hints that the user needs a bit more studying of existing tools. When we know v1.1 but do not know if we already have branches that are based on it, we DO NOT do "git checkout v1.1". Instead the first thing we would do is "git branch --contains v1.1" (add "--no-merged main" to exclude the branches that have already graduated to 'main').
"git branch --contains v1.1" can find all branches whose history contains the
commit tagged as v1.1. So what if "git checkout --contains v1.1"?
If there is only one branch, checkout to it; if there are multiple branches, it will degenerate into "git branch --contains v1.1" to show these branches to the user. Of course this feature is not very consistent with my original intention...
Quoted 5 lines
So, for this partcular topic, what I would recommend is *not* jump in and add a new feature, but to study what's available and build a workflow around the existing features.
Thanks.
--
ZheNing Hu
Quoted 8 lines · ZheNing Hu
Quoted 5 lines
It is unclear if you mean "dev1" exactly point at the commit tagged as v1.1, or you want the branch "dev1" that is a descedanant of v1.1. Without telling that to the reader, the above explanation is useless.
I meant the former.
Quoted 8 lines
Quoted 5 lines
And whether you meant the former or the latter, neither use case does not make much sense.
...
So, "--to-branch v1.1" that finds and checks out a branch whose tip exactly points at v1.1 would be pretty useless....
"git branch --contains v1.1" can find all branches whose history contains the
commit tagged as v1.1. So what if "git checkout --contains v1.1"?
I already said, whether you meant "the only branch that points exactly at" or "the only branch that contains", the feature does not make sense. Forcing users to keep only a single branch that either points at a given tag is simply impossible and also useless. Once the branch gains even a single commit, it will no loger be pointing at the tag, so "let's prepare a branch pointing at v1.1 just in case when I want to start working from there" would not be a good workflow to begin with. Forcing users to keep only a single branch that contains a given tag would encourage even a worse workflow to throw in unrelated things, whose only commonality is that they all want to fork from a single tag, into a single branch.
IOW, there is nothing we want to add to "git checkout/switch" for this topic. "git checkout --contains $tag" smells like a solution looking for a problem.
Quoted 31 lines · Junio C Hamano
Quoted 8 lines · ZheNing Hu
Quoted 5 lines
It is unclear if you mean "dev1" exactly point at the commit tagged as v1.1, or you want the branch "dev1" that is a descedanant of v1.1. Without telling that to the reader, the above explanation is useless.
I meant the former.
Quoted 8 lines
Quoted 5 lines
And whether you meant the former or the latter, neither use case does not make much sense.
...
So, "--to-branch v1.1" that finds and checks out a branch whose tip exactly points at v1.1 would be pretty useless....
"git branch --contains v1.1" can find all branches whose history contains the
commit tagged as v1.1. So what if "git checkout --contains v1.1"?I already said, whether you meant "the only branch that points exactly at" or "the only branch that contains", the feature does not make sense. Forcing users to keep only a single branch that either points at a given tag is simply impossible and also useless. Once the branch gains even a single commit, it will no loger be pointing at the tag, so "let's prepare a branch pointing at v1.1 just in case when I want to start working from there" would not be a good workflow to begin with. Forcing users to keep only a single branch that contains a given tag would encourage even a worse workflow to throw in unrelated things, whose only commonality is that they all want to fork from a single tag, into a single branch.
Quoted 3 lines
IOW, there is nothing we want to add to "git checkout/switch" for this topic. "git checkout --contains $tag" smells like a solution looking for a problem.
I don't see how it's fundimentally different than the DWIM logic of taking "<name>" and seeing that there's only one "refs/heads/<name>", and giving up in other cases where we get ambiguous reference names that we can't resolve.
I.e. I think this is probably useful for some, it's a common workflow to have a 1=1 relationship like this, and if it's 1=many we can just handle it as we do with ambiguous ref, or ambiguous short OIDs for that matter.
But as I noted upthread I really don't see this making sense as a per-command thing, as opposed to some extension of the "peel" syntax. I.e. we should (or at least expose it as such to the user) interpret that sort of argument/newx syntax before "git checkout" et al get to it, so from a UX perspective you could feed a ^{oid2ref} (or whatever it would be called) to rev-parse, checkout etc. etc.
Quoted 4 lines · Ævar Arnfjörð Bjarmason
I don't see how it's fundimentally different than the DWIM logic of taking "<name>" and seeing that there's only one "refs/heads/<name>", and giving up in other cases where we get ambiguous reference names that we can't resolve.
In that example, once you obtained a local branch whose name is <name>, it is obvious how you would work with that. Your next "git checkout <name>" would work on the local one, and only the initial one does something magical.
Which makes quite a lot of sense.
There is no similarity in it with "--to-branch <tag>" that is being discussed here.
Quoted 12 lines · Junio C Hamano
Quoted 4 lines · Ævar Arnfjörð Bjarmason
I don't see how it's fundimentally different than the DWIM logic of taking "<name>" and seeing that there's only one "refs/heads/<name>", and giving up in other cases where we get ambiguous reference names that we can't resolve.
In that example, once you obtained a local branch whose name is <name>, it is obvious how you would work with that. Your next "git checkout <name>" would work on the local one, and only the initial one does something magical.
Quoted 4 lines
Which makes quite a lot of sense.
There is no similarity in it with "--to-branch <tag>" that is being discussed here.
Another thing you seem to be ignoring is that it requires you to prepare in advance and keep one (and only one) dormant branch for the <tag> you will use the "--to-branch <tag>" with, because it finds the branch whose tip exactly points at the tag.
You can use the command exactly once for the <tag> and check out that branch, but then what? Once you start working on that branch, you would by definition no longer have a suitable branch that can be used to with "--to-branch <tag>", because (1) it was the only one that pointed at <tag> or you'd have gotten an error, and (2) you have moved the tip of the branch so it no longer points at the tag. And you'd expect the user to say "git branch <next-branch> <tag>" in preparation for the next time you might want to use "--to-branch <tag>" and keep that pristine?
I do not think of any similar counterpart in "teach checkout <name> to dwim and fork from a remote-tracking branch from a remote, if there is no other remote with a branch with that name" DWIMming to these nonsense.