add repo fork command
This commit is contained in:
parent
d4012b1312
commit
6eaab2562f
2 changed files with 212 additions and 11 deletions
|
|
@ -12,9 +12,10 @@ import (
|
|||
|
||||
// Repository contains information about a GitHub repo
|
||||
type Repository struct {
|
||||
ID string
|
||||
Name string
|
||||
Owner RepositoryOwner
|
||||
ID string
|
||||
Name string
|
||||
CloneURL string
|
||||
Owner RepositoryOwner
|
||||
|
||||
IsPrivate bool
|
||||
HasIssuesEnabled bool
|
||||
|
|
@ -59,6 +60,46 @@ func (r Repository) ViewerCanPush() bool {
|
|||
}
|
||||
}
|
||||
|
||||
func RepoExistsOnGitHub(client *Client, repo ghrepo.Interface) (bool, error) {
|
||||
query := `
|
||||
query($owner: String!, $name: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
variables := map[string]interface{}{
|
||||
"owner": repo.RepoOwner(),
|
||||
"name": repo.RepoName(),
|
||||
}
|
||||
|
||||
result := struct {
|
||||
Repository Repository
|
||||
}{}
|
||||
err := client.GraphQL(query, variables, &result)
|
||||
|
||||
if err == nil {
|
||||
// we found it.
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// we didn't find it, but need to determine if we hit an error or it just doesn't exist.
|
||||
graphqlError, isGraphQLError := err.(*GraphQLErrorResponse)
|
||||
if isGraphQLError {
|
||||
tolerated := true
|
||||
for _, ge := range graphqlError.Errors {
|
||||
if ge.Type != "NOT_FOUND" {
|
||||
tolerated = false
|
||||
}
|
||||
}
|
||||
if tolerated {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
// GitHubRepo looks up the node ID of a named repository
|
||||
func GitHubRepo(client *Client, repo ghrepo.Interface) (*Repository, error) {
|
||||
query := `
|
||||
|
|
@ -190,9 +231,10 @@ func RepoNetwork(client *Client, repos []ghrepo.Interface) (RepoNetworkResult, e
|
|||
|
||||
// repositoryV3 is the repository result from GitHub API v3
|
||||
type repositoryV3 struct {
|
||||
NodeID string
|
||||
Name string
|
||||
Owner struct {
|
||||
NodeID string
|
||||
Name string
|
||||
CloneURL string `json:"clone_url"`
|
||||
Owner struct {
|
||||
Login string
|
||||
}
|
||||
}
|
||||
|
|
@ -208,8 +250,9 @@ func ForkRepo(client *Client, repo ghrepo.Interface) (*Repository, error) {
|
|||
}
|
||||
|
||||
return &Repository{
|
||||
ID: result.NodeID,
|
||||
Name: result.Name,
|
||||
ID: result.NodeID,
|
||||
Name: result.Name,
|
||||
CloneURL: result.CloneURL,
|
||||
Owner: RepositoryOwner{
|
||||
Login: result.Owner.Login,
|
||||
},
|
||||
|
|
|
|||
164
command/repo.go
164
command/repo.go
|
|
@ -2,12 +2,16 @@ package command
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/git"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/utils"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
|
@ -15,6 +19,10 @@ func init() {
|
|||
RootCmd.AddCommand(repoCmd)
|
||||
repoCmd.AddCommand(repoCloneCmd)
|
||||
repoCmd.AddCommand(repoViewCmd)
|
||||
repoCmd.AddCommand(repoForkCmd)
|
||||
|
||||
repoForkCmd.Flags().BoolP("yes", "y", false, "Run non-interactively, saying yes to prompts")
|
||||
repoForkCmd.Flags().BoolP("no", "n", false, "Run non-interactively, saying no to prompts")
|
||||
}
|
||||
|
||||
var repoCmd = &cobra.Command{
|
||||
|
|
@ -37,9 +45,18 @@ To pass 'git clone' options, separate them with '--'.`,
|
|||
RunE: repoClone,
|
||||
}
|
||||
|
||||
var repoForkCmd = &cobra.Command{
|
||||
Use: "fork [<repository>]",
|
||||
Short: "Create a fork of a repository.",
|
||||
Long: `Create a fork of a repository.
|
||||
|
||||
With no argument, creates a fork of the current repository. Otherwise, forks the specified repository.`,
|
||||
RunE: repoFork,
|
||||
}
|
||||
|
||||
var repoViewCmd = &cobra.Command{
|
||||
Use: "view [<repo>]",
|
||||
Short: "View a repository in the browser",
|
||||
Use: "view [<repository>]",
|
||||
Short: "View a repository in the browser.",
|
||||
Long: `View a GitHub repository in the browser.
|
||||
|
||||
With no argument, the repository for the current directory is opened.`,
|
||||
|
|
@ -63,6 +80,147 @@ func repoClone(cmd *cobra.Command, args []string) error {
|
|||
return utils.PrepareCmd(cloneCmd).Run()
|
||||
}
|
||||
|
||||
func isURL(arg string) bool {
|
||||
return strings.HasPrefix(arg, "http:/") || strings.HasPrefix(arg, "https:/")
|
||||
}
|
||||
|
||||
func repoFork(cmd *cobra.Command, args []string) error {
|
||||
ctx := contextForCommand(cmd)
|
||||
|
||||
forceYes, err := cmd.Flags().GetBool("yes")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
forceNo, err := cmd.Flags().GetBool("no")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiClient, err := apiClientForContext(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create client: %w", err)
|
||||
}
|
||||
|
||||
var toFork ghrepo.Interface
|
||||
inParent := false // whether or not we're forking the repo we're currently "in"
|
||||
if len(args) == 0 {
|
||||
baseRepo, err := determineBaseRepo(cmd, ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to determine base repository: %w", err)
|
||||
}
|
||||
inParent = true
|
||||
toFork = baseRepo
|
||||
} else {
|
||||
repoArg := args[0]
|
||||
|
||||
if isURL(repoArg) {
|
||||
parsedURL, err := url.Parse(repoArg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("did not understand argument: %w", err)
|
||||
}
|
||||
|
||||
toFork, err = ghrepo.FromURL(parsedURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("did not understand argument: %w", err)
|
||||
}
|
||||
|
||||
} else {
|
||||
toFork = ghrepo.FromFullName(repoArg)
|
||||
if toFork.RepoName() == "" || toFork.RepoOwner() == "" {
|
||||
return fmt.Errorf("could not parse owner or repo name from %s", repoArg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out := colorableOut(cmd)
|
||||
fmt.Fprintf(out, "Forking %s...\n", utils.Cyan(ghrepo.FullName(toFork)))
|
||||
|
||||
authLogin, err := ctx.AuthLogin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not determine current username: %w", err)
|
||||
}
|
||||
|
||||
possibleFork := ghrepo.New(authLogin, toFork.RepoName())
|
||||
exists, err := api.RepoExistsOnGitHub(apiClient, possibleFork)
|
||||
if err != nil {
|
||||
return fmt.Errorf("problem with API request: %w", err)
|
||||
}
|
||||
|
||||
if exists {
|
||||
return fmt.Errorf("%s %s", utils.Cyan(ghrepo.FullName(possibleFork)), utils.Red("already exists!"))
|
||||
}
|
||||
|
||||
forkedRepo, err := api.ForkRepo(apiClient, toFork)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fork: %w", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(out, "%s %s %s!\n",
|
||||
utils.Cyan(ghrepo.FullName(toFork)),
|
||||
utils.Green("successfully forked to"),
|
||||
utils.Cyan(ghrepo.FullName(forkedRepo)))
|
||||
|
||||
if forceNo {
|
||||
return nil
|
||||
}
|
||||
|
||||
if inParent {
|
||||
if !forceYes {
|
||||
remoteDesired := forceYes
|
||||
if !forceYes {
|
||||
prompt := &survey.Confirm{
|
||||
Message: "Would you like to add a remote for the new fork?",
|
||||
}
|
||||
err = survey.AskOne(prompt, &remoteDesired)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to prompt: %w", err)
|
||||
}
|
||||
}
|
||||
if remoteDesired {
|
||||
_, err := git.AddRemote("fork", forkedRepo.CloneURL, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add remote: %w", err)
|
||||
}
|
||||
|
||||
fetchCmd := git.GitCommand("fetch", "fork")
|
||||
fetchCmd.Stdin = os.Stdin
|
||||
fetchCmd.Stdout = os.Stdout
|
||||
fetchCmd.Stderr = os.Stderr
|
||||
err = utils.PrepareCmd(fetchCmd).Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch new remote: %w", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(out, "%s %s\n", utils.Green("remote added at "), utils.Cyan("fork"))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cloneDesired := forceYes
|
||||
if !forceYes {
|
||||
prompt := &survey.Confirm{
|
||||
Message: "Would you like to clone the new fork?",
|
||||
}
|
||||
err = survey.AskOne(prompt, &cloneDesired)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to prompt: %w", err)
|
||||
}
|
||||
|
||||
}
|
||||
if cloneDesired {
|
||||
cloneCmd := git.GitCommand("clone", forkedRepo.CloneURL)
|
||||
cloneCmd.Stdin = os.Stdin
|
||||
cloneCmd.Stdout = os.Stdout
|
||||
cloneCmd.Stderr = os.Stderr
|
||||
err = utils.PrepareCmd(cloneCmd).Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to clone fork: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func repoView(cmd *cobra.Command, args []string) error {
|
||||
ctx := contextForCommand(cmd)
|
||||
|
||||
|
|
@ -75,7 +233,7 @@ func repoView(cmd *cobra.Command, args []string) error {
|
|||
openURL = fmt.Sprintf("https://github.com/%s", ghrepo.FullName(baseRepo))
|
||||
} else {
|
||||
repoArg := args[0]
|
||||
if strings.HasPrefix(repoArg, "http:/") || strings.HasPrefix(repoArg, "https:/") {
|
||||
if isURL(repoArg) {
|
||||
openURL = repoArg
|
||||
} else {
|
||||
openURL = fmt.Sprintf("https://github.com/%s", repoArg)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue