From daeb9127764fc9dd469294f755e277fdfca09a00 Mon Sep 17 00:00:00 2001 From: Stefan Majewsky Date: Wed, 10 Jun 2026 00:07:56 +0200 Subject: rtree: add support for multiple URLs per remote This requires changing the index format, which I'm using as an opportunity to: - move from YAML to JSON (so I can maybe kill the YAML parser dependency at some point) - move the index file into standard XDG paths --- internal/rtree/repo.go | 110 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 83 insertions(+), 27 deletions(-) (limited to 'internal/rtree/repo.go') diff --git a/internal/rtree/repo.go b/internal/rtree/repo.go index c8e8ff3..92b7e74 100644 --- a/internal/rtree/repo.go +++ b/internal/rtree/repo.go @@ -16,17 +16,16 @@ import ( // Repo describes the entry for a repository in the index file. type Repo struct { //CheckoutPath shall be relative to the RootPath. - CheckoutPath string `yaml:"path"` + CheckoutPath string `json:"path"` //Remotes maps remote names (as noted in the .git/config of the repo) to //remote URLs (as they appear in the .git/config of the repo, i.e. possibly //abbreviated). - Remotes []Remote `yaml:"remotes"` + Remotes map[string]Remote `json:"remotes"` } // Remote describes a remote that is configured in a Repo. type Remote struct { - Name string `yaml:"name"` - URL RemoteURL `yaml:"url"` + URLs []RemoteURL `json:"urls"` } // AbsolutePath returns the absolute CheckoutPath of this repo. @@ -39,6 +38,15 @@ func (r Repo) GitDirPath() string { return filepath.Join(r.AbsolutePath(), ".git") } +// CompactURLs returns all URLs for this remote in their compact form. +func (r Remote) CompactURLs() []string { + result := make([]string, len(r.URLs)) + for idx, url := range r.URLs { + result[idx] = url.CompactURL() + } + return result +} + // NewRepoFromAbsolutePath initializes a Repo instance by scanning the existing // checkout at the given path. func NewRepoFromAbsolutePath(path string) (repo Repo, err error) { @@ -56,15 +64,19 @@ func NewRepoFromAbsolutePath(path string) (repo Repo, err error) { return } + repo.Remotes = make(map[string]Remote) for line := range strings.SplitSeq(out, "\n") { match := remoteConfigRx.FindStringSubmatch(line) if match == nil { continue } - repo.Remotes = append(repo.Remotes, Remote{ - Name: match[1], - URL: ParseRemoteURL(match[2]), - }) + name, url := match[1], ParseRemoteURL(match[2]) + if remote, ok := repo.Remotes[name]; ok { + remote.URLs = append(remote.URLs, url) + repo.Remotes[name] = remote + } else { + repo.Remotes[name] = Remote{URLs: []RemoteURL{url}} + } } return } @@ -75,10 +87,9 @@ func NewRepoFromRemoteURL(remoteURL RemoteURL) (Repo, error) { checkoutPath, err := remoteURL.CheckoutPath() return Repo{ CheckoutPath: checkoutPath, - Remotes: []Remote{ - { - Name: "origin", - URL: remoteURL, + Remotes: map[string]Remote{ + "origin": { + URLs: []RemoteURL{remoteURL}, }, }, }, err @@ -121,9 +132,9 @@ func ForeachPhysicalRepo(action func(repo Repo) error) error { func (r Repo) Checkout() error { //check if we have an "origin" remote to clone from var originURL RemoteURL - for _, remote := range r.Remotes { - if remote.Name == "origin" { - originURL = remote.URL + for remoteName, remote := range r.Remotes { + if remoteName == "origin" { + originURL = remote.URLs[0] break } } @@ -146,16 +157,27 @@ func (r Repo) Checkout() error { } remotesAdded := false - for _, remote := range r.Remotes { - if remote.Name != "origin" { - err := cli.Interface.Run(cli.Command{ - Program: []string{"git", "remote", "add", remote.Name, remote.URL.CanonicalURL()}, - WorkDir: r.AbsolutePath(), - }) - if err != nil { - return err + for remoteName, remote := range r.Remotes { + for idx, url := range remote.URLs { + if idx == 0 && remoteName != "origin" { + err := cli.Interface.Run(cli.Command{ + Program: []string{"git", "remote", "add", remoteName, url.CanonicalURL()}, + WorkDir: r.AbsolutePath(), + }) + if err != nil { + return err + } + remotesAdded = true + } else if idx > 0 { + err := cli.Interface.Run(cli.Command{ + Program: []string{"git", "remote", "set-url", "--add", remoteName, url.CanonicalURL()}, + WorkDir: r.AbsolutePath(), + }) + if err != nil { + return err + } + remotesAdded = true } - remotesAdded = true } } if remotesAdded { @@ -215,16 +237,50 @@ func (r *Repo) Move(checkoutPath string, makeSymlink bool) error { } // ReformatRemoteURLs rewrites the remote URLs in this repo's .git/config into -// their compact forms. +// their canonical forms. func (r Repo) ReformatRemoteURLs() error { - for _, remote := range r.Remotes { + // NOTE: This is a bit convoluted because the specific case of updating URLs + // for a remote with multiple URLs requires multiple steps. First, we clear + // out all non-primary URLs, and then re-add them after updating the primary URL. + actualRepo, err := NewRepoFromAbsolutePath(r.AbsolutePath()) + if err != nil { + return err + } + + for remoteName, remote := range r.Remotes { + actualRemote := actualRepo.Remotes[remoteName] + + if len(actualRemote.URLs) > 1 { + for _, url := range actualRemote.URLs[1:] { + err := cli.Interface.Run(cli.Command{ + Program: []string{"git", "remote", "set-url", "--delete", remoteName, url.CanonicalURL()}, + WorkDir: r.AbsolutePath(), + }) + if err != nil { + return err + } + } + } + err := cli.Interface.Run(cli.Command{ - Program: []string{"git", "remote", "set-url", remote.Name, remote.URL.CanonicalURL()}, + Program: []string{"git", "remote", "set-url", remoteName, remote.URLs[0].CanonicalURL()}, WorkDir: r.AbsolutePath(), }) if err != nil { return err } + + if len(remote.URLs) > 1 { + for _, url := range remote.URLs[1:] { + err := cli.Interface.Run(cli.Command{ + Program: []string{"git", "remote", "set-url", "--add", remoteName, url.CanonicalURL()}, + WorkDir: r.AbsolutePath(), + }) + if err != nil { + return err + } + } + } } return nil } -- cgit v1.3.1