Skip to content

gh repo fork#549

Merged
mislav merged 21 commits intomasterfrom
repo-fork
Mar 3, 2020
Merged

gh repo fork#549
mislav merged 21 commits intomasterfrom
repo-fork

Conversation

@vilmibm
Copy link
Contributor

@vilmibm vilmibm commented Feb 25, 2020

Summary

Create a fork of a repository.

With no argument, creates a fork of the current repository. Otherwise, forks the specified repository.

Usage:
  gh repo fork [<repository>] [flags]

Flags:
  -n, --no    Run non-interactively, saying no to prompts
  -y, --yes   Run non-interactively, saying yes to prompts

From outside a cloned repo:

image

From inside a cloned repo:
image

  • Prompts about either adding a remote (when run from a repo directory) or cloning new fork (when run not in a repo directory)
  • Uses https for remotes for now
  • Allows non-interactive use with -y and -n

closes #334

@vilmibm vilmibm marked this pull request as ready for review February 25, 2020 22:27
@vilmibm vilmibm requested a review from mislav February 25, 2020 22:27
@vilmibm vilmibm changed the title [WIP] gh repo fork gh repo fork Feb 25, 2020
@ampinsk
Copy link

ampinsk commented Feb 26, 2020

Woo this is looking great! This was the formatting and language I had in mind:

From outside cloned repo:

Screen Shot 2020-02-25 at 5 09 39 PM

From inside cloned repo:

Screen Shot 2020-02-25 at 5 09 48 PM

I know we haven't included all of these unicode characters before so no worries if we want to add that in later!

Copy link
Contributor

@mislav mislav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job with comprehensive tests! 🏆

command/repo.go Outdated
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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idea: maybe we can be more specific with flag naming here and have the flag betray the specific operation that it applies to? E.g. repo fork --clone or repo fork --clone=false

I feel like --yes and --no could prove hard to maintain over time if it turns out they mean different things in different commands, and in case those commands have more than one boolean prompt

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just having clone doesn't help the remote adding case. i based y/n on apt-get and i like how it's easy to reason about in different contexts.

that said, i could be ok with --add-remote and --clone. i know @ampinsk pointed this out too; what do you think?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I like the idea of us being explicit with our flag language like this. I'm into --add-remote and --clone with the options to set them to false as well

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 I'll switch to --add-remote / -a and --clone / -c

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm not super thrilled about this but here's what I came up:

Flags:
  -c, --clone string    true: clone fork. false: never clone fork (default "prompt")
  -r, --remote string   true: add remote for fork. false: never add remote fork (default "prompt")

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried tweaking a bit:

Flags:
  -c, --clone string    Clone fork. Pass false to not clone fork. No flag will prompt.
  -r, --remote string   Add remote for fork. Pass false to not add remote fork. No flag will prompt.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

distinguishing between "user specified flag but passed nothing" and "user did not specify flag" is weirdly unintuitive in our codebase; I got frustrated and did the explicit true/false thing. I can take another swing at it though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah I immediately figured out how to do your suggestion, @ampinsk 🤦‍♀️ so now a bare --clone or --remote implies true.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks nate!!

command/repo.go Outdated
}

possibleFork := ghrepo.New(authLogin, toFork.RepoName())
exists, err := api.RepoExistsOnGitHub(apiClient, possibleFork)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I just realized something. We do not have to check whether the fork already exists if you are going to fork otherwise. You can simply initiate the API fork operation and it will be a no-op and return the existing repo name if your fork already exists. That makes it simpler for you I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's a good point, i can play with that implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The create fork endpoint seems to behave exactly the same whether or not it created the repo; it returns 202 and the fork's information. This means I can't tell whether or not the fork already existed and we lose the ability to tell the user "hey, that thing you wanted to do is already done."

I personally like telling them that the fork already exists. Pretending that we created it (and offering to add a remote or clone) seems only like it will create confusion.

@ampinsk I'm curious what you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a good point that we would prefer to communicate to the user what just happened.

One problem with looking up YOURUSER/REPONAME as possible fork repo is that a person might have a fork of a repo, but that fork will not have the same repository name.

Example: I fork vilmibm/dotfiles. Because I already have a dotfiles repo, the fork will be created with the name https://github.com/mislav/dotfiles-1. Thus, we cannot assume that someone's fork will always have the same repository name.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that I'd rather be transparent and clear about what's going on - so +1 on telling them the fork already exists in this scenario!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to leave the repo-existence check in. That's a good point about the repo names; it seems like the right approach is to actually list the forks for the repo-to-fork and check for one belonging to the executing user. I'll take a stab at that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unfortunately there isn't an efficient way to do a more sophisticated check. the best i came up with was:

query { 
  viewer {
    repositories(isFork: true, first:100) {
      edges {
        node {
          name
          parent {
            owner {
              login
            }          }        }      }    }  }}

and then comparing the forks' login with the target repo's owner's login. I don't know that it's worth it at this point given that most of the time an existing fork is going to have the same name.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an efficient way with GraphQL, at least in theory; in practice it looks like it's broken since it returns wrong results:

{
  repository(owner:"github", name:"fetch") {
    forks(first: 1, affiliations: OWNER) {
      nodes {
        nameWithOwner
      }
    }
  }
}

I would advise against doing a lookup which we know in advance is imprecise. Right now the only precise API lookup we have is to kick off the fork operation and read the resulting repository. We could read the created_at attribute of the result to find out if the fork was just created so we can communicate that to the user.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The created_at approach worked out pretty well. Once that gql query is fixed I'd rather switch to that.

@vilmibm
Copy link
Contributor Author

vilmibm commented Feb 26, 2020

@ampinsk I switched to your suggested UX (and have since fixed the ellipsis color to be consistent)

spinner

@ampinsk
Copy link

ampinsk commented Feb 26, 2020

Ahhh yay I love the checks! Looks great!

@ampinsk
Copy link

ampinsk commented Feb 26, 2020

Oh one last thing, should we output the fork URL at any point?

@vilmibm
Copy link
Contributor Author

vilmibm commented Feb 26, 2020

hm, I feel like someone's next action is going to be editing code or doing something git-related so I feel like the URL isn't super necessary? I'd be curious to leave it out and see if there's a request.

I didn't initially use GitHubRepo because it had the weird error
wrapping. I poked around and I think this function is leftover from a
vestigial GitHubRepoID function that used to be more single-purpose. I
took out the weird error handling so it could be reused and then used it
from the new GitHubRepoExists.
@vilmibm vilmibm requested a review from mislav February 27, 2020 22:56
Copy link
Contributor

@mislav mislav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job; almost there! I think polish is all that is necessary still

@vilmibm vilmibm requested a review from mislav March 2, 2020 21:25
repoForkCmd.Flags().String("clone", "prompt", "Clone fork. Pass false to not clone fork.")
repoForkCmd.Flags().String("remote", "prompt", "Add remote for fork. Pass false to not add remote.")
repoForkCmd.Flags().Lookup("clone").NoOptDefVal = "true"
repoForkCmd.Flags().Lookup("remote").NoOptDefVal = "true"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL NoOptDefVal!

I wonder, though, if the users will understand that the default value for --clone omitted is "prompt" (as indicated in help docs), but that the default value for --clone passed without value is "true" 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mislav Yep, same ambivalence as you here, and @ampinsk and @vilmibm went back and forth on it starting here: #549 (comment)

It didn't seem like anything we thought of was obviously very elegant, but this feels like a reasonable starting point at least to me and we can always update it if we see evidence of confusion.


} else {
toFork = ghrepo.FromFullName(repoArg)
if toFork.RepoName() == "" || toFork.RepoOwner() == "" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if there should exist a shared variant of FromFullName that raises an error like this so we can consistently parse user input with that

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do think FromFullName should be stricter; I can't think of a time when an empty owner or name is anything but an error.

command/repo.go Outdated
return fmt.Errorf("failed to add remote: %w", err)
}

fetchCmd := git.GitCommand("fetch", "fork")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noticed that AddRemote already does auto-fetch, so we don't have to do git fetch here

command/repo.go Outdated
repoCmd.AddCommand(repoForkCmd)

repoForkCmd.Flags().String("clone", "prompt", "Clone fork. Pass false to not clone fork.")
repoForkCmd.Flags().String("remote", "prompt", "Add remote for fork. Pass false to not add remote.")
Copy link
Contributor

@mislav mislav Mar 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: could we generally avoid punctuation and multiple sentences within flags docs? This is for the same reason why we would avoid them when raising error messages in Go: for readability and so that the end formatter (in this case, gh help) has more control over the final output.

I'm also thinking that it would be useful to list possible flag values like we do elsewhere. I wonder if this would do the trick:

    repoForkCmd.Flags().String("clone", "prompt", "Clone the new fork: {true|false|prompt}")
    repoForkCmd.Flags().String("remote", "prompt", "Add a git remote for the fork: {true|false|prompt}")

The help formatter would then append the default value in the final output:

Flags:
  --clone string    Clone the new fork: {true|false|prompt} (default "prompt")
  --remote string   Add a git remote for the fork: {true|false|prompt} (default "prompt")

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious what @ampinsk thinks. i have zero strong feelings. i'll switch to this and we can always change it.

@vilmibm vilmibm requested a review from mislav March 3, 2020 21:41
Copy link
Contributor

@mislav mislav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome! Thanks for the updates

@mislav mislav merged commit c610f5c into master Mar 3, 2020
@mislav mislav deleted the repo-fork branch March 3, 2020 21:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add explicit ability to fork a repo

5 participants