-
Notifications
You must be signed in to change notification settings - Fork 803
Support pushing to an anonymous remote #1512
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR enables pushing to an anonymous remote by updating the PushOptions validation, ensuring that anonymous remotes use the dedicated constant, and avoiding remote-tracking reference updates.
- Update PushOptions.Validate() to set RemoteName to the anonymous constant when RemoteURL is provided.
- Ensure anonymous remotes skip updating remote-tracking references during push.
- Add tests to verify correct behavior of anonymous remotes.
Reviewed Changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.
Show a summary per file
File | Description |
---|---|
repository_test.go | Added tests for anonymous remote and push verification. |
repository.go | Updated CreateRemoteAnonymous and PushContext to use the anonymous constant. |
remote.go | Introduced IsAnonymous method and conditional remote reference update. |
options.go | Modified Validate() to assign anonymous remote when applicable. |
config/config_test.go | Added tests for anonymous remote config validations. |
config/config.go | Added AnonymousRemoteName constant and helper functions for anonymous remotes. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @mhagger, thanks for looking into this and for the detailed description - that's much appreciated.
The main change I'd suggest for your PR is to avoid creating the arbitrary Remote name. Instead, we could set the RemoteName to the default in the PushContext
, whenever both pushOptions.URL
and pushOptions.RemoteName
are empty.
That way, later down the execution path we will always have a Remote name set, unless we are handling an anonymous push. WDYT?
Also, do you mind targeting the v6-transport
branch instead? We are working transport related work there which will be part of our v6
release.
dd7b094
to
ca6457f
Compare
@pjbgf Thanks for your quick reply. I rebased this branch onto the I'm not exactly sure what you mean by "avoid creating the arbitrary Remote name". Are you suggesting that anonymous remotes should have the empty string as their name? That's doable, though it would require the following changes:
If we go that route, then for backwards compatibility, one could imagine having
If that's what you want, please confirm and let me know if you have a preference for one of the above options, then I'll make the change. If you had something else in mind, please clarify. |
@mhagger thanks for rebasing this. 🙇
Yes. I added a few comments below with the changes we may need to do to achieve this.
Given that we are already working on the new V6 API, we can remove this method altogether. Let's align with upstream UX, and handle this internally at the point of use (e.g. at a Push operation for example).
Upstream this doesn't really happen, as adhoc remotes are not really listed. I think we can either leave it as empty, or show something that is not a valid Please squash the changes into 1-2 commits. For example, one for removing the concept of "anonymous" remotes, and another to handle non-persistent remotes. |
config/config.go
Outdated
func NewAnonymousRemoteConfig(url string) *RemoteConfig { | ||
return &RemoteConfig{ | ||
Name: AnonymousRemoteName, | ||
URLs: []string{url}, | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
func NewAnonymousRemoteConfig(url string) *RemoteConfig { | |
return &RemoteConfig{ | |
Name: AnonymousRemoteName, | |
URLs: []string{url}, | |
} | |
} |
options.go
Outdated
if o.RemoteURL != "" { | ||
o.RemoteName = config.AnonymousRemoteName | ||
} else { | ||
o.RemoteName = DefaultRemoteName | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To align with git, Validate
should error if both RemoteName
and RemoteURL
are set.
Then when o.RemoteName == ""
and o.RemoteURL == ""
we should set the RemoteName
to its default.
RemoteURL: url, | ||
}) | ||
s.NoError(err) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's confirm no new remote was added.
s.Len(c.Remotes, 1) |
repository.go
Outdated
var err error | ||
remote, err = r.CreateRemoteAnonymous( | ||
config.NewAnonymousRemoteConfig(o.RemoteURL), | ||
) | ||
if err != nil { | ||
return err | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can create an ephemeral Remote
directly via:
var err error | |
remote, err = r.CreateRemoteAnonymous( | |
config.NewAnonymousRemoteConfig(o.RemoteURL), | |
) | |
if err != nil { | |
return err | |
} | |
remote = NewRemote(r.Storer, &config.RemoteConfig{ | |
URLs: []string{o.RemoteURL}, | |
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, but that would mean that the RemoteConfig
doesn't go through any validation at all. Are you OK with that?
remote.go
Outdated
func (r *Remote) updateRemoteReferenceStorage( | ||
cmds []*packp.Command, | ||
) error { | ||
if r.IsAnonymous() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if r.IsAnonymous() { | |
if r.isEphemeral() { |
config/config_test.go
Outdated
@@ -288,6 +288,25 @@ func (s *ConfigSuite) TestValidateInvalidRemoteKey() { | |||
s.ErrorIs(config.Validate(), ErrInvalid) | |||
} | |||
|
|||
func (s *ConfigSuite) TestAnonymousRemoteConfig() { | |||
config := &RemoteConfig{ | |||
Name: "anonymous", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Name: "anonymous", | |
Name: "", |
config/config_test.go
Outdated
} | ||
|
||
s.NoError(config.Validate()) | ||
s.Equal("anonymous", config.Name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s.Equal("anonymous", config.Name) | |
s.Empty(config.Name) |
URLs: []string{"http://foo/bar"}, | ||
} | ||
|
||
s.NoError(config.Validate()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Validate logic will have to be slightly amended for this to work.
remote.go
Outdated
// IsAnonymous returns true if the remote is anonymous; i.e., if it | ||
// was created directly from a URL and isn't defined in the git | ||
// config. | ||
func (r *Remote) IsAnonymous() bool { | ||
return r.c.IsAnonymous() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's avoid making this part of the public API for the time being. As for naming, I'm leaning to something that comes across as it being temporary/adhoc:
// IsAnonymous returns true if the remote is anonymous; i.e., if it | |
// was created directly from a URL and isn't defined in the git | |
// config. | |
func (r *Remote) IsAnonymous() bool { | |
return r.c.IsAnonymous() | |
} | |
// isEphemeral returns true if this is an adhoc remote; i.e., if it | |
// was created directly from a URL and isn't defined in the git | |
// config. | |
func (r *Remote) isEphemeral() bool { | |
return r.c.Name == "" | |
} |
repository.go
Outdated
// CreateRemoteAnonymous creates a new anonymous remote. c.Name must | ||
// be config.AnonymousRemoteName. It's used for the equivalent of | ||
// | ||
// git fetch git@github.com:src-d/go-git.git master:master |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that this branch is already on the new module version v6
, I think we can ditch the CreateRemoteAnonymous
method altogether.
My understanding is that upstream does not handle remotes named anonymous
any differently, so go-git
probably shouldn't as well.
It is equal to "anonymous", the `RemoteName` that is used for anonymous remotes (i.e., remotes that are specified using an explicit URL, rather than via gitconfig). It's about to get some more use, so create a named constant for it.
Add new methods to make it easy and less error-prone to determine whether a remote configuration or a remote is anonymous: * `Remote.IsAnonymous()` * `RemoteConfig.IsAnonymous()` These will be useful for the next changes.
Add a new convenience function, `NewAnonymousRemoteConfig()`, that makes an anonymous `RemoteConfig` given its URL (which is all that is needed for an anonymous remote). It will be used in a moment.
Git allows you to push to a remote that isn't defined in your gitconfig: git push https://example.com/foo.git refs/heads/bar:refs/heads/bar The effect of this command is to push to the remote repository at the specified URL, without updating any remote-tracking references corresponding to the reference that was pushed. It would seem that this should be possible with `go-git`, by writing something like r, _ := git.PlainOpen(".") _ = r.Push(&git.PushOptions{ RemoteURL: "https://example.com/foo.git", RefSpecs: []config.RefSpec{"refs/heads/bar:refs/heads/bar"}, }) But `go-git` wasn't handling this correctly: * `PushOptions.Validate()` saw the empty `o.RemoteName` and changed it to `DefaultRemoteName` ("origin"). * If no "origin" remote was defined in the gitconfig, `Repository.Push()` failed entirely with `ErrRemoteNotFound`. * If: an "origin" _was_ defined in the gitconfig, then after the push, remote-tracking references were created for the "origin" remote (e.g., `refs/remotes/origin/foo`), even though the push was probably to a remote that is not related to "origin". Alternatively, if the `PushOptions` were created using `RemoteName: "anonymous"`, the push still failed with `ErrRemoteNotFound` because a remote called "anonymous" was sought in the gitconfig, and that remote:(presumably!) doesn't exist. This commit fixes all three problems: * Change the behavior of `PushOptions.Validate()`: if there is a `RemoteURL` but no `RemoteName`, then set the `RemoteName` to `config.AnonymousRemoteName` rather than setting it to `DefaultRemoteName`. (If neither a `RemoteName` nor a `RemoteURL` is set, then set `RemoteName` to `DefaultRemoteName` as before.) * Change `Remote.updateRemoteReferenceStorage()` to skip updating any remote-tracking references if the remote is anonymous.
ca6457f
to
e9bb4b8
Compare
I just re-pushed the branch, rebased on the current v6-transport, with a fixup commit on top addressing most of your feedback. Once we are agreed on the approach I'll squash the PR down to one or two commits as you requested. The main open question for me is whether/when the |
@mhagger +1 on your suggestion. Let's move this to |
Git allows you to push to a remote that isn't defined in your gitconfig:
The effect of this command is to push to the remote repository at the specified URL, without updating any remote-tracking references corresponding to the reference that was pushed.
It would seem that this should be possible with
go-git
, by writing something likeBut
go-git
wasn't handling this correctly:PushOptions.Validate()
saw the emptyo.RemoteName
and changed it toDefaultRemoteName
("origin").If no "origin" remote was defined in the gitconfig,
Repository.Push()
failed entirely withErrRemoteNotFound
.If: an "origin" was defined in the gitconfig, then after the push, remote-tracking references were incorrectly created for the "origin" remote (e.g.,
refs/remotes/origin/foo
), even though the push was probably to a remote that is not related to "origin".Alternatively, if the
PushOptions
were created usingRemoteName: "anonymous"
, the push still failed withErrRemoteNotFound
because a remote called "anonymous" was sought in the gitconfig, and that remote (presumably!) doesn't exist.This commit fixes all three problems:
Change the behavior of
PushOptions.Validate()
: if there is aRemoteURL
but noRemoteName
, then set theRemoteName
to "anonymous" rather than setting it toDefaultRemoteName
. (If neither aRemoteName
nor aRemoteURL
is set, then setRemoteName
toDefaultRemoteName
as before.)Change
Remote.updateRemoteReferenceStorage()
to skip updating any remote-tracking references if the remote is anonymous.The earlier patches in this series introduce some helpers to make it easier to deal with anonymous remotes in the first place, and somewhat formalizing the concept (which already existed in the form of
Repository.CreateRemoteAnonymous()
but didn't seem to be used much).Aside: In my opinion, it would be preferable to use a more distinct name than "anonymous" for anonymous remotes, to avoid the (admittedly unlikely) possibility that the user has created an actual remote named "anonymous". For example, an anonymous remote could use the empty string for its name, or at least something more distinct like "anonymous" or "(anonymous)" or "anonymous". But there is already a precedent in
Repository.CreateRemoteAnonymous()
to use the name "anonymous", so I didn't make that change.This is my first contribution to go-git. I broke this PR into separate patches because I think that makes them easier to review, but if this project prefers single-commit PRs, I can either squash them together or submit separate PRs, as you prefer. (But the PRs will depend on each other, so there doesn't seem to be much value in the latter.)
In case you're curious, I contributed extensively to
git
several years ago and currently work at GitHub. I haven't used this library before, but am considering it for a small internal project. I'm happy that it exists and am glad to contribute a little bit to it ❤️