Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions internal/gitclone/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,13 +265,21 @@ func (m *Manager) DiscoverExisting(ctx context.Context) ([]*Repository, error) {
}

// RepoPathFromURL extracts a normalised "host/path" from an upstream Git URL,
// stripping any ".git" suffix.
// stripping any ".git" suffix. The URL must contain at least an owner and
// repository component (e.g. "github.com/org/repo"); org-only URLs like
// "github.com/org" are rejected to prevent operations on parent directories
// that could affect other repositories.
func RepoPathFromURL(upstreamURL string) (string, error) {
parsed, err := url.Parse(upstreamURL)
if err != nil {
return "", errors.Wrap(err, "parse upstream URL")
}
return filepath.Join(parsed.Host, strings.TrimSuffix(parsed.Path, ".git")), nil
trimmed := strings.Trim(strings.TrimSuffix(parsed.Path, ".git"), "/")
parts := strings.Split(trimmed, "/")
if len(parts) < 2 || parts[0] == "" || parts[1] == "" {
return "", errors.Errorf("invalid repository URL %q: must contain at least owner and repository (e.g. host/owner/repo)", upstreamURL)
}
return filepath.Join(parsed.Host, trimmed), nil
}

func (m *Manager) clonePathForURL(upstreamURL string) (string, error) {
Expand Down
30 changes: 30 additions & 0 deletions internal/gitclone/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,36 @@ def456 refs/heads/develop
assert.Equal(t, "789012", refs["refs/tags/v1.0.0"])
}

func TestRepoPathFromURL(t *testing.T) {
tests := []struct {
name string
url string
want string
wantErr bool
}{
{name: "valid org/repo", url: "https://github.com/squareup/blox", want: "github.com/squareup/blox"},
{name: "valid with .git suffix", url: "https://github.com/squareup/blox.git", want: "github.com/squareup/blox"},
{name: "valid nested path", url: "https://gitlab.com/group/subgroup/repo", want: "gitlab.com/group/subgroup/repo"},
{name: "org only", url: "https://github.com/squareup", wantErr: true},
{name: "org only trailing slash", url: "https://github.com/squareup/", wantErr: true},
{name: "org only with .git", url: "https://github.com/squareup/.git", wantErr: true},
{name: "host only", url: "https://github.com", wantErr: true},
{name: "host only trailing slash", url: "https://github.com/", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := RepoPathFromURL(tt.url)
if tt.wantErr {
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid repository URL")
} else {
assert.NoError(t, err)
assert.Equal(t, tt.want, got)
}
})
}
}

func TestState_String(t *testing.T) {
assert.Equal(t, "empty", StateEmpty.String())
assert.Equal(t, "cloning", StateCloning.String())
Expand Down