Skip to content

Interactive merge#899

Merged
probablycorey merged 34 commits intomasterfrom
merge-interactive-merge
May 26, 2020
Merged

Interactive merge#899
probablycorey merged 34 commits intomasterfrom
merge-interactive-merge

Conversation

@probablycorey
Copy link
Contributor

@probablycorey probablycorey commented May 11, 2020

This PR adds two features:

  1. Calling gh merge with no merge method flags (--merge | --rebase | --squash) enters you into interactive mode.
  2. Adds the ability to delete the PR's branch after a merge using a flag (--delete) or as a choice in interactive mode.

Here is how it looks:
CleanShot 2020-05-12 at 09 49 05@2x
CleanShot 2020-05-12 at 09 51 39

Closes #373

probablycorey and others added 9 commits May 12, 2020 08:51
Co-Authored-By: Nate Smith <vilmibm@neongrid.space>
Co-Authored-By: Nate Smith <vilmibm@neongrid.space>
Co-Authored-By: Nate Smith <vilmibm@neongrid.space>
Co-Authored-By: Nate Smith <vilmibm@neongrid.space>
Co-Authored-By: Nate Smith <vilmibm@neongrid.space>
Co-Authored-By: Nate Smith <vilmibm@neongrid.space>
Co-Authored-By: Nate Smith <vilmibm@neongrid.space>
Co-Authored-By: Nate Smith <vilmibm@neongrid.space>
@probablycorey probablycorey marked this pull request as ready for review May 12, 2020 17:13
@probablycorey probablycorey self-assigned this May 12, 2020
@probablycorey probablycorey requested review from mislav and vilmibm and removed request for mislav May 12, 2020 17:13
@probablycorey probablycorey marked this pull request as ready for review May 12, 2020 17:20
@DanyC97
Copy link

DanyC97 commented May 13, 2020

@probablycorey wouldn't this conflict with the git repo branch settings where the branch gets deleted when the PR is merged ?

mislav
mislav previously requested changes May 13, 2020
command/pr.go Outdated
return "", err
}

err = git.CheckoutBranch(repo.DefaultBranchRef.Name)
Copy link
Contributor

Choose a reason for hiding this comment

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

This might be edge case-y, but there might not be a local branch equivalent of origin/<default-branch>. In that case, this command would fail. We could maybe fall back to any other branch, as long as we can switch away from the current one so we can delete it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This seems pretty edge case-y, but maybe this is something we could fix in a future PR. I'm also not sure how i'd fix it and wondering if failing is better than switching to a random branch because that would be pretty unexpected.

if call >= len(cs.Stubs) {
panic(fmt.Sprintf("more execs than stubs. most recent call: %v", cmd))
}
// fmt.Printf("Called stub for `%v`\n", cmd) // Helpful for debugging
Copy link
Contributor

Choose a reason for hiding this comment

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

This makes me thing that should expand our cmd stubber to be able to assert that a specific command was invoked

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 think that would still run into similar problems. When things don't match up correctly you still want to print out the commands that are called. I think there is a better solution to what we're doing with command stubbing, but I've not been able to think of it yet!

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.

Something that I only caught in the re-review: I understood that in the design spec, the prompt to delete a branch also suggests that the branch in the remote repository will be deleted. In this implementation, only the local branch is deleted; the remote branch stays. Is this feature something that is planned for this PR or as a follow-up?

In my view, deleting the remote branch would be a bigger priority than deleting the local one, since the former matches the behavior of the web UI flow with Merge Button followed by "delete branch".

@probablycorey
Copy link
Contributor Author

In this implementation, only the local branch is deleted; the remote branch stays. Is this feature something that is planned for this PR or as a follow-up?

I just noticed the "and on GitHub" language too! I think this PR is getting big enough as it is. A follow-up PR to add remote branch deletion makes sense to me.

command/root.go Outdated
return baseRepo, nil
}

func convertRepoInterfaceToRepository(ctx context.Context, repo ghrepo.Interface) (*api.Repository, error) {
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've added this function so we can get an *api.Repository object from a ghrepo.Interface.

Copy link
Contributor

Choose a reason for hiding this comment

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

Is it worth doing a type check in this to see if a given interface-satisfying value is already an api.Repository? (genuine question, I'm not sure)

@probablycorey
Copy link
Contributor Author

Is it worth doing a type check in this to see if a given interface-satisfying value is already an api.Repository? (genuine question, I'm not sure)

@vilmibm I feel like that would make the code more complicated. I do think this is annoying though and if you'd be interested it might be worth doing a spike on trying to return api.Repository in more places instead of the ghrepo interface.

command/pr.go Outdated
return err
}

err = git.CheckoutBranch(repo.DefaultBranchRef.Name)
Copy link
Contributor

Choose a reason for hiding this comment

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

You can technically get the default branch from git by doing

git symbolic-ref refs/remotes/origin/HEAD

however, in my experience this value will usually be out of date after the remote repository changes its default branch. So I think it's best to continue to get this information from the server. 👍

command/pr.go Outdated
return err
}

err = git.CheckoutBranch(repo.DefaultBranchRef.Name)
Copy link
Contributor

Choose a reason for hiding this comment

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

Technically, we only need to switch to a different branch if we're doing to delete a branch that happens to be the current branch. If the pr.HeadRefName branch to delete is not the current one, I would say that we should ideally avoid the git checkout step.

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 makes sense. I added the behavior you recommended and a test.

prCmd.AddCommand(prReopenCmd)
prCmd.AddCommand(prMergeCmd)
prMergeCmd.Flags().BoolP("merge", "m", true, "Merge the commits with the base branch")
prMergeCmd.Flags().BoolP("delete-branch", "d", true, "Delete the local branch after merge")
Copy link
Contributor

Choose a reason for hiding this comment

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

For scripting use, I would prefer that this flag would be false by default.

The deletion of the local branch is potentially destructive right now and, in my mind, prone to edge-cases:

  • assumes that a local branch corresponding to a PR always exists;
  • assumes that the local branch name is always the same as PR head;
  • assumes that the branch to be deleted does not have unpushed commits.

If I write a script that programatically merges multiple PRs:

for pr in 12 13 14; do gh pr merge --merge $pr; done

I would get 3 git errors about not being able to delete the local branch if I've never actually checked out those PRs locally before. I could avoid it by doing:

gh pr merge --merge $pr --delete-branch=false

but I feel that it's best if this flag was opt-in instead of opt-out by default.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My only concern here is that the default behavior for deletion would be different between interactive and non-interactive. But if other people don't think this is a problem then I'm cool with the change.

command/root.go Outdated
}

func convertRepoInterfaceToRepository(ctx context.Context, repo ghrepo.Interface) (*api.Repository, error) {
apiClient, err := apiClientForContext(ctx)
Copy link
Contributor

Choose a reason for hiding this comment

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

We should try to pass an API client from the outside to avoid instantiating multiple API clients during the lifetime of the same command #943

command/root.go Outdated
return nil, err
}

repoContext, err := context.ResolveRemotesToRepos(remotes, apiClient, "")
Copy link
Contributor

@mislav mislav May 20, 2020

Choose a reason for hiding this comment

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

I think this method of resolving is too complex for this use case and that resolving a "ghrepo" to a *Repository object could be done simpler. Additionally, it doesn't look like the repo argument to the parent function is respected in the lookup.

We have a function that can resolve a ghrepo to a Repository: api.GitHubRepo(). Currently, the payload returned does not include the information about the default branch, but you could expand the field list requested in that query to include defaultBranchRef { name }.

Copy link
Contributor

@mislav mislav May 20, 2020

Choose a reason for hiding this comment

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

One more optimization that we could easily do: since the repo ghrepo.Interface object might already be an *api.Repository (and often it will be), we could detect and return that and avoid making an API request:

if r, ok := repo.(*api.Repository); ok && r.DefaultBranchRef.Name != "" {
  return r, nil
}
return api.GitHubRepo(repo)

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 got rid of the convertRepoInterfaceToRepository method and replaced it with api.GitHubRepo. I'm against the type checking idea though. It feels like extra code without much benefit. But I'm interested in exploring how to ensure we always get an api.Repository (or why we sometimes only need a ghrepo interface)

@probablycorey
Copy link
Contributor Author

probablycorey commented May 20, 2020

@ampinsk I've updated the text to show this now

CleanShot 2020-05-20 at 08 27 43@2x

Instead of saying merge it uses the same language as it had in the choices: "Create a merge commit"

I've also got better warnings for commits.

@probablycorey probablycorey mentioned this pull request May 21, 2020
@vilmibm vilmibm self-requested a review May 21, 2020 18:19
Copy link
Contributor

@vilmibm vilmibm left a comment

Choose a reason for hiding this comment

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

🙏 🙏 🙏 🙏 🙏 🙏 🙏 🙏

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.

If someone used gh pr merge --repo OWNER/REPO to merge PRs in a repository different than the current one, should we still offer to delete the local branch? If there happens to be one locally, I don't think we should ever delete it because it likely belongs to another (unrelated) repo.

for _, s := range stubs {
http.StubResponse(s.ResponseCode, s.ResponseBody)
}
http.StubRepoResponse("OWNER", "REPO")
Copy link
Contributor

Choose a reason for hiding this comment

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

I noticed that this test already has http.StubRepoResponse("OWNER", "REPO") above. Is this extra stub necessary?

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 yes, because we have to convert the ghrepo interface into an api.Repository interface which does another lookup.

@probablycorey
Copy link
Contributor Author

probablycorey commented May 22, 2020

If someone used gh pr merge --repo OWNER/REPO to merge PRs in a repository different than the current one, should we still offer to delete the local branch?

I think we should remove the -R flag for the merge command. It adds a lot of complexity to the command and the benefits seem pretty minimal.

After some internal discussion we're moving to this idea...

considering that Monday is a holiday for US folks, we can just keep current behavior and disable local branch cleanup if --repo was used (since those features don't make sense to use together anyway). doing the latter would be the least work

@probablycorey
Copy link
Contributor Author

probablycorey commented May 23, 2020

I think all the gh pr merge --repo OWNER/REPO concerns are now covered.

Local branches are never deleted when the repo flag is set
Remote branches are always deleted and use the api to do it. (handled in #982)

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.

I left some suggestions about improvements around certain checks, but as a whole this feature looks good! Thanks for your hard work on this 🌟

state
url
headRefName
mergeable
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it necessary to request this field if we're never going to consume it when listing pull requests? This is a potentially expensive field as GitHub starts a background job to check mergeability.

err := fmt.Errorf("%s Pull request #%d has conflicts and isn't mergeable ", utils.Red("!"), pr.Number)
return err
} else if pr.Mergeable == "UNKNOWN" {
err := fmt.Errorf("%s Pull request #%d can't be merged right now; try again in a few seconds", utils.Red("!"), pr.Number)
Copy link
Contributor

Choose a reason for hiding this comment

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

"UNKNOWN" usually means that mergeability is currently still being calculated. Instead of presenting this as an error, we could be optimistic and assume that the PR is OK to merge and allow the API merge operation to proceed—it has a chance of succeeding, and if it fails the API error message should let us know why.

} else if pr.Mergeable == "UNKNOWN" {
err := fmt.Errorf("%s Pull request #%d can't be merged right now; try again in a few seconds", utils.Red("!"), pr.Number)
return err
} else if pr.State == "MERGED" {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should check this state before any checks around the .Mergeable field. If the PR is already merged, its .Mergeable field might report "UNKOWN" or "CONFLICTING", in which case the current logic will trigger one of the error messages above, which would be misleading to the user.

fmt.Fprintf(colorableOut(cmd), "%s %s pull request #%d\n", utils.Magenta("✔"), action, pr.Number)

if deleteBranch && !cmd.Flags().Changed("repo") {
repo, err := api.GitHubRepo(apiClient, baseRepo)
Copy link
Contributor

Choose a reason for hiding this comment

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

The repo value looks like it's only used inside a conditional, so we can initialize it inside that same conditional as well (skipping an API request if the condition is false).

@probablycorey probablycorey merged commit be5fc07 into master May 26, 2020
@probablycorey probablycorey deleted the merge-interactive-merge branch May 26, 2020 15:34
@jglick
Copy link

jglick commented Jun 5, 2020

Tried this in gh version 0.9.0:

$ gh pr merge -dm 14
✔ Merged pull request #14
✔ Deleted branch docs

It did merge the pull request. It did not actually delete the docs branch, either locally or remotely.

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.

Support to merge pull request from gh cli

6 participants