From 7777d8ced868b22c3411812102d79848247fd391 Mon Sep 17 00:00:00 2001 From: Jordan Barrett <90195985+barrettj12@users.noreply.github.com.> Date: Sat, 5 Jul 2025 21:37:09 -0500 Subject: [PATCH] cloning supports non-github repos - attach git clone output --- cmd/clone.go | 34 +++++++++++-------- common/git/git.go | 18 ++++++---- common/url/url.go | 74 ++++++++++++++++++++++++++++++++++++++++++ common/url/url_test.go | 51 ++++++++++++++++++++++++++++- 4 files changed, 156 insertions(+), 21 deletions(-) diff --git a/cmd/clone.go b/cmd/clone.go index caf37db..fd5780f 100644 --- a/cmd/clone.go +++ b/cmd/clone.go @@ -2,13 +2,14 @@ package cmd import ( "fmt" + "strconv" + "github.com/barrettj12/jit/common" "github.com/barrettj12/jit/common/git" "github.com/barrettj12/jit/common/path" "github.com/barrettj12/jit/common/types" "github.com/barrettj12/jit/common/url" "github.com/spf13/cobra" - "strconv" ) var cloneDocs = ` @@ -45,12 +46,12 @@ func newCloneCmd() *cobra.Command { // Clone clones the provided repo, using the workflow described in // https://morgan.cugerone.com/blog/how-to-use-git-worktree-and-in-a-clean-way/ func Clone(cmd *cobra.Command, args []string) error { - githubRepo := url.GitHubURL(args...) - user := githubRepo.Owner() + repoURL := url.URL(args...) + user := repoURL.Owner() if user == "" { return fmt.Errorf("must specify a user to clone repo from") } - repo := githubRepo.RepoName() + repo := repoURL.RepoName() if repo == "" { return fmt.Errorf("must specify a repo to clone") } @@ -64,7 +65,7 @@ func Clone(cmd *cobra.Command, args []string) error { // Clone the repo remote := types.RemoteName(user) err = git.Clone(git.CloneArgs{ - Repo: githubRepo, + Repo: repoURL, CloneDir: path.GitFolderPath(cloneDir), Bare: true, OriginName: remote, @@ -97,11 +98,14 @@ Create new branches using var shouldFork bool if forkFlagVal == "" { - // The user did not specify when typing the command whether we should - // fork the repo or not. Ask them. - shouldFork, err = confirm("Create a fork") - if err != nil { - return err + // Only ask to fork if this is a GitHub repo + if repoURL.HostedBy() == url.GitHub { + // The user did not specify when typing the command whether we should + // fork the repo or not. Ask them. + shouldFork, err = confirm("Create a fork") + if err != nil { + return err + } } } else { shouldFork, err = strconv.ParseBool(forkFlagVal) @@ -111,9 +115,13 @@ Create new branches using } if shouldFork { - err = fork(cloneDir, user, repo) - if err != nil { - return err + if repoURL.HostedBy() == url.GitHub { + err = fork(cloneDir, user, repo) + if err != nil { + return err + } + } else { + fmt.Printf("WARNING: don't know how to fork for repo type %q, skipping\n", repoURL.HostedBy()) } } diff --git a/common/git/git.go b/common/git/git.go index 4cd7762..c384d6d 100644 --- a/common/git/git.go +++ b/common/git/git.go @@ -3,14 +3,15 @@ package git import ( "bytes" "fmt" - "github.com/barrettj12/jit/common/env" - "github.com/barrettj12/jit/common/path" - "github.com/barrettj12/jit/common/types" - "github.com/barrettj12/jit/common/url" "io" "os" "os/exec" "strings" + + "github.com/barrettj12/jit/common/env" + "github.com/barrettj12/jit/common/path" + "github.com/barrettj12/jit/common/types" + "github.com/barrettj12/jit/common/url" ) type CloneArgs struct { @@ -33,7 +34,11 @@ func Clone(opts CloneArgs) error { args = append(args, out) } - _, err := internalExec(internalExecArgs{args: args}) + _, err := internalExec(internalExecArgs{ + args: args, + attachStdout: true, + attachStderr: true, + }) return err } @@ -150,8 +155,7 @@ var internalExec = func(opts internalExecArgs) (string, error) { fmt.Println(cmd.String()) } - var runErr error - runErr = cmd.Run() // this error contains the exit code + runErr := cmd.Run() // this error contains the exit code // handle errors if runErr != nil { diff --git a/common/url/url.go b/common/url/url.go index 780bb18..696188d 100644 --- a/common/url/url.go +++ b/common/url/url.go @@ -9,6 +9,9 @@ import ( // RemoteRepo represents a remote Git repository on GitHub, GitLab, etc type RemoteRepo interface { URL() string + Owner() string + RepoName() string + HostedBy() RepoSource } // Raw is a raw URL. @@ -18,6 +21,41 @@ func (u Raw) URL() string { return string(u) } +func (u Raw) Owner() string { + // Assume the first url component is the owner + parsed, _ := url.Parse(string(u)) + split := strings.Split(parsed.Path, "/") + if len(split) > 1 { + return split[1] + } + return "" +} + +func (u Raw) RepoName() string { + // Assume the second url component is the repo name + parsed, _ := url.Parse(string(u)) + split := strings.Split(parsed.Path, "/") + if len(split) > 2 { + return split[2] + } + return "" +} + +func (u Raw) HostedBy() RepoSource { + parsed, err := url.Parse(string(u)) + if err == nil { + switch parsed.Host { + case "github.com": + return GitHub + case "gitlab.com": + return GitLab + default: + return UnknownSite + } + } + return UnknownSite +} + // Nil represents an unspecified URL. var Nil RemoteRepo = Raw("") @@ -69,3 +107,39 @@ func (r GitHubRepo) RepoName() string { } return "" } + +func (r GitHubRepo) HostedBy() RepoSource { + return GitHub +} + +// URL converts the given path components into a repo URL. If the website is +// not specified, GitHub will be assumed. +// +// "user" -> "https://github.com/user" +// "user/repo" -> "https://github.com/user/repo" +// "user", "repo" -> "https://github.com/user/repo" +// "https://server.com/user/repo" -> "https://server.com/user/repo" +func URL(c ...string) RemoteRepo { + if len(c) == 0 { + return Nil + } + parsed, err := url.Parse(c[0]) + if err != nil { + return GitHubURL(c...) + } + if parsed.Host == "" { + // Assume GitHub + return GitHubURL(c...) + } + return Raw(c[0]) +} + +// RepoSource represents a possible hosting site for a Git repo, e.g. GitHub, +// GitLab, private website, ... +type RepoSource string + +const ( + GitHub RepoSource = "github" + GitLab RepoSource = "gitlab" + UnknownSite RepoSource = "unknown" +) diff --git a/common/url/url_test.go b/common/url/url_test.go index 5992c3c..61f1f05 100644 --- a/common/url/url_test.go +++ b/common/url/url_test.go @@ -1,8 +1,9 @@ package url import ( - "github.com/barrettj12/jit/common/testutil" "testing" + + "github.com/barrettj12/jit/common/testutil" ) func TestGitHubRepo(t *testing.T) { @@ -55,3 +56,51 @@ func TestIsNil(t *testing.T) { testutil.AssertEqual(t, IsNil(url), true) } } + +func TestParseURL(t *testing.T) { + tests := []struct { + input []string + url string + owner string + repoName string + hostedBy RepoSource + }{{ + input: []string{"user"}, + url: "https://github.com/user", + owner: "user", + repoName: "", + hostedBy: GitHub, + }, { + input: []string{"user", "repo"}, + url: "https://github.com/user/repo", + owner: "user", + repoName: "repo", + hostedBy: GitHub, + }, { + input: []string{"user/repo"}, + url: "https://github.com/user/repo", + owner: "user", + repoName: "repo", + hostedBy: GitHub, + }, { + input: []string{"https://github.com/user/repo"}, + url: "https://github.com/user/repo", + owner: "user", + repoName: "repo", + hostedBy: GitHub, + }, { + input: []string{"https://gitlab.com/user/repo"}, + url: "https://gitlab.com/user/repo", + owner: "user", + repoName: "repo", + hostedBy: GitLab, + }} + + for _, test := range tests { + url := URL(test.input...) + testutil.AssertEqual(t, url.URL(), test.url) + testutil.AssertEqual(t, url.Owner(), test.owner) + testutil.AssertEqual(t, url.RepoName(), test.repoName) + testutil.AssertEqual(t, url.HostedBy(), test.hostedBy) + } +}