add repo fork command

This commit is contained in:
vilmibm 2020-02-24 14:23:34 -06:00
parent d4012b1312
commit 6eaab2562f
2 changed files with 212 additions and 11 deletions

View file

@ -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,
},

View file

@ -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)