Skip to content

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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

mhagger
Copy link
Contributor

@mhagger mhagger commented Apr 13, 2025

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 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 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 "anonymous" 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.

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 ❤️

@Copilot Copilot AI review requested due to automatic review settings April 13, 2025 16:12
Copy link
Contributor

@Copilot Copilot AI left a 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.

Copy link
Member

@pjbgf pjbgf left a 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.

@mhagger mhagger force-pushed the push-to-anonymous-remote branch from dd7b094 to ca6457f Compare April 14, 2025 07:12
@mhagger mhagger changed the base branch from main to v6-transport April 14, 2025 07:13
@mhagger
Copy link
Contributor Author

mhagger commented Apr 14, 2025

@pjbgf Thanks for your quick reply.

I rebased this branch onto the v6-transport branch, changed the target to that branch, and squashed in your suggested change.

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:

  • It would require a change to Repository.CreateRemoteAnonymous(), which currently requires that the name of an anonymous remote be "anonymous", and also to the tests TestCreateRemoteAnonymous() and TestCreateRemoteAnonymousInvalidName(), which explicitly test for this behavior.
  • It would require a change to Remote.String(), which currently incorporates the remote's name into its string representation.

If we go that route, then for backwards compatibility, one could imagine having Repository.CreateRemoteAnonymous() accept either "" or "anonymous" as the input RemoteConfig.name. But since the passed-in RemoteConfig is (unfortunately?) built straight into the constructed Remote, one would either have to:

  1. accept either name as meaning "anonymous" in a Remote instance, thus making the code more, rather than less, complicated, and continue to live with the inability to handle a configured remote that happens to be named "anonymous";
  2. overwrite "anonymous" with the empty string in the passed-in RemoteConfig;
  3. copy the RemoteConfig but with "anonymous" changed to "", and store the copy (not the original) in the Remote instance; or
  4. slightly break backwards compatibility by changing Repository.CreateRemoteAnonymous() to require anonymous remotes to be created with the empty string as their name.

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.

@pjbgf
Copy link
Member

pjbgf commented Apr 15, 2025

@mhagger thanks for rebasing this. 🙇

Are you suggesting that anonymous remotes should have the empty string as their name?

Yes. I added a few comments below with the changes we may need to do to achieve this.

It would require a change to Repository.CreateRemoteAnonymous()

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).

It would require a change to Remote.String(), which currently incorporates the remote's name into its string representation.

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 Remote name, so that it is clear this is not a valid Remote e.g. <unnamed>.

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.

@pjbgf pjbgf added the breaking label Apr 15, 2025
config/config.go Outdated
Comment on lines 658 to 664
func NewAnonymousRemoteConfig(url string) *RemoteConfig {
return &RemoteConfig{
Name: AnonymousRemoteName,
URLs: []string{url},
}
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func NewAnonymousRemoteConfig(url string) *RemoteConfig {
return &RemoteConfig{
Name: AnonymousRemoteName,
URLs: []string{url},
}
}

options.go Outdated
Comment on lines 312 to 316
if o.RemoteURL != "" {
o.RemoteName = config.AnonymousRemoteName
} else {
o.RemoteName = DefaultRemoteName
}
Copy link
Member

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)

Copy link
Member

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.

Suggested change
s.Len(c.Remotes, 1)

repository.go Outdated
Comment on lines 1252 to 1258
var err error
remote, err = r.CreateRemoteAnonymous(
config.NewAnonymousRemoteConfig(o.RemoteURL),
)
if err != nil {
return err
}
Copy link
Member

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:

Suggested change
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},
})

Copy link
Contributor Author

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() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if r.IsAnonymous() {
if r.isEphemeral() {

@@ -288,6 +288,25 @@ func (s *ConfigSuite) TestValidateInvalidRemoteKey() {
s.ErrorIs(config.Validate(), ErrInvalid)
}

func (s *ConfigSuite) TestAnonymousRemoteConfig() {
config := &RemoteConfig{
Name: "anonymous",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Name: "anonymous",
Name: "",

}

s.NoError(config.Validate())
s.Equal("anonymous", config.Name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
s.Equal("anonymous", config.Name)
s.Empty(config.Name)

URLs: []string{"http://foo/bar"},
}

s.NoError(config.Validate())
Copy link
Member

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
Comment on lines 77 to 82
// 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()
}
Copy link
Member

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:

Suggested change
// 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
Comment on lines 667 to 670
// 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
Copy link
Member

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.

mhagger added 5 commits April 22, 2025 00:34
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.
@mhagger mhagger force-pushed the push-to-anonymous-remote branch from ca6457f to e9bb4b8 Compare April 21, 2025 22:35
@mhagger
Copy link
Contributor Author

mhagger commented Apr 21, 2025

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 RemoteConfig should be validated if NewRemote() is called. Currently it is validated by Repository.CreateRemote() but not by NewRemote(), which seems strange to me. Perhaps the validation should be moved to NewRemote() (which is called by Repository.CreateRemote()) to cover both code paths?

@pjbgf
Copy link
Member

pjbgf commented Apr 23, 2025

Currently it is validated by Repository.CreateRemote() but not by NewRemote(), which seems strange to me. Perhaps the validation should be moved to NewRemote() (which is called by Repository.CreateRemote()) to cover both code paths?

@mhagger +1 on your suggestion. Let's move this to NewRemote() and update the signature to also return an error. Otherwise, LGTM.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants