diff mbox series

branch: allow "-" as a short-hand for "previous branch"

Message ID pull.1315.git.1659910949556.gitgitgadget@gmail.com (mailing list archive)
State Superseded
Headers show
Series branch: allow "-" as a short-hand for "previous branch" | expand

Commit Message

Rubén Justo Aug. 7, 2022, 10:22 p.m. UTC
From: rjusto <rjusto@gmail.com>

Align "branch" with the intuitive use of "-" as a short-hand
for "@{-1}", like in "checkout" and "merge" commands.

$ git branch -d -      # short-hand for: "git branch -d @{-1}"
$ git branch -D -      # short-hand for: "git branch -D @{-1}"

Signed-off-by: rjusto <rjusto@gmail.com>
---
    branch: allow "-" as a short-hand for "previous branch"
    
    Align "branch" with the intuitive use of "-" as a short-hand for
    "@{-1}", like in "checkout" and "merge" commands.
    
    $ git branch -d - # short-hand for: "git branch -d @{-1}" $ git branch
    -D - # short-hand for: "git branch -D @{-1}"
    
    Signed-off-by: rjusto rjusto@gmail.com

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1315%2Frjusto%2Fmaster-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1315/rjusto/master-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/1315

 builtin/branch.c | 4 ++++
 1 file changed, 4 insertions(+)


base-commit: 679aad9e82d0dfd8ef3d1f98fa4629665496cec9

Comments

Johannes Schindelin Aug. 8, 2022, 1:26 p.m. UTC | #1
Hi Rubén,

On Sun, 7 Aug 2022, Rubén Justo via GitGitGadget wrote:

> From: rjusto <rjusto@gmail.com>
>
> Align "branch" with the intuitive use of "-" as a short-hand
> for "@{-1}", like in "checkout" and "merge" commands.
>
> $ git branch -d -      # short-hand for: "git branch -d @{-1}"
> $ git branch -D -      # short-hand for: "git branch -D @{-1}"

A valuable goal!

> diff --git a/builtin/branch.c b/builtin/branch.c
> index 55cd9a6e998..59c19f38d2e 100644
> --- a/builtin/branch.c
> +++ b/builtin/branch.c
> @@ -241,6 +241,10 @@ static int delete_branches(int argc, const char **argv, int force, int kinds,

Touching only the `delete_branches()` function suggests that other
commands are left as before, e.g. `git branch --unset-upstream -` would
probably fail.

That's fine, but the commit message claims that the `"-"` special-casing
is introduced for the `git branch` command, not just for `git branch -d`.

>  			die(_("Couldn't look up commit object for HEAD"));
>  	}

At this stage, we already handled the `--remotes` flag, therefore I think
that this patch does not do the intended thing for this command-line:

	git branch -d --remotes -

>
> +	if ((argc == 1) && !strcmp(argv[0], "-")) {
> +		argv[0] = "@{-1}";
> +	}

This means that we only handle `git branch -d -`, but not `git branch -d
some-branch - some-other-branch`.

Could you fix that?

My thinking is that this probably should be a sibling of the `@{-1}`
handling, most likely somewhat like this (I only compile-tested this
patch, please take it from here):

-- snip --
diff --git a/object-name.c b/object-name.c
index 4d2746574cd..ae6c2ed7b83 100644
--- a/object-name.c
+++ b/object-name.c
@@ -1420,6 +1420,12 @@ static int interpret_nth_prior_checkout(struct repository *r,
 	const char *brace;
 	char *num_end;

+	if (namelen == 1 && *name == '-') {
+		brace = name;
+		nth = 1;
+		goto find_nth_checkout;
+	}
+
 	if (namelen < 4)
 		return -1;
 	if (name[0] != '@' || name[1] != '{' || name[2] != '-')
@@ -1432,6 +1438,8 @@ static int interpret_nth_prior_checkout(struct repository *r,
 		return -1;
 	if (nth <= 0)
 		return -1;
+
+find_nth_checkout:
 	cb.remaining = nth;
 	cb.sb = buf;

-- snap --

Naturally, this has much bigger ramifications than just `git branch -d -`,
and might even obsolete some `-` special-casing elsewhere; I have not
looked to see if there is any such special-casing, and would like to ask
you to see whether you can find those and remove them in separate commits
after implementing (and testing) the above
`interpret_nth_prior_checkout()` approach.

Thanks,
Johannes

> +
>  	for (i = 0; i < argc; i++, strbuf_reset(&bname)) {
>  		char *target = NULL;
>  		int flags = 0;
>
> base-commit: 679aad9e82d0dfd8ef3d1f98fa4629665496cec9
> --
> gitgitgadget
>
Junio C Hamano Aug. 8, 2022, 2:46 p.m. UTC | #2
"Rubén Justo via GitGitGadget"  <gitgitgadget@gmail.com> writes:

> From: rjusto <rjusto@gmail.com>
>
> Align "branch" with the intuitive use of "-" as a short-hand
> for "@{-1}", like in "checkout" and "merge" commands.
>
> $ git branch -d -      # short-hand for: "git branch -d @{-1}"
> $ git branch -D -      # short-hand for: "git branch -D @{-1}"

The "-d" and "-D" options being the more detructive ones among other
operation modes of the command, I am not sure if this change is even
desirable.  Even if it were, the implementation to special case a
single argument case like this ...

> +	if ((argc == 1) && !strcmp(argv[0], "-")) {
> +		argv[0] = "@{-1}";
> +	}

... (by the way, we don't write braces around a single statement
block) would invite cries from confused users why none of these ...

 $ git branch -m - new-name
 $ git branch new-branch -
 $ git branch --set-upstream-to=<upstream> -
    
work and "-" works only for deletion.

So, I dunno.
Junio C Hamano Aug. 8, 2022, 4:06 p.m. UTC | #3
Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:

> @@ -1420,6 +1420,12 @@ static int interpret_nth_prior_checkout(struct repository *r,
>  	const char *brace;
>  	char *num_end;
>
> +	if (namelen == 1 && *name == '-') {
> +		brace = name;
> +		nth = 1;
> +		goto find_nth_checkout;
> +	}
> +
>  	if (namelen < 4)
>  		return -1;
>  	if (name[0] != '@' || name[1] != '{' || name[2] != '-')

If a solution along this line works, it would be far cleaner design
than the various hacks we have done in the past, noticing "-" and
replacing with "@{-1}".  For one thing, we wouldn't be receiving a
"-" from the end user on the command line and in response say @{-1}
does not make sense in the context in an error message.  That alone
makes the above approach to deal with it at the lowest level quite
attractive.

In the list archive, however, you may be able to find a few past
discussions on why this is not a good idea (some of which I may no
longer agree with).  One thing that still worries me a bit is that
we often disambiguate the command line arguments by seeing "is this
(still) a rev, or is this a file, or can it be interpreted as both?"
and "-" is not judged to be a "rev", IIRC.

Luckily, not many commands we have take "-" as if it were a file and
make it read from the standard input stream, but if there were (or
if we were to add a command to behave like so), treating "-" to mean
the same thing as "@{-1}" everywhere may require the "does this look
like a rev?"  heuristics (which is used by the "earlier ones must be
rev and not file, later ones must be file and cannot be interpreted
as rev, for you to omit '--' from the command line" logic) to be
taught that a lone "-" can be a rev.

So it is quite a lot of thing that the new code needs to get right
before getting there.
Rubén Justo Aug. 13, 2022, 9:08 a.m. UTC | #4
Hi Johannes,

Thanks for the /allow!

On Mon, Aug 8, 2022 at 3:26 PM Johannes Schindelin
<Johannes.Schindelin@gmx.de>  wrote:

> Touching only the `delete_branches()` function suggests that other
> commands are left as before, e.g. `git branch --unset-upstream -` would
> probably fail.
>
> That's fine, but the commit message claims that the `"-"` special-casing
> is introduced for the `git branch` command, not just for `git branch -d`.
>
>>                        die(_("Couldn't look up commit object for HEAD"));
>>        }
> At this stage, we already handled the `--remotes` flag, therefore I think
> that this patch does not do the intended thing for this command-line:
>
>          git branch -d --remotes -
>
>> +     if ((argc == 1) && !strcmp(argv[0], "-")) {
>> +             argv[0] = "@{-1}";
>> +     }
> This means that we only handle `git branch -d -`, but not `git branch -d
> some-branch - some-other-branch`.
>
> Could you fix that?

I did it on purpose, to be interpreted in the context of "git branch
-d/D" with just one branch: "previous branch". I agree the commit
message does not suggest this, I can fix it.

My intention is a short-hand for "delete previous branch", the same
way "git merge -" is "merge previous branch".

The workflow to address is just allow doing:

$ git checkout work_to_review
$ git checkout -
$ git merge -
$ git branch -d -

Instead of:

$ git checkout work_to_review
$ git checkout -
$ git merge -
$ git branch -d work_to_review

The syntax @{-1} is hard for me to write, and I feel intuitive "-",
like in "cd -".

Make sense to me...
Rubén Justo Aug. 13, 2022, 9:14 a.m. UTC | #5
On Mon, Aug 8, 2022 at 4:47 PM Junio C Hamano<gitster@pobox.com>  wrote:

> The "-d" and "-D" options being the more detructive ones among other
> operation modes of the command, I am not sure if this change is even
> desirable.  Even if it were, the implementation to special case a
> single argument case like this ...
>
>> +     if ((argc == 1) && !strcmp(argv[0], "-")) {
>> +             argv[0] = "@{-1}";
>> +     }
> ... (by the way, we don't write braces around a single statement
> block) would invite cries from confused users why none of these ...
>
>   $ git branch -m - new-name
>   $ git branch new-branch -
>   $ git branch --set-upstream-to=<upstream> -
>
> work and "-" works only for deletion.

Agree. But the approach is to ease the deletion of previous branch,
aligned with merge:

$ git merge - -
merge: - - not something we can merge
$ git merge - old-branch
merge: - - not something we can merge

In fact, I think it is a bit confuse to allow use it that way, and
probably induces to error.

Haven't think about -m, -c. If you think it is a good addition, I can do it.

I can fix the braces around that single statement block, sorry.
Rubén Justo Aug. 13, 2022, 9:19 a.m. UTC | #6
On Mon, Aug 8, 2022 at 6:06 PM Junio C Hamano <gitster@pobox.com 
<mailto:gitster@pobox.com>> wrote:
 >
 > Johannes Schindelin <Johannes.Schindelin@gmx.de 
<mailto:Johannes.Schindelin@gmx.de>> writes:
 >
 > > @@ -1420,6 +1420,12 @@ static int 
interpret_nth_prior_checkout(struct repository *r,
 > >       const char *brace;
 > >       char *num_end;
 > >
 > > +     if (namelen == 1 && *name == '-') {
 > > +             brace = name;
 > > +             nth = 1;
 > > +             goto find_nth_checkout;
 > > +     }
 > > +
 > >       if (namelen < 4)
 > >               return -1;
 > >       if (name[0] != '@' || name[1] != '{' || name[2] != '-')
 >
 > If a solution along this line works, it would be far cleaner design
 > than the various hacks we have done in the past, noticing "-" and
 > replacing with "@{-1}".  For one thing, we wouldn't be receiving a
 > "-" from the end user on the command line and in response say @{-1}
 > does not make sense in the context in an error message. That alone
 > makes the above approach to deal with it at the lowest level quite
 > attractive.
 >
 > In the list archive, however, you may be able to find a few past
 > discussions on why this is not a good idea (some of which I may no
 > longer agree with).  One thing that still worries me a bit is that
 > we often disambiguate the command line arguments by seeing "is this
 > (still) a rev, or is this a file, or can it be interpreted as both?"
 > and "-" is not judged to be a "rev", IIRC.
 >
 > Luckily, not many commands we have take "-" as if it were a file and
 > make it read from the standard input stream, but if there were (or
 > if we were to add a command to behave like so), treating "-" to mean
 > the same thing as "@{-1}" everywhere may require the "does this look
 > like a rev?"  heuristics (which is used by the "earlier ones must be
 > rev and not file, later ones must be file and cannot be interpreted
 > as rev, for you to omit '--' from the command line" logic) to be
 > taught that a lone "-" can be a rev.
 >
 > So it is quite a lot of thing that the new code needs to get right
 > before getting there.

Agree. To make a substitution in the command line and to consider "-"
in interpret_nth_prior_checkout, I see them as two very different games.

Previous to this, I thought about making also a "git diff -",
https://github.com/gitgitgadget/git/pull/1314 
<https://github.com/gitgitgadget/git/pull/1314>. Suddenly there was 5
commands with this substitution (checkout, merge, rebase, branch, diff)
so I follow a little the path now Johannes suggests, making the
substitution "- ~ @{-1}" deep in the system. For me, the implications,
error cases, test cases... to consider was not worth the change to
get what I was looking for: align the workflow "checkout/merge/branch
-d".

Also discarded the "git diff -" change, because of so many flags and
conditions "diff" has. So I only sent the "branch -" patch.
Junio C Hamano Aug. 13, 2022, 10:28 p.m. UTC | #7
Rubén Justo <rjusto@gmail.com> writes:

> Previous to this, I thought about making also a "git diff -",
> ...
> Also discarded the "git diff -" change, because of so many flags and
> conditions "diff" has. So I only sent the "branch -" patch.

Yeah, that is why the approach Johannes showed, i.e. instead of
making "in this context but not in that context, '-' means
'previous'" changes to random commands, teaching anybody that takes
@{-1} to understand that '-' means 'previous' everywhere, appears
very tempting.
Johannes Schindelin Aug. 16, 2022, 9:31 a.m. UTC | #8
Hi Rubén,

On Sat, 13 Aug 2022, Rubén Justo wrote:

> On Mon, Aug 8, 2022 at 4:47 PM Junio C Hamano<gitster@pobox.com>  wrote:
>
> > The "-d" and "-D" options being the more detructive ones among other
> > operation modes of the command, I am not sure if this change is even
> > desirable.  Even if it were, the implementation to special case a
> > single argument case like this ...
> >
> > > +     if ((argc == 1) && !strcmp(argv[0], "-")) {
> > > +             argv[0] = "@{-1}";
> > > +     }
> > ... (by the way, we don't write braces around a single statement
> > block) would invite cries from confused users why none of these ...
> >
> >   $ git branch -m - new-name
> >   $ git branch new-branch -
> >   $ git branch --set-upstream-to=<upstream> -
> >
> > work and "-" works only for deletion.
>
> Agree. But the approach is to ease the deletion of previous branch,
> aligned with merge:
>
> $ git merge - -
> merge: - - not something we can merge
> $ git merge - old-branch
> merge: - - not something we can merge

This is confusing me: how is the patch supporting `git branch -d -`
aligned with the presented `git merge` invocations?

In any case, you now have two sets of feedback that say that
special-casing one particular command-line and leaving all other
invocations using `-` unchanged is undesirable, if you needed more than
one such feedback.

Ciao,
Dscho
Johannes Schindelin Aug. 16, 2022, 9:49 a.m. UTC | #9
Hi Junio,

On Mon, 8 Aug 2022, Junio C Hamano wrote:

> Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
>
> > @@ -1420,6 +1420,12 @@ static int interpret_nth_prior_checkout(struct repository *r,
> >  	const char *brace;
> >  	char *num_end;
> >
> > +	if (namelen == 1 && *name == '-') {
> > +		brace = name;
> > +		nth = 1;
> > +		goto find_nth_checkout;
> > +	}
> > +
> >  	if (namelen < 4)
> >  		return -1;
> >  	if (name[0] != '@' || name[1] != '{' || name[2] != '-')
>
> If a solution along this line works, it would be far cleaner design
> than the various hacks we have done in the past, noticing "-" and
> replacing with "@{-1}".

Indeed, but it does not work as-is: `interpret_nth_prior_checkout()` is
used on prefixes of a rev, and for the special handling of `-` we cannot
have that.

To illustrate what I mean: `-` should not be idempotent to `@{-1}` because
we want to allow things like `@{-1}^2~15` but we do not want `-^2~15` to
be a synonym for that.

Therefore, the layer where this `-` handling needs to happen is somewhere
above `interpret_nth_prior_checkout()`, but still well below
`delete_branches()`.

> For one thing, we wouldn't be receiving a "-" from the end user on the
> command line and in response say @{-1} does not make sense in the
> context in an error message.  That alone makes the above approach to
> deal with it at the lowest level quite attractive.
>
> In the list archive, however, you may be able to find a few past
> discussions on why this is not a good idea (some of which I may no
> longer agree with).  One thing that still worries me a bit is that
> we often disambiguate the command line arguments by seeing "is this
> (still) a rev, or is this a file, or can it be interpreted as both?"
> and "-" is not judged to be a "rev", IIRC.

I haven't had the time to perform a thorough analysis (and hoped that
Rubén would rise up to the challenge), but I have not seen a lot of places
where `-` would be ambiguous, especially when taking into account that
revision and file name arguments can be separated via `--`.

One thing we could do, however, would be to patch only
`repo_interpret_branch_name()`, i.e. only allow `-` to imply the previous
branch name in invocations where a branch name is asked for _explicitly_.
I.e. not any random revision, but specifically a branch name.

This would address all of the `git branch` operations we care about, and
leave invocations like `git diff -` unaddressed (which might be more
confusing than we want it to be).

> Luckily, not many commands we have take "-" as if it were a file and
> make it read from the standard input stream, but if there were (or
> if we were to add a command to behave like so), treating "-" to mean
> the same thing as "@{-1}" everywhere may require the "does this look
> like a rev?"  heuristics (which is used by the "earlier ones must be
> rev and not file, later ones must be file and cannot be interpreted
> as rev, for you to omit '--' from the command line" logic) to be
> taught that a lone "-" can be a rev.
>
> So it is quite a lot of thing that the new code needs to get right
> before getting there.

I am not claiming that it will be easy to perform that analysis. It will
be worth the effort, though, I am sure.

And it will be necessary because the current approach of
special-special-casing `git branch -d -` is just too narrow, and a recipe
for totally valid complaints by users.

Ciao,
Dscho
Rubén Justo Aug. 16, 2022, 5:03 p.m. UTC | #10
Hi Johannes,

On 8/16/22 11:31 AM, Johannes Schindelin wrote:

>> $ git merge - old-branch
>> merge: - - not something we can merge
> 
> This is confusing me: how is the patch supporting `git branch -d -`
> aligned with the presented `git merge` invocations?

"merge" supports multiple objects to be specified, but "-" only is 
accepted if just one argument is specified, as Junio did it in:

commit 4e8115fff135a09f75020083f51722e7e35eb6e9
Author: Junio C Hamano <gitster@pobox.com>
Date:   Thu Apr 7 15:57:57 2011 -0700

     merge: allow "-" as a short-hand for "previous branch"

     Just like "git checkout -" is a short-hand for "git checkout @{-1}" to
     conveniently switch back to the previous branch, "git merge -" is a
     short-hand for "git merge @{-1}" to conveniently merge the previous 
branch.

     It will allow me to say:

         $ git checkout -b au/topic
         $ git am -s ./+au-topic.mbox
         $ git checkout pu
         $ git merge -

     which is an extremely typical and repetitive operation during my 
git day.

     Signed-off-by: Junio C Hamano <gitster@pobox.com>

diff --git a/builtin/merge.c b/builtin/merge.c
index d54e7ddbb1..0bdd19a137 100644
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@ -1062,9 +1062,12 @@ int cmd_merge(int argc, const char **argv, const 
char *prefix)
         if (!allow_fast_forward && fast_forward_only)
                 die(_("You cannot combine --no-ff with --ff-only."));

-       if (!argc && !abort_current_merge && default_to_upstream)
-               argc = setup_with_upstream(&argv);
-
+       if (!abort_current_merge) {
+               if (!argc && default_to_upstream)
+                       argc = setup_with_upstream(&argv);
+               else if (argc == 1 && !strcmp(argv[0], "-"))
+                       argv[0] = "@{-1}";
+       }
         if (!argc)
                 usage_with_options(builtin_merge_usage,
                         builtin_merge_options);



So I aligned "branch -d" (or "delete-branch") with that.

The other two commands that already support "-", also works the same way:

$ git checkout -B - default
fatal: '-' is not a valid branch name

$ git rebase default -
fatal: no such branch/commit '-'

To summarize, my goal is to allow:

$ git checkout work_to_review
$ git checkout -
$ git merge - # or git rebase -
$ git branch -d -

Makes sense to me...

I've updated the commit message with a more specific message and removed 
the braces, jic.
Johannes Schindelin Aug. 19, 2022, 11:42 a.m. UTC | #11
Hi Rubén,

On Tue, 16 Aug 2022, Rubén Justo wrote:

> On 8/16/22 11:31 AM, Johannes Schindelin wrote:
>
> > > $ git merge - old-branch
> > > merge: - - not something we can merge
> >
> > This is confusing me: how is the patch supporting `git branch -d -`
> > aligned with the presented `git merge` invocations?
>
> "merge" supports multiple objects to be specified, but "-" only is accepted if
> just one argument is specified, as Junio did it in:
>
> commit 4e8115fff135a09f75020083f51722e7e35eb6e9
> Author: Junio C Hamano <gitster@pobox.com>
> Date:   Thu Apr 7 15:57:57 2011 -0700
>
>     merge: allow "-" as a short-hand for "previous branch"
>
>     Just like "git checkout -" is a short-hand for "git checkout @{-1}" to
>     conveniently switch back to the previous branch, "git merge -" is a
>     short-hand for "git merge @{-1}" to conveniently merge the previous
> branch.
>
>     It will allow me to say:
>
>         $ git checkout -b au/topic
>         $ git am -s ./+au-topic.mbox
>         $ git checkout pu
>         $ git merge -
>
>     which is an extremely typical and repetitive operation during my git day.
>
>     Signed-off-by: Junio C Hamano <gitster@pobox.com>
>
> diff --git a/builtin/merge.c b/builtin/merge.c
> index d54e7ddbb1..0bdd19a137 100644
> --- a/builtin/merge.c
> +++ b/builtin/merge.c
> @@ -1062,9 +1062,12 @@ int cmd_merge(int argc, const char **argv, const char
> *prefix)
>         if (!allow_fast_forward && fast_forward_only)
>                 die(_("You cannot combine --no-ff with --ff-only."));
>
> -       if (!argc && !abort_current_merge && default_to_upstream)
> -               argc = setup_with_upstream(&argv);
> -
> +       if (!abort_current_merge) {
> +               if (!argc && default_to_upstream)
> +                       argc = setup_with_upstream(&argv);
> +               else if (argc == 1 && !strcmp(argv[0], "-"))
> +                       argv[0] = "@{-1}";
> +       }
>         if (!argc)
>                 usage_with_options(builtin_merge_usage,
>                         builtin_merge_options);

Ah, the vagaries of being a maintainer and everybody following your lead,
even if you have a bad day and are grumpy, or as in this case just want to
get a quick fix in that supports your workflow better, and then move on.

If you read the commit message carefully, you will note that there is no
justification for restricting it to the `argc == 1` case.

I assume that the implicit rationale is that it was just simpler to do it
this way.

The alternative would have been to modify `collect_parents()`, or even
`get_merge_parent()` (which has many more callers), and at some stage the
investigation would have been as involved as it will be in this here
thread.

However, it is one thing to integrate such a patch as a one-off, or do it
two times, or three.

It is another thing to do this again and again and again and seeing that
we're not getting anywhere and only piling hack upon hack.

It is this latter stage that we have arrived at.

> So I aligned "branch -d" (or "delete-branch") with that.
>
> The other two commands that already support "-", also works the same way:
>
> $ git checkout -B - default
> fatal: '-' is not a valid branch name
>
> $ git rebase default -
> fatal: no such branch/commit '-'
>
> To summarize, my goal is to allow:
>
> $ git checkout work_to_review
> $ git checkout -
> $ git merge - # or git rebase -
> $ git branch -d -
>
> Makes sense to me...

There are different qualities at play with these commands, though. `git
checkout` cannot support more than a single revision argument. With `git
merge`, technically we do support more than a single revision argument
(via octopus merges), but support for it is limited (for example, we do
not even support recursive octopus merges). You might say that it is
discouraged to call `git merge` with more than one revision argument.

With `git branch -d` or with `git branch --list`, we are in a different
league. Those commands are commonly called with more than just a single
branch name.

And then there are the other commands that would benefit from support for
`-` and that accept many more than one revision argument, too, such as
`log`, `rev-parse`, `merge-base`, etc.

Sure, we can accept one more one-off hack to support a single `-` argument
to refer to the previous branch. The sum of those hacks, however, becomes
a burden.

Ciao,
Dscho
Johannes Schindelin Aug. 19, 2022, 1:05 p.m. UTC | #12
Hi Junio & Rubén,

On Tue, 16 Aug 2022, Johannes Schindelin wrote:

> On Mon, 8 Aug 2022, Junio C Hamano wrote:
>
> > Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
> >
> > > @@ -1420,6 +1420,12 @@ static int interpret_nth_prior_checkout(struct repository *r,
> > >  	const char *brace;
> > >  	char *num_end;
> > >
> > > +	if (namelen == 1 && *name == '-') {
> > > +		brace = name;
> > > +		nth = 1;
> > > +		goto find_nth_checkout;
> > > +	}
> > > +
> > >  	if (namelen < 4)
> > >  		return -1;
> > >  	if (name[0] != '@' || name[1] != '{' || name[2] != '-')
> >
> > If a solution along this line works, it would be far cleaner design
> > than the various hacks we have done in the past, noticing "-" and
> > replacing with "@{-1}".
>
> Indeed, but it does not work as-is: `interpret_nth_prior_checkout()` is
> used on prefixes of a rev, and for the special handling of `-` we cannot
> have that.
>
> [...]
>
> One thing we could do, however, would be to patch only
> `repo_interpret_branch_name()`, i.e. only allow `-` to imply the
> previous branch name in invocations where a branch name is asked for
> _explicitly_. I.e. not any random revision, but specifically a branch
> name.

This patch does that:

-- snip --
diff --git a/object-name.c b/object-name.c
index 4d2746574cd..a2732be3b71 100644
--- a/object-name.c
+++ b/object-name.c
@@ -1616,6 +1616,20 @@ int repo_interpret_branch_name(struct repository *r,
 	if (!namelen)
 		namelen = strlen(name);

+	if (namelen == 1 && *name == '-') {
+		struct grab_nth_branch_switch_cbdata cb = {
+			.remaining = 1,
+			.sb = buf
+		};
+
+		if (refs_for_each_reflog_ent_reverse(get_main_ref_store(r),
+						     "HEAD",
+						     grab_nth_branch_switch,
+						     &cb) <= 0)
+			return -1;
+		return namelen;
+	}
+
 	if (!options->allowed || (options->allowed & INTERPRET_BRANCH_LOCAL)) {
 		len = interpret_nth_prior_checkout(r, name, namelen, buf);
 		if (!len) {
diff --git a/t/t3204-branch-name-interpretation.sh b/t/t3204-branch-name-interpretation.sh
index 993a6b5eff7..5acbef95913 100755
--- a/t/t3204-branch-name-interpretation.sh
+++ b/t/t3204-branch-name-interpretation.sh
@@ -67,6 +67,22 @@ test_expect_success 'delete branch via @{-1}' '
 	expect_deleted previous-del
 '

+test_expect_success 'delete branch via -' '
+	git checkout -b previous-del &&
+	git checkout - &&
+
+	git branch -d - &&
+	expect_deleted previous-del &&
+
+	git branch previous-del2 &&
+	git checkout -b previous-del &&
+	git checkout - &&
+
+	git branch -d previous-del2 - &&
+	expect_deleted previous-del &&
+	expect_deleted previous-del2
+'
+
 test_expect_success 'delete branch via local @{upstream}' '
 	git branch local-del &&
 	git branch --set-upstream-to=local-del &&
-- snap --

This adds support for things like `git branch -d -`, and it even verifies
that that works.

What does not work with this patch is `git show -`. I am not sure whether
we want to make that work, although I have to admit that I would use it.
Often. And this patch would make it work, the test suite even passes with
it:

-- snip --
diff --git a/revision.c b/revision.c
index f4eee11cc8b..207b554aef1 100644
--- a/revision.c
+++ b/revision.c
@@ -2802,7 +2802,7 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s
 		revarg_opt |= REVARG_CANNOT_BE_FILENAME;
 	for (left = i = 1; i < argc; i++) {
 		const char *arg = argv[i];
-		if (!seen_end_of_options && *arg == '-') {
+		if (!seen_end_of_options && *arg == '-' && arg[1]) {
 			int opts;

 			opts = handle_revision_pseudo_opt(
-- snap --

That latter patch obviously needs at least one accompanying test case, and
the patch series would then need to drop the special-casings we already
have for "-".

Rubén, do you want to take this a bit further?

Ciao,
Dscho

P.S.: For convenient fetching, I opened a draft PR here:
https://github.com/gitgitgadget/git/pull/1329
Junio C Hamano Aug. 19, 2022, 6:11 p.m. UTC | #13
Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:

> This patch does that:
>
> -- snip --
> diff --git a/object-name.c b/object-name.c
> index 4d2746574cd..a2732be3b71 100644
> --- a/object-name.c
> +++ b/object-name.c
> @@ -1616,6 +1616,20 @@ int repo_interpret_branch_name(struct repository *r,
>  	if (!namelen)
>  		namelen = strlen(name);
>
> +	if (namelen == 1 && *name == '-') {
> +		struct grab_nth_branch_switch_cbdata cb = {
> +			.remaining = 1,
> +			.sb = buf
> +		};
> +
> +		if (refs_for_each_reflog_ent_reverse(get_main_ref_store(r),
> +						     "HEAD",
> +						     grab_nth_branch_switch,
> +						     &cb) <= 0)
> +			return -1;
> +		return namelen;
> +	}
> +

This is very reasonable.  

Anywhere we take '@{-1}', 'main', or 'js/dash-is-previous', nobody
would be surprised if we take '-' and interpreted as '@{-1}' in
addition.

However, as I said earlier, we have command line disambiguation that
wants to tell dashed options, revs, and paths apart.  We need to
find places that need adjusting and adjust them, *AND* then make
sure that such tweaks do not introduce unintended side effect.
These, especially the last one, take a careful work I would rather
not to see unexperienced developer perform alone and take the
finding by them blindly.

> What does not work with this patch is `git show -`. I am not sure whether
> we want to make that work, although I have to admit that I would use it.
> Often. And this patch would make it work, the test suite even passes with
> it:

Exactly my above point.  Nobody including our tests expect that a
single '-' to be taken as a rev when we disambiguate command line
arguments, so it is very unlikely that it is tested to ensure that a
single '-' ends the revs and is checked for its "path-ness".  It is
not surprising that the tests do not fail ;-).

> -- snip --
> diff --git a/revision.c b/revision.c
> index f4eee11cc8b..207b554aef1 100644
> --- a/revision.c
> +++ b/revision.c
> @@ -2802,7 +2802,7 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s
>  		revarg_opt |= REVARG_CANNOT_BE_FILENAME;
>  	for (left = i = 1; i < argc; i++) {
>  		const char *arg = argv[i];
> -		if (!seen_end_of_options && *arg == '-') {
> +		if (!seen_end_of_options && *arg == '-' && arg[1]) {
>  			int opts;
>
>  			opts = handle_revision_pseudo_opt(
> -- snap --

Thanks.  These two patch snippets in the message I am responding to
are promising and exciting.
Rubén Justo Aug. 25, 2022, 7:57 a.m. UTC | #14
Hi,

On 8/19/22 3:05 PM, Johannes Schindelin wrote:
> 
> Rubén, do you want to take this a bit further?
> 

Just wanted to delete the previous branch, I didn't want to enter in a 
deep change... but here we are :-)

Allow the "-" in setup_revisions:

diff --git a/revision.c b/revision.c
index f4eee11cc8..65e7eb85d8 100644
--- a/revision.c
+++ b/revision.c
@@ -2802,7 +2802,7 @@ int setup_revisions(int argc, const char **argv, 
struct rev_info *revs, struct s
                 revarg_opt |= REVARG_CANNOT_BE_FILENAME;
         for (left = i = 1; i < argc; i++) {
                 const char *arg = argv[i];
-               if (!seen_end_of_options && *arg == '-') {
+               if (!seen_end_of_options && *arg == '-' && 
!strchr(".^~:@", arg[1])) {
                         int opts;

Then, consider "-" as nth_prior, just like @{-1}:

diff --git a/object-name.c b/object-name.c
index 4d2746574c..87b4c33cce 100644
--- a/object-name.c
+++ b/object-name.c
@@ -934,6 +934,9 @@ static int get_oid_basic(struct repository *r, const 
char *str, int len,
                 }
         }

+        if ((len == 1) && (str[0] == '-'))
+                nth_prior = 1;
+
         /* Accept only unambiguous ref paths. */
         if (len && ambiguous_path(str, len))
                 return -1;

diff --git a/object-name.c b/object-name.c
index 4d2746574c..87b4c33cce 100644
--- a/object-name.c
+++ b/object-name.c
@@ -1420,18 +1423,24 @@ static int interpret_nth_prior_checkout(struct 
repository *r,
         const char *brace;
         char *num_end;

-       if (namelen < 4)
-               return -1;
-       if (name[0] != '@' || name[1] != '{' || name[2] != '-')
-               return -1;
-       brace = memchr(name, '}', namelen);
-       if (!brace)
-               return -1;
-       nth = strtol(name + 3, &num_end, 10);
-       if (num_end != brace)
-               return -1;
-       if (nth <= 0)
-               return -1;
+        if (name[0] == '-' && strchr(".^~:@", name[1])) {
+                nth = 1;
+                brace = name;
+        } else {
+                if (namelen < 4)
+                        return -1;
+                if (name[0] != '@' || name[1] != '{' || name[2] != '-')
+                        return -1;
+                brace = memchr(name, '}', namelen);
+                if (!brace)
+                        return -1;
+                nth = strtol(name + 3, &num_end, 10);
+                if (num_end != brace)
+                        return -1;
+                if (nth <= 0)
+                        return -1;
+        }
+
         cb.remaining = nth;

Two checks needs to be adjusted:

diff --git a/refs.c b/refs.c
index 90bcb27168..0ed9f99ccc 100644
--- a/refs.c
+++ b/refs.c
@@ -198,6 +198,11 @@ static int check_or_sanitize_refname(const char 
*refname, int flags,
                 else
                         return -1;
         }
+
+       if (component_len == 1 && refname[0] == '-') {
+                return -1;
+       }
+

diff --git a/object-name.c b/object-name.c
index 4d2746574c..87b4c33cce 100644
@@ -1684,7 +1693,7 @@ int strbuf_check_branch_ref(struct strbuf *sb, 
const char *name)
          */
         strbuf_splice(sb, 0, 0, "refs/heads/", 11);

-       if (*name == '-' ||
+       if ((*name == '-' && name[1]) ||
             !strcmp(sb->buf, "refs/heads/HEAD"))
                 return -1;


I know this changes open the possibility of having things like "-^2" or 
-@{yesterday} that you said was not desiable. But, why wouldn't we want 
that? Having parse_opt_result to handle that:

diff --git a/parse-options.c b/parse-options.c
index edf55d3ef5..2757bd94c1 100644
--- a/parse-options.c
+++ b/parse-options.c
@@ -740,7 +740,7 @@ enum parse_opt_result parse_options_step(struct 
parse_opt_ctx_t *ctx,
                     ctx->argc != ctx->total)
                         break;

-               if (*arg != '-' || !arg[1]) {
+               if (*arg != '-' || strchr(".^~:@", arg[1])) {
                         if (parse_nodash_opt(ctx, arg, options) == 0)
                                 continue;
                         if (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)


With this changes, all the current uses of "-", with the hacks already 
removed, keep working and fixes the weird cases:

$ git merge branch - other_branch
$ git branch -d branch - other_branch


Also, I've checked that work:
$ git diff -
$ git show -
$ git blame -
and branch -d :-)

I've checked the current tests and added new ones for this, all passes. ie:

diff --git a/t/t1505-rev-parse-last.sh b/t/t1505-rev-parse-last.sh
index 4a5758f08a..231457df50 100755
--- a/t/t1505-rev-parse-last.sh
+++ b/t/t1505-rev-parse-last.sh
@@ -40,10 +40,18 @@ test_expect_success '@{-1} works' '
         test_cmp_rev side @{-1}
  '

+test_expect_success '- works' '
+       test_cmp_rev side -
+'
+
  test_expect_success '@{-1}~2 works' '
         test_cmp_rev side~2 @{-1}~2
  '

+test_expect_success '-~2 works' '
+       test_cmp_rev side~2 -~2
+'
+
  test_expect_success '@{-1}^2 works' '
         test_cmp_rev side^2 @{-1}^2
  '

Still needs some work, for example: git log shows "-" in the warning:
$ ../git/git log "-@{10000 minutes ago}"
warning: log for '-' only goes back to Wed, 24 Aug 2022 14:16:17 +0200

What do you think, is it worth the change?

I've created a PR with all the changes and tests.
https://github.com/gitgitgadget/git/pull/1338

Thanks.
Junio C Hamano Aug. 25, 2022, 4:23 p.m. UTC | #15
Rubén Justo <rjusto@gmail.com> writes:

> struct rev_info *revs, struct s
>                 revarg_opt |= REVARG_CANNOT_BE_FILENAME;
>         for (left = i = 1; i < argc; i++) {
>                 const char *arg = argv[i];
> -               if (!seen_end_of_options && *arg == '-') {
> +               if (!seen_end_of_options && *arg == '-' &&
> !strchr(".^~:@", arg[1])) {

Yuck.  I really didn't want to see that strchr() or any other logic
that _knows_ what letter is allowed after a "rev".  It will be
impossible to keep it in sync with the real code paths that needs to
know and parses these syntactical constructs, and folks new to the
codebase will never be able to tell at a first glance if the above
is sufficient (or what the strchr() is doing).
Rubén Justo Aug. 25, 2022, 7:50 p.m. UTC | #16
On 8/25/22 6:23 PM, Junio C Hamano wrote:
> Rubén Justo <rjusto@gmail.com> writes:
> 
>> struct rev_info *revs, struct s
>>                  revarg_opt |= REVARG_CANNOT_BE_FILENAME;
>>          for (left = i = 1; i < argc; i++) {
>>                  const char *arg = argv[i];
>> -               if (!seen_end_of_options && *arg == '-') {
>> +               if (!seen_end_of_options && *arg == '-' &&
>> !strchr(".^~:@", arg[1])) {
> 
> Yuck.  I really didn't want to see that strchr() or any other logic
> that _knows_ what letter is allowed after a "rev".  It will be
> impossible to keep it in sync with the real code paths that needs to
> know and parses these syntactical constructs, and folks new to the
> codebase will never be able to tell at a first glance if the above
> is sufficient (or what the strchr() is doing).
> 

Some logic is needed to disambiguate from options. It is difficult than 
that set of chars changes, they are all around the code. And if any new 
is added should be reviewed and hopefully some test will be broken.

Maybe a more centralized approach?

diff --git a/parse-options.c b/parse-options.c
index 2757bd94c1..303854e8a4 100644
--- a/parse-options.c
+++ b/parse-options.c
@@ -740,7 +741,7 @@ enum parse_opt_result parse_options_step(struct 
parse_opt_ctx_t *ctx,
                     ctx->argc != ctx->total)
                         break;

-               if (*arg != '-' || strchr(".^~:@", arg[1])) {
+               if (*arg != '-' || 
!check_refchar_component_special(arg[1])) {
                         if (parse_nodash_opt(ctx, arg, options) == 0)
                                 continue;
                         if (ctx->flags & PARSE_OPT_STOP_AT_NON_OPTION)


diff --git a/refs.c b/refs.c
index 0ed9f99ccc..5327a8ec1f 100644
--- a/refs.c
+++ b/refs.c
@@ -159,6 +159,32 @@ static int check_refname_component(const char 
*refname, int *flags,
         return cp - refname;
  }

+int check_refchar_component_special(char refchar)
+{
+        int ch = refchar & 255;
+        unsigned char disp = refname_disposition[ch];
+
+        switch (disp) {
+        case 1:
+                /* end of component */
+                return 0;
+        case 2:
+                /* ".." components */
+                return 0;
+        case 3:
+                /* "@{" components */
+                return 0;
+        case 4:
+                /* forbidden char */
+                return 0;
+        case 5:
+                /* pattern */
+                return 0;
+        }
+
+        return -1;
+}
+
  static int check_or_sanitize_refname(const char *refname, int flags,
                                      struct strbuf *sanitized)
  {
diff mbox series

Patch

diff --git a/builtin/branch.c b/builtin/branch.c
index 55cd9a6e998..59c19f38d2e 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -241,6 +241,10 @@  static int delete_branches(int argc, const char **argv, int force, int kinds,
 			die(_("Couldn't look up commit object for HEAD"));
 	}
 
+	if ((argc == 1) && !strcmp(argv[0], "-")) {
+		argv[0] = "@{-1}";
+	}
+
 	for (i = 0; i < argc; i++, strbuf_reset(&bname)) {
 		char *target = NULL;
 		int flags = 0;