Skip to content

Conversation

@bchadwic
Copy link
Contributor

@bchadwic bchadwic commented Dec 3, 2021

fixes #4715

Given the two options in the issue, I thought it would be best to allow this functionality rather than to produce an error. I tried to minimize the footprint of the solution while doing so.

@bchadwic bchadwic requested a review from a team as a code owner December 3, 2021 08:18
@bchadwic bchadwic requested review from mislav and removed request for a team December 3, 2021 08:18
@cliAutomation cliAutomation added the external pull request originating outside of the CLI core team label Dec 3, 2021
@bchadwic bchadwic changed the title using gh browse, open last the commit from a repo other than the current repo allow users to open up the last commit of a repository while using repo override flag Dec 3, 2021
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.

Thanks for taking this on!

DefaultBranchRef struct {
Target struct {
Commit struct {
History struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of reading History off of a Commit object, you can read its oid field which will be the commit SHA.

query ($repoOwner: String!, $repoName: String!) {
	repository(owner: $repoOwner, name: $repoName) {
		defaultBranchRef {
			name
			target {
				... on Commit {
					oid
					messageHeadline
				}
			}
		}
	}
}

if err := gql.QueryNamed(context.Background(), "LastCommit", &responseData, variables); err != nil {
return nil, err
}
return (*git.Commit)(&responseData.Repository.DefaultBranchRef.Target.Commit.History.Edges[0].Node), nil
Copy link
Contributor

Choose a reason for hiding this comment

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

It feels super weird to cast at unrelated struct into a git.Commit object in Go. I would rather copy the fields manually:

return &git.Commit{
  Sha: ...,
  Title: ...
}, ...

}, nil
}

func LastCommit(client *Client, repo ghrepo.Interface) (*git.Commit, error) {
Copy link
Contributor

@mislav mislav Dec 6, 2021

Choose a reason for hiding this comment

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

It's unfortunate that now the api package has to import (and thus depend on) the whole git package just so it can return a git.Commit type. Ideally api and git packages should be entirely separated concerns, unaware of each others' existence. I have some ideas how to iron this out, but I don't think it's time yet, and since this isn't a pressing issue, let's leave this like it is and circle back later.

if err != nil {
return err
}
opts.LastCommit = func() (*git.Commit, error) { return api.LastCommit(api.NewClientFromHTTP(httpClient), baseRepo) }
Copy link
Contributor

@mislav mislav Dec 6, 2021

Choose a reason for hiding this comment

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

OK, I see what you're doing here and it's a solid start, but I wonder if we can make this even more simpler & straightforward for the runBrowse() implementation. Right now, swapping out implementations of LastCommit like this is kind of awkward. Even though we pass around and assign funcs to struct fields quite often in gh codebase, I'm thinking that it has gone too far and that we should replace this approach with interfaces instead.

Ultimately, runBrowse() doesn't need to know about the type of lookup that will be performed. The only thing it wants to know is: what is the last commit for this repository? Of course, the definition of what "this repository" is ultimately depends on the --repo flag, but runBrowse() ideally should not be aware of whether --repo was used or not.

I propose to pass a concept of a "git client" to runBrowse via its opts struct. Something like:

type BrowseOptions struct {
  GitClient gitClient
}

type gitClient interface {
  LastCommit() (*git.Commit, error)
}

What implements the gitClient interface? Right now there are no implementations, but you could create some:

  1. The first and obvious implementation is the one that consults git.LastCommit() and therefore reads from the local git repository of the current directory;
  2. The second implementation is the one that uses api.LastCommit(httpClient, repo) to fetch commit information from the API.

Either of these implementations could be assigned to opts.GitClient by the command's RunE block based on whether or not the --repo flag has been passed. Then, runBrowse() can consume from the git client without knowing what type of a git client that is or where it reads from.

@mislav mislav self-assigned this Dec 6, 2021
@bchadwic
Copy link
Contributor Author

bchadwic commented Dec 7, 2021

@mislav

Thanks for taking this on!

Thank you for the quick review!

Assuming I read your review suggestions correctly:
On the one hand, there is more code now 😬 , but on the other, I found the interface approach to be semantically better. I can admit that in the first attempt, it wouldn't be easy to understand what this hack is doing down the road:

if opts.CommitFlag && cmd.Flags().Changed("repo") {
opts.LastCommit = nil
}

I like the interface because this same section seems closer to plain text now:

if cmd.Flags().Changed("repo") {
opts.GitClient = &remoteGitClient{opts.BaseRepo, opts.HttpClient}
}

This still needs tests. I wasn't sure if the client implementations belonged in browse.go. Also, I removed the git dependency out of the api package 👍

@mislav
Copy link
Contributor

mislav commented Dec 7, 2021

On the one hand, there is more code now 😬

That's to be expected, since declaring interfaces and whole types to satisfy them is always going to be more work than defining anonymous funcs. The size of the code is not a code smell in it of itself, especially considering that I wanted you to take this approach so that we can extend it (as a follow up) to other commands that take --repo but also perform operations on a local git repository in their normal mode of operation.

I wasn't sure if the client implementations belonged in browse.go

Eventually I do want this to live somewhere that's easily imported by other commands, so definitely outside of browse.go, but for now you can keep it in the same package while prototyping.

commit, err := opts.LastCommit()
if err == nil {
commit, err := opts.GitClient.LastCommit()
if err == nil && commit != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

  1. If there was an error, that error should be surfaced to the user and the command aborted. Is there a reason to intentionally silence it?
  2. What are the cases when commit == nil?

Copy link
Contributor Author

@bchadwic bchadwic Dec 9, 2021

Choose a reason for hiding this comment

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

Oops, commit should never be nil if err is nil.

I only silenced the error because that was how the code was before:

5bdaab8

I think it might be silenced because the git error wouldn't be understandable.

fatal: ambiguous argument 'HEAD': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'
/usr/bin/git: exit status 128

Edit: if the retrieval part of LastCommit fails, maybe we should return a custom message, that way we can return err?
git.LastCommit: "a previous commit could not be found for this repository"
api.LastCommit: "a previous commit for OWNER/REPO could not be found"

opts: BrowseOptions{
CommitFlag: true,
LastCommit: git.LastCommit,
GitClient: &localGitClient{},
Copy link
Contributor

Choose a reason for hiding this comment

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

One nice thing about GitClient now being an interface is that you can pass any fake implementation for tests, as long as it satisfies the interface. You can make a test git client that returns a static commit information on LastCommit without having to look it up over either HTTP or by shelling out to git. Then, you don't have to stub either HTTP calls nor git calls in tests anymore 🎉

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.

Fantastic! Thank you for your work and your patience

@mislav mislav merged commit 4bbbf46 into cli:trunk Dec 13, 2021
@bchadwic bchadwic deleted the repo-commit-combo branch December 14, 2021 04:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external pull request originating outside of the CLI core team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Excuting "gh browse gh browse -c -R owner/repo" inside another repo workspace uses the last commit of the workspace repo

3 participants