From 3bbfd9cae63b367663f427374c9edeff58ea07d7 Mon Sep 17 00:00:00 2001 From: Xueqin Cui Date: Fri, 9 Oct 2026 15:08:44 +1100 Subject: [PATCH 1/4] util/git: strip trailing slashes in canon Signed-off-by: Xueqin Cui --- util/git/canon.go | 2 ++ util/git/repo_test.go | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/util/git/canon.go b/util/git/canon.go index df760624..6c2e7562 100644 --- a/util/git/canon.go +++ b/util/git/canon.go @@ -51,5 +51,7 @@ func canon(repo *url.URL) *url.URL { u.User = url.User(u.User.Username()) } + u.Path = strings.TrimRight(u.Path, "/") + return &u } diff --git a/util/git/repo_test.go b/util/git/repo_test.go index 7335fefe..07e7bec1 100644 --- a/util/git/repo_test.go +++ b/util/git/repo_test.go @@ -53,6 +53,12 @@ func TestParseRepo(t *testing.T) { input: "https://github.com/only-one", wantErr: ErrInvalidRepo, // defaultHostHandler requires 2 segments }, + { + name: "trailing slash", + input: "https://chromium.googlesource.com/chromium/src.git/", + wantCanon: "https://chromium.googlesource.com/chromium/src.git", + wantParsed: "https://chromium.googlesource.com/chromium/src.git/", + }, } for _, tt := range tests { @@ -114,6 +120,11 @@ func TestRepo_ID(t *testing.T) { input: "https://github.com/user/repo/", want: "github.com/user/repo", }, + { + name: ".git and trailing slash", + input: "https://chromium.googlesource.com/chromium/src.git/", + want: "chromium.googlesource.com/chromium/src", + }, } for _, tt := range tests { From 12ea4c3ac7594ddad45c5b6334bcb10f053ff15e Mon Sep 17 00:00:00 2001 From: Xueqin Cui Date: Tue, 13 Oct 2026 16:01:03 +1100 Subject: [PATCH 2/4] util/git: add domain suffix host matching and fix trailing slash handling Signed-off-by: Xueqin Cui --- util/git/canon.go | 2 -- util/git/hosts.go | 48 ++++++++++++++++++++++++++++-------------- util/git/hosts_test.go | 31 +++++++++++++++++++++++++++ util/git/repo.go | 4 ++-- util/git/repo_test.go | 2 +- 5 files changed, 66 insertions(+), 21 deletions(-) diff --git a/util/git/canon.go b/util/git/canon.go index 6c2e7562..df760624 100644 --- a/util/git/canon.go +++ b/util/git/canon.go @@ -51,7 +51,5 @@ func canon(repo *url.URL) *url.URL { u.User = url.User(u.User.Username()) } - u.Path = strings.TrimRight(u.Path, "/") - return &u } diff --git a/util/git/hosts.go b/util/git/hosts.go index 6c3a3db9..a5e185b2 100644 --- a/util/git/hosts.go +++ b/util/git/hosts.go @@ -18,22 +18,25 @@ type HostHandler interface { } var hostHandlers []struct { - exactHost string - handler HostHandler + host string + handler HostHandler } // RegisterHostHandler allows for the registration of a host handler -// that is triggered when the host of a URL exactly matches the given host, +// that is triggered when the host of a URL matches the given host, // ignoring case. // +// If host starts with a ".", it will be matched as a domain suffix. +// Exact host matches take precedence over suffix matches. +// // This method is not thread-safe, and is expected to be called from init(). -func RegisterHostHandler(exactHost string, handler HostHandler) { +func RegisterHostHandler(host string, handler HostHandler) { hostHandlers = append(hostHandlers, struct { - exactHost string - handler HostHandler + host string + handler HostHandler }{ - exactHost: exactHost, - handler: handler, + host: strings.ToLower(host), + handler: handler, }) } @@ -41,7 +44,12 @@ func RegisterHostHandler(exactHost string, handler HostHandler) { func getHostHandler(host string) HostHandler { host = strings.ToLower(host) for _, h := range hostHandlers { - if h.exactHost == host { + if h.host == host { + return h.handler + } + } + for _, h := range hostHandlers { + if strings.HasPrefix(h.host, ".") && strings.HasSuffix(host, h.host) { return h.handler } } @@ -143,9 +151,13 @@ func (h *StandardHostHandler) Canon(u *url.URL) *url.URL { res.User = nil } + // Always strip trailing slashes first so segment counting and .git suffix + // handling operate on the path without trailing slashes. + res.Path = strings.TrimRight(res.Path, "/") + if h.LowerPathSegments != 0 { - // Strip the prefix and trailing slash so that we can count the remaining path segments correctly. - path := strings.TrimRight(res.Path, "/") + // Strip the prefix so that we can count the remaining path segments correctly. + path := res.Path prefix := "" if h.PathPrefix != "" { prefix = "/" + strings.Trim(h.PathPrefix, "/") @@ -184,11 +196,7 @@ func (h *StandardHostHandler) Canon(u *url.URL) *url.URL { } if h.HasTrailingSlash { - if !strings.HasSuffix(res.Path, "/") { - res.Path += "/" - } - } else { - res.Path = strings.TrimSuffix(res.Path, "/") + res.Path += "/" } return &res @@ -232,4 +240,12 @@ func init() { MinPathSegments: 2, MaxPathSegments: 2, }) + + RegisterHostHandler(".googlesource.com", &StandardHostHandler{ + ForceScheme: "https", + StripUser: true, + HasDotGitSuffix: true, + LowerPathSegments: math.MaxInt, // All path segments are lowercased. + MinPathSegments: 1, + }) } diff --git a/util/git/hosts_test.go b/util/git/hosts_test.go index 3b85879d..fd4175aa 100644 --- a/util/git/hosts_test.go +++ b/util/git/hosts_test.go @@ -12,8 +12,12 @@ func TestGetHostHandler(t *testing.T) { hostHandlers = nil h1 := &StandardHostHandler{} + h2 := &StandardHostHandler{} + h3 := &StandardHostHandler{} RegisterHostHandler("example.com", h1) + RegisterHostHandler(".other.com", h2) + RegisterHostHandler("special.other.com", h3) tests := []struct { host string @@ -22,6 +26,9 @@ func TestGetHostHandler(t *testing.T) { {"example.com", h1}, {"foo.example.com", noop}, {"other.com", noop}, + {"foo.other.com", h2}, + {"bar.foo.other.com", h2}, + {"special.other.com", h3}, } for _, tt := range tests { @@ -179,12 +186,36 @@ func TestStandardHostHandler_Canon(t *testing.T) { url: "https://example.com/foo/bar", want: "https://example.com/foo/bar.git", }, + { + name: "add .git suffix when input has trailing slash", + handler: StandardHostHandler{HasDotGitSuffix: true}, + url: "https://example.com/foo/bar/", + want: "https://example.com/foo/bar.git", + }, + { + name: "keep .git suffix when input has trailing slash", + handler: StandardHostHandler{HasDotGitSuffix: true}, + url: "https://example.com/foo/bar.git/", + want: "https://example.com/foo/bar.git", + }, + { + name: "add .git suffix and keep trailing slash", + handler: StandardHostHandler{HasDotGitSuffix: true, HasTrailingSlash: true}, + url: "https://example.com/foo/bar/", + want: "https://example.com/foo/bar.git/", + }, { name: "has dot git suffix (remove)", handler: StandardHostHandler{HasDotGitSuffix: false}, url: "https://example.com/foo/bar.git", want: "https://example.com/foo/bar", }, + { + name: "remove .git suffix when input has trailing slash", + handler: StandardHostHandler{HasDotGitSuffix: false}, + url: "https://example.com/foo/bar.git/", + want: "https://example.com/foo/bar", + }, { name: "has trailing slash (add)", handler: StandardHostHandler{HasTrailingSlash: true}, diff --git a/util/git/repo.go b/util/git/repo.go index 15f6709c..1e63ea19 100644 --- a/util/git/repo.go +++ b/util/git/repo.go @@ -69,8 +69,8 @@ func (r *Repo) ID() string { host = "[" + host + "]" } - path := strings.TrimSuffix(u.EscapedPath(), ".git") - path = strings.TrimSuffix(path, "/") + path := strings.TrimSuffix(u.EscapedPath(), "/") + path = strings.TrimSuffix(path, ".git") return host + path } diff --git a/util/git/repo_test.go b/util/git/repo_test.go index 07e7bec1..d3d16d0f 100644 --- a/util/git/repo_test.go +++ b/util/git/repo_test.go @@ -54,7 +54,7 @@ func TestParseRepo(t *testing.T) { wantErr: ErrInvalidRepo, // defaultHostHandler requires 2 segments }, { - name: "trailing slash", + name: "googlesource trailing slash", input: "https://chromium.googlesource.com/chromium/src.git/", wantCanon: "https://chromium.googlesource.com/chromium/src.git", wantParsed: "https://chromium.googlesource.com/chromium/src.git/", From d2348a095bbf283dc64cf413f415c85dfdd2da5b Mon Sep 17 00:00:00 2001 From: Xueqin Cui Date: Tue, 13 Oct 2026 16:32:15 +1100 Subject: [PATCH 3/4] case --- util/git/hosts.go | 9 ++++----- util/git/repo.go | 2 +- util/git/repo_test.go | 13 ++++++++++++- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/util/git/hosts.go b/util/git/hosts.go index a5e185b2..5e642d63 100644 --- a/util/git/hosts.go +++ b/util/git/hosts.go @@ -242,10 +242,9 @@ func init() { }) RegisterHostHandler(".googlesource.com", &StandardHostHandler{ - ForceScheme: "https", - StripUser: true, - HasDotGitSuffix: true, - LowerPathSegments: math.MaxInt, // All path segments are lowercased. - MinPathSegments: 1, + ForceScheme: "https", + StripUser: true, + HasDotGitSuffix: true, + MinPathSegments: 1, }) } diff --git a/util/git/repo.go b/util/git/repo.go index 1e63ea19..f8550f89 100644 --- a/util/git/repo.go +++ b/util/git/repo.go @@ -69,7 +69,7 @@ func (r *Repo) ID() string { host = "[" + host + "]" } - path := strings.TrimSuffix(u.EscapedPath(), "/") + path := strings.TrimRight(u.EscapedPath(), "/") path = strings.TrimSuffix(path, ".git") return host + path diff --git a/util/git/repo_test.go b/util/git/repo_test.go index d3d16d0f..9f7deda9 100644 --- a/util/git/repo_test.go +++ b/util/git/repo_test.go @@ -59,6 +59,12 @@ func TestParseRepo(t *testing.T) { wantCanon: "https://chromium.googlesource.com/chromium/src.git", wantParsed: "https://chromium.googlesource.com/chromium/src.git/", }, + { + name: "googlesource preserves path case", + input: "https://android.googlesource.com/platform/packages/apps/Settings", + wantCanon: "https://android.googlesource.com/platform/packages/apps/Settings.git", + wantParsed: "https://android.googlesource.com/platform/packages/apps/Settings", + }, } for _, tt := range tests { @@ -121,10 +127,15 @@ func TestRepo_ID(t *testing.T) { want: "github.com/user/repo", }, { - name: ".git and trailing slash", + name: "googlesource .git and trailing slash", input: "https://chromium.googlesource.com/chromium/src.git/", want: "chromium.googlesource.com/chromium/src", }, + { + name: "unregistered host .git and trailing slash", + input: "https://example.com/user/repo.git/", + want: "example.com/user/repo", + }, } for _, tt := range tests { From fa87b932c90a7e4107eda8a7c1a1b0d602d1ebd1 Mon Sep 17 00:00:00 2001 From: Xueqin Cui Date: Tue, 13 Oct 2026 17:22:37 +1100 Subject: [PATCH 4/4] comment --- util/git/hosts.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/util/git/hosts.go b/util/git/hosts.go index 5e642d63..d9c7971b 100644 --- a/util/git/hosts.go +++ b/util/git/hosts.go @@ -152,7 +152,8 @@ func (h *StandardHostHandler) Canon(u *url.URL) *url.URL { } // Always strip trailing slashes first so segment counting and .git suffix - // handling operate on the path without trailing slashes. + // handling operate on the path without trailing slashes. If h.HasTrailingSlash + // is true, a trailing slash will be re-added at the end. res.Path = strings.TrimRight(res.Path, "/") if h.LowerPathSegments != 0 {