diff mbox series

[1/2] Teach send-pack & push to --send-up-to-date refs

Message ID SA1PR14MB469144B8903909F65493323A8D2B2@SA1PR14MB4691.namprd14.prod.outlook.com (mailing list archive)
State New
Headers show
Series Optionally support push options on up-to-date branches | expand

Commit Message

Christopher Lindee March 12, 2024, 9:55 p.m. UTC
Provide an option that forces the local Git transport to transmit a local
ref even when the receiving ref would not change (i.e. the local & remote
refs point to the same object).  This is not done normally, as no changes
would take place on the server in a vanilla Git setup.  However, some Git
servers support push options and those push options decide which branches
to operate on based on the refs that are transmitted alongside the option.

This option ensures the refs listed on the command line are always sent.

Signed-off-by: Christopher Lindee <christopher.lindee@webpros.com>
---
 Documentation/git-push.txt      | 8 +++++++-
 Documentation/git-send-pack.txt | 7 +++++++
 builtin/push.c                  | 1 +
 builtin/send-pack.c             | 4 ++++
 send-pack.c                     | 2 +-
 send-pack.h                     | 3 ++-
 transport-helper.c              | 7 ++++++-
 transport.c                     | 1 +
 transport.h                     | 1 +
 9 files changed, 30 insertions(+), 4 deletions(-)

Comments

Junio C Hamano March 13, 2024, 11:38 p.m. UTC | #1
Christopher Lindee <christopher.lindee@webpros.com> writes:

> Subject: Re: [PATCH 1/2] Teach send-pack & push to --send-up-to-date refs

cf. Documentation/SubmittingPatches:describe-changes.

Perhaps

	[PATCH 1/2] push: allow pushing a no-op update to refs

or something?

> Provide an option that forces the local Git transport to transmit a local
> ref even when the receiving ref would not change (i.e. the local & remote
> refs point to the same object).  This is not done normally, as no changes
> would take place on the server in a vanilla Git setup.  However, some Git
> servers support push options and those push options decide which branches
> to operate on based on the refs that are transmitted alongside the option.
> This option ensures the refs listed on the command line are always sent.

Flip the order of description to give your observation of the
current behaviour first, your perceived problem in it next, and then
finally your solution.  That way, those who are totally unfamiliar
with the problem area can just read from start to end straight, and
those who know what we do currently can skip and start from your
problem description.

Also, it would be nice if you throw in the issue about missing ref
in the push certificate, which I mentioned in my response to the
cover letter.

>  Documentation/git-push.txt      | 8 +++++++-
>  Documentation/git-send-pack.txt | 7 +++++++
>  builtin/push.c                  | 1 +
>  builtin/send-pack.c             | 4 ++++
>  send-pack.c                     | 2 +-
>  send-pack.h                     | 3 ++-
>  transport-helper.c              | 7 ++++++-
>  transport.c                     | 1 +
>  transport.h                     | 1 +
>  9 files changed, 30 insertions(+), 4 deletions(-)

No tests?

>  'git push' [--all | --branches | --mirror | --tags] [--follow-tags] [--atomic] [-n | --dry-run] [--receive-pack=<git-receive-pack>]
>  	   [--repo=<repository>] [-f | --force] [-d | --delete] [--prune] [-q | --quiet] [-v | --verbose]
> -	   [-u | --set-upstream] [-o <string> | --push-option=<string>]
> +	   [-u | --set-upstream] [-o <string> | --push-option=<string>] [--send-up-to-date]

I do not know if the new option is a name that is easy to
understand.  I am not great at naming, either but how does
"--force-no-op" sound?

> +	Usually, the command will not send a local ref when the remote ref
> +	already matches, as no change would occur if it did.  This flag

In the context of "push", 'match' is a verb that is used in
different contexts, like "'git push master' finds a ref that matches
'master' and updates refs/heads/master".  You would want to avoid it
when you can.

    Usually the command omits instructing the receiving end to
    update a ref to point at an object, if the target ref points at
    the exact object already, as no change ...

> +	disables that check.  This allows push options to be sent alongside
> +	up-to-date references on the remote.

Aside from options and push certificates, there may be other side
effects from this change.  I am not sure if we want to make sure we
enumerate the benefit all like this.  Perhaps drop "This allows ..."
altogether?

> diff --git a/send-pack.c b/send-pack.c
> index 37f59d4f66..30b0f2f276 100644
> --- a/send-pack.c
> +++ b/send-pack.c
> @@ -313,7 +313,7 @@ static int check_to_send_update(const struct ref *ref, const struct send_pack_ar
>  	case REF_STATUS_REJECT_NODELETE:
>  		return CHECK_REF_STATUS_REJECTED;
>  	case REF_STATUS_UPTODATE:
> -		return CHECK_REF_UPTODATE;
> +		return args->send_uptodate ? 0 : CHECK_REF_UPTODATE;
>  
>  	default:
>  	case REF_STATUS_EXPECTING_REPORT:

Given the existing flow of this code, I would prefer to see it
written more like so:

diff --git i/send-pack.c w/send-pack.c
index 37f59d4f66..97d01726bb 100644
--- i/send-pack.c
+++ w/send-pack.c
@@ -313,8 +313,9 @@ static int check_to_send_update(const struct ref *ref, const struct send_pack_ar
 	case REF_STATUS_REJECT_NODELETE:
 		return CHECK_REF_STATUS_REJECTED;
 	case REF_STATUS_UPTODATE:
-		return CHECK_REF_UPTODATE;
-
+		if (!args->send_uptodate)
+			return CHECK_REF_UPTODATE;
+		/* fallthru */
 	default:
 	case REF_STATUS_EXPECTING_REPORT:
 		/* already passed checks on the local side */

to make it clear that the caller gets 0 ("go ahead and do it")
unless it is one of the cases explicitly listed abouve the "default"
label.

> +	int do_nop = flags & TRANSPORT_PUSH_SEND_UPTODATE;

Here we do call it "do_nop", showing that at least to you, "nop" is
a much more fitting word than "uptodate" for what we are trying to
achieve in this topic.  It would give us one piece of good input to
decide what the end-user facing name should be.  In fact it is where
I took inspiration from for "force-noop" I mentioned earlier.

> @@ -1010,7 +1011,11 @@ static int push_refs_with_push(struct transport *transport,
>  			} else
>  				continue;
>  		case REF_STATUS_UPTODATE:
> -			continue;
> +			if (do_nop) {
> +				; /* do nothing */
> +			}
> +			else
> +				continue;

Drop needless braces around a single-statement block.
Alternatively, we could write it like so:

			if (!do_nop)
				continue;
			/* fallthru */

but I think what you wrote, modulo the unnecessary braces, makes the
intent a bit more clear.

>  		default:
>  			; /* do nothing */
>  		}
Christopher Lindee March 15, 2024, 8:18 a.m. UTC | #2
Junio C Hamano <gitster@pobox.com> writes:

> Christopher Lindee <christopher.lindee@webpros.com> writes:
> 
> > Subject: Re: [PATCH 1/2] Teach send-pack & push to --send-up-to-date refs
> 
> cf. Documentation/SubmittingPatches:describe-changes.
> 
> Perhaps
> 
>         [PATCH 1/2] push: allow pushing a no-op update to refs
> 
> or something?

Thanks!  I will change the commit message.

> > Provide an option that forces the local Git transport to transmit a local
> > ref even when the receiving ref would not change (i.e. the local & remote
> > refs point to the same object).  This is not done normally, as no changes
> > would take place on the server in a vanilla Git setup.  However, some Git
> > servers support push options and those push options decide which branches
> > to operate on based on the refs that are transmitted alongside the option.
> > This option ensures the refs listed on the command line are always sent.
> 
> Flip the order of description to give your observation of the
> current behaviour first, your perceived problem in it next, and then
> finally your solution.  That way, those who are totally unfamiliar
> with the problem area can just read from start to end straight, and
> those who know what we do currently can skip and start from your
> problem description.

Can do.  I wonder if the new message will come out justified too.

> Also, it would be nice if you throw in the issue about missing ref
> in the push certificate, which I mentioned in my response to the
> cover letter.

I'd like to get a better understanding of the feature before I comment
on push certificates.  I'll read up and see if I can include it.

> >  Documentation/git-push.txt      | 8 +++++++-
> >  Documentation/git-send-pack.txt | 7 +++++++
> >  builtin/push.c                  | 1 +
> >  builtin/send-pack.c             | 4 ++++
> >  send-pack.c                     | 2 +-
> >  send-pack.h                     | 3 ++-
> >  transport-helper.c              | 7 ++++++-
> >  transport.c                     | 1 +
> >  transport.h                     | 1 +
> >  9 files changed, 30 insertions(+), 4 deletions(-)
> 
> No tests?

Only manual.  I'm happy to add automated tests; can you recommend a
t####?

I was also hesitant since I did not know what to expect and thought a
wholesale redesign might be required; v2 will have less uncertainty.

> >  'git push' [--all | --branches | --mirror | --tags] [--follow-tags] [--atomic] [-n | --dry-run] [--receive-pack=<git-receive-pack>]
> >          [--repo=<repository>] [-f | --force] [-d | --delete] [--prune] [-q | --quiet] [-v | --verbose]
> > -        [-u | --set-upstream] [-o <string> | --push-option=<string>]
> > +        [-u | --set-upstream] [-o <string> | --push-option=<string>] [--send-up-to-date]
> 
> I do not know if the new option is a name that is easy to
> understand.  I am not great at naming, either but how does
> "--force-no-op" sound?

I already gave a response in a reply to the cover letter, but I also
have some new thoughts:

Is this something that can be easy to understand?  It seems to require
some knowledge of transports and server behavior.  Moreover, with the
server-side implications mentioned by brian, we should encourage a
deeper understanding before people use this option.  In that case,
perhaps a simple name is not as valuable.

How do people feel about --aggressive? ;)

To discourage unconsidered use, we could go with a scary option name,
like --disable-noop-optimizations, though that particular name has the
unfortunate American vs. British spelling issue.  It does exactly what
it says on the tinĀ® though, which is nice.

See the cover letter reply for other suggestions.

> > +     Usually, the command will not send a local ref when the remote ref
> > +     already matches, as no change would occur if it did.  This flag
> 
> In the context of "push", 'match' is a verb that is used in
> different contexts, like "'git push master' finds a ref that matches
> 'master' and updates refs/heads/master".  You would want to avoid it
> when you can.
> 
>     Usually the command omits instructing the receiving end to
>     update a ref to point at an object, if the target ref points at
>     the exact object already, as no change ...

Good to know.  I will avoid overloading the term.

> > +     disables that check.  This allows push options to be sent alongside
> > +     up-to-date references on the remote.
> 
> Aside from options and push certificates, there may be other side
> effects from this change.  I am not sure if we want to make sure we
> enumerate the benefit all like this.  Perhaps drop "This allows ..."
> altogether?

I think we both agree this is a option that is difficult to convey, so
much so that I think we need examples.  How obvious would it be that I
wanted to use this for push options if I didn't explicitly mention it?

But I also think you're right.  There are a decent number of things to
consider before using this.  Perhaps a fuller discussion on the option
- with examples - is warranted in a more deep-dive document and we can
refer the user to that document here.  If so, would any existing pages
be appropriate?

> > diff --git a/send-pack.c b/send-pack.c
> > index 37f59d4f66..30b0f2f276 100644
> > --- a/send-pack.c
> > +++ b/send-pack.c
> > @@ -313,7 +313,7 @@ static int check_to_send_update(const struct ref *ref, const struct send_pack_ar
> >       case REF_STATUS_REJECT_NODELETE:
> >               return CHECK_REF_STATUS_REJECTED;
> >       case REF_STATUS_UPTODATE:
> > -             return CHECK_REF_UPTODATE;
> > +             return args->send_uptodate ? 0 : CHECK_REF_UPTODATE;
> >
> >       default:
> >       case REF_STATUS_EXPECTING_REPORT:
> 
> Given the existing flow of this code, I would prefer to see it
> written more like so:
> 
> diff --git i/send-pack.c w/send-pack.c
> index 37f59d4f66..97d01726bb 100644
> --- i/send-pack.c
> +++ w/send-pack.c
> @@ -313,8 +313,9 @@ static int check_to_send_update(const struct ref *ref, const struct send_pack_ar
>         case REF_STATUS_REJECT_NODELETE:
>                 return CHECK_REF_STATUS_REJECTED;
>         case REF_STATUS_UPTODATE:
> -               return CHECK_REF_UPTODATE;
> -
> +               if (!args->send_uptodate)
> +                       return CHECK_REF_UPTODATE;
> +               /* fallthru */
>         default:
>         case REF_STATUS_EXPECTING_REPORT:
>                 /* already passed checks on the local side */
> 
> to make it clear that the caller gets 0 ("go ahead and do it")
> unless it is one of the cases explicitly listed abouve the "default"
> label.

Nice; will do.

> > +     int do_nop = flags & TRANSPORT_PUSH_SEND_UPTODATE;
> 
> Here we do call it "do_nop", showing that at least to you, "nop" is
> a much more fitting word than "uptodate" for what we are trying to
> achieve in this topic.  It would give us one piece of good input to
> decide what the end-user facing name should be.  In fact it is where
> I took inspiration from for "force-noop" I mentioned earlier.
>
> > @@ -1010,7 +1011,11 @@ static int push_refs_with_push(struct transport *transport,
> >                       } else
> >                               continue;
> >               case REF_STATUS_UPTODATE:
> > -                     continue;
> > +                     if (do_nop) {
> > +                             ; /* do nothing */
> > +                     }
> > +                     else
> > +                             continue;
> 
> Drop needless braces around a single-statement block.
> Alternatively, we could write it like so:
> 
>                         if (!do_nop)
>                                 continue;
>                         /* fallthru */
> 
> but I think what you wrote, modulo the unnecessary braces, makes the
> intent a bit more clear.
> 
> >               default:
> >                       ; /* do nothing */
> >               }

I was having trouble coming up with a good name when developing this,
so I went with something that was 6 characters long.  I'm open to
identifier suggestions that might be easier to read.

Thanks,
Chris.
diff mbox series

Patch

diff --git a/Documentation/git-push.txt b/Documentation/git-push.txt
index 9b7cfbc5c1..44d1e04931 100644
--- a/Documentation/git-push.txt
+++ b/Documentation/git-push.txt
@@ -11,7 +11,7 @@  SYNOPSIS
 [verse]
 'git push' [--all | --branches | --mirror | --tags] [--follow-tags] [--atomic] [-n | --dry-run] [--receive-pack=<git-receive-pack>]
 	   [--repo=<repository>] [-f | --force] [-d | --delete] [--prune] [-q | --quiet] [-v | --verbose]
-	   [-u | --set-upstream] [-o <string> | --push-option=<string>]
+	   [-u | --set-upstream] [-o <string> | --push-option=<string>] [--send-up-to-date]
 	   [--[no-]signed|--signed=(true|false|if-asked)]
 	   [--force-with-lease[=<refname>[:<expect>]] [--force-if-includes]]
 	   [--no-verify] [<repository> [<refspec>...]]
@@ -225,6 +225,12 @@  already exists on the remote side.
 	line, the values of configuration variable `push.pushOption`
 	are used instead.
 
+--send-up-to-date::
+	Usually, the command will not send a local ref when the remote ref
+	already matches, as no change would occur if it did.  This flag
+	disables that check.  This allows push options to be sent alongside
+	up-to-date references on the remote.
+
 --receive-pack=<git-receive-pack>::
 --exec=<git-receive-pack>::
 	Path to the 'git-receive-pack' program on the remote
diff --git a/Documentation/git-send-pack.txt b/Documentation/git-send-pack.txt
index b9e73f2e77..2517d226d0 100644
--- a/Documentation/git-send-pack.txt
+++ b/Documentation/git-send-pack.txt
@@ -13,6 +13,7 @@  SYNOPSIS
 		[--receive-pack=<git-receive-pack>]
 		[--verbose] [--thin] [--atomic]
 		[--[no-]signed | --signed=(true|false|if-asked)]
+		[--send-up-to-date]
 		[<host>:]<directory> (--all | <ref>...)
 
 DESCRIPTION
@@ -88,6 +89,12 @@  be in a separate packet, and the list must end with a flush packet.
 	options, error out.  See linkgit:git-push[1] and
 	linkgit:githooks[5] for details.
 
+--send-up-to-date::
+	Usually, the command will not send a local ref when the remote ref
+	already matches, as no change would occur if it did.  This flag
+	disables that check.  This allows push options to be sent alongside
+	up-to-date references on the remote.
+
 <host>::
 	A remote host to house the repository.  When this
 	part is specified, 'git-receive-pack' is invoked via
diff --git a/builtin/push.c b/builtin/push.c
index 2fbb31c3ad..555e8b53fe 100644
--- a/builtin/push.c
+++ b/builtin/push.c
@@ -617,6 +617,7 @@  int cmd_push(int argc, const char **argv, const char *prefix)
 		OPT_BIT(0, "no-verify", &flags, N_("bypass pre-push hook"), TRANSPORT_PUSH_NO_HOOK),
 		OPT_BIT(0, "follow-tags", &flags, N_("push missing but relevant tags"),
 			TRANSPORT_PUSH_FOLLOW_TAGS),
+		OPT_BIT(0, "send-up-to-date", &flags, N_("send refs when there is no change"), TRANSPORT_PUSH_SEND_UPTODATE),
 		OPT_CALLBACK_F(0, "signed", &push_cert, "(yes|no|if-asked)", N_("GPG sign the push"),
 				PARSE_OPT_OPTARG, option_parse_push_signed),
 		OPT_BIT(0, "atomic", &flags, N_("request atomic transaction on remote side"), TRANSPORT_PUSH_ATOMIC),
diff --git a/builtin/send-pack.c b/builtin/send-pack.c
index 3df9eaad09..da6630fa12 100644
--- a/builtin/send-pack.c
+++ b/builtin/send-pack.c
@@ -19,6 +19,7 @@  static const char * const send_pack_usage[] = {
 	   "              [--receive-pack=<git-receive-pack>]\n"
 	   "              [--verbose] [--thin] [--atomic]\n"
 	   "              [--[no-]signed | --signed=(true|false|if-asked)]\n"
+	   "              [--send-up-to-date]\n"
 	   "              [<host>:]<directory> (--all | <ref>...)"),
 	NULL,
 };
@@ -165,6 +166,7 @@  int cmd_send_pack(int argc, const char **argv, const char *prefix)
 	const char *receivepack = "git-receive-pack";
 	unsigned dry_run = 0;
 	unsigned send_mirror = 0;
+	unsigned send_uptodate = 0;
 	unsigned force_update = 0;
 	unsigned quiet = 0;
 	int push_cert = 0;
@@ -200,6 +202,7 @@  int cmd_send_pack(int argc, const char **argv, const char *prefix)
 		OPT_BOOL(0, "stateless-rpc", &stateless_rpc, N_("use stateless RPC protocol")),
 		OPT_BOOL(0, "stdin", &from_stdin, N_("read refs from stdin")),
 		OPT_BOOL(0, "helper-status", &helper_status, N_("print status from remote helper")),
+		OPT_BOOL(0, "send-up-to-date", &send_uptodate, N_("send refs when there is no change")),
 		OPT_CALLBACK_F(0, "force-with-lease", &cas, N_("<refname>:<expect>"),
 		  N_("require old value of ref to be at this value"),
 		  PARSE_OPT_OPTARG, parseopt_push_cas_option),
@@ -221,6 +224,7 @@  int cmd_send_pack(int argc, const char **argv, const char *prefix)
 	args.verbose = verbose;
 	args.dry_run = dry_run;
 	args.send_mirror = send_mirror;
+	args.send_uptodate = send_uptodate;
 	args.force_update = force_update;
 	args.quiet = quiet;
 	args.push_cert = push_cert;
diff --git a/send-pack.c b/send-pack.c
index 37f59d4f66..30b0f2f276 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -313,7 +313,7 @@  static int check_to_send_update(const struct ref *ref, const struct send_pack_ar
 	case REF_STATUS_REJECT_NODELETE:
 		return CHECK_REF_STATUS_REJECTED;
 	case REF_STATUS_UPTODATE:
-		return CHECK_REF_UPTODATE;
+		return args->send_uptodate ? 0 : CHECK_REF_UPTODATE;
 
 	default:
 	case REF_STATUS_EXPECTING_REPORT:
diff --git a/send-pack.h b/send-pack.h
index 7edb80596c..241b4278bc 100644
--- a/send-pack.h
+++ b/send-pack.h
@@ -27,7 +27,8 @@  struct send_pack_args {
 		push_cert:2,
 		stateless_rpc:1,
 		atomic:1,
-		disable_bitmaps:1;
+		disable_bitmaps:1,
+		send_uptodate:1;
 	const struct string_list *push_options;
 };
 
diff --git a/transport-helper.c b/transport-helper.c
index dd6002b393..115f46f6b8 100644
--- a/transport-helper.c
+++ b/transport-helper.c
@@ -983,6 +983,7 @@  static int push_refs_with_push(struct transport *transport,
 	int force_all = flags & TRANSPORT_PUSH_FORCE;
 	int mirror = flags & TRANSPORT_PUSH_MIRROR;
 	int atomic = flags & TRANSPORT_PUSH_ATOMIC;
+	int do_nop = flags & TRANSPORT_PUSH_SEND_UPTODATE;
 	struct helper_data *data = transport->data;
 	struct strbuf buf = STRBUF_INIT;
 	struct ref *ref;
@@ -1010,7 +1011,11 @@  static int push_refs_with_push(struct transport *transport,
 			} else
 				continue;
 		case REF_STATUS_UPTODATE:
-			continue;
+			if (do_nop) {
+				; /* do nothing */
+			}
+			else
+				continue;
 		default:
 			; /* do nothing */
 		}
diff --git a/transport.c b/transport.c
index df518ead70..84deadd2b6 100644
--- a/transport.c
+++ b/transport.c
@@ -867,6 +867,7 @@  static int git_transport_push(struct transport *transport, struct ref *remote_re
 
 	memset(&args, 0, sizeof(args));
 	args.send_mirror = !!(flags & TRANSPORT_PUSH_MIRROR);
+	args.send_uptodate = !!(flags & TRANSPORT_PUSH_SEND_UPTODATE);
 	args.force_update = !!(flags & TRANSPORT_PUSH_FORCE);
 	args.use_thin_pack = data->options.thin;
 	args.verbose = (transport->verbose > 0);
diff --git a/transport.h b/transport.h
index 6393cd9823..61a20e84f0 100644
--- a/transport.h
+++ b/transport.h
@@ -158,6 +158,7 @@  struct transport {
 #define TRANSPORT_RECURSE_SUBMODULES_ONLY	(1<<15)
 #define TRANSPORT_PUSH_FORCE_IF_INCLUDES	(1<<16)
 #define TRANSPORT_PUSH_AUTO_UPSTREAM		(1<<17)
+#define TRANSPORT_PUSH_SEND_UPTODATE		(1<<18)
 
 int transport_summary_width(const struct ref *refs);