Skip to content

Commit df36a08

Browse files
committed
Extract concepts of local and remote git repos
The `browse` command queries the GitClient object it received for information like "LastCommit" and "PathPrefix". Depending on the `--repo` flag, the repository queried will be either the local git repository (normal case) or the remote git repository via the GitHub API (in case of the repo override). Each git query method now supports passing a `context.Context` object to support cancellation at a lower level.
1 parent 4bbbf46 commit df36a08

File tree

5 files changed

+182
-96
lines changed

5 files changed

+182
-96
lines changed

api/queries_repo.go

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -524,26 +524,6 @@ func ForkRepo(client *Client, repo ghrepo.Interface, org string) (*Repository, e
524524
}, nil
525525
}
526526

527-
func LastCommit(client *Client, repo ghrepo.Interface) (*Commit, error) {
528-
var responseData struct {
529-
Repository struct {
530-
DefaultBranchRef struct {
531-
Target struct {
532-
Commit `graphql:"... on Commit"`
533-
}
534-
}
535-
} `graphql:"repository(owner: $owner, name: $repo)"`
536-
}
537-
variables := map[string]interface{}{
538-
"owner": githubv4.String(repo.RepoOwner()), "repo": githubv4.String(repo.RepoName()),
539-
}
540-
gql := graphQLClient(client.http, repo.RepoHost())
541-
if err := gql.QueryNamed(context.Background(), "LastCommit", &responseData, variables); err != nil {
542-
return nil, err
543-
}
544-
return &responseData.Repository.DefaultBranchRef.Target.Commit, nil
545-
}
546-
547527
// RepoFindForks finds forks of the repo that are affiliated with the viewer
548528
func RepoFindForks(client *Client, repo ghrepo.Interface, limit int) ([]*Repository, error) {
549529
result := struct {

git/local_repo.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package git
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"os/exec"
7+
"strings"
8+
9+
"github.com/cli/safeexec"
10+
)
11+
12+
type LocalRepo struct {
13+
gitExePath string
14+
gitExeErr error
15+
}
16+
17+
func (c *LocalRepo) PathPrefix(ctx context.Context) (string, error) {
18+
showCmd, err := c.gitCommand(ctx, "rev-parse", "--show-prefix")
19+
if err != nil {
20+
return "", err
21+
}
22+
output, err := showCmd.Output()
23+
return strings.TrimRight(string(output), "\n"), err
24+
}
25+
26+
func (c *LocalRepo) LastCommit(ctx context.Context) (*Commit, error) {
27+
showCmd, err := c.gitCommand(ctx, "-c", "log.ShowSignature=false", "show", "-s", "--pretty=format:%H,%s", "HEAD")
28+
if err != nil {
29+
return nil, err
30+
}
31+
output, err := showCmd.Output()
32+
if err != nil {
33+
return nil, err
34+
}
35+
idx := bytes.IndexByte(output, ',')
36+
return &Commit{
37+
Sha: string(output[0:idx]),
38+
Title: strings.TrimSpace(string(output[idx+1:])),
39+
}, nil
40+
}
41+
42+
func (c *LocalRepo) gitCommand(ctx context.Context, args ...string) (*exec.Cmd, error) {
43+
gitExe, err := c.gitExe()
44+
if err != nil {
45+
return nil, err
46+
}
47+
cmd := exec.CommandContext(ctx, gitExe, args...)
48+
return cmd, nil
49+
}
50+
51+
func (c *LocalRepo) gitExe() (string, error) {
52+
if c.gitExePath == "" && c.gitExeErr == nil {
53+
c.gitExePath, c.gitExeErr = safeexec.LookPath("git")
54+
}
55+
return c.gitExePath, c.gitExeErr
56+
}

git/remoterepo/remote_repo.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package remoterepo
2+
3+
import (
4+
"context"
5+
"errors"
6+
"net/http"
7+
8+
"github.com/cli/cli/v2/git"
9+
"github.com/cli/cli/v2/internal/ghinstance"
10+
"github.com/cli/cli/v2/internal/ghrepo"
11+
graphql "github.com/cli/shurcooL-graphql"
12+
"github.com/shurcooL/githubv4"
13+
)
14+
15+
type Client struct {
16+
// TODO: accept interfaces instead of funcs
17+
Repo func() (ghrepo.Interface, error)
18+
HTTPClient func() (*http.Client, error)
19+
}
20+
21+
var ErrNotImplemented = errors.New("not implemented")
22+
23+
func (c *Client) PathPrefix(ctx context.Context) (string, error) {
24+
return "", ErrNotImplemented
25+
}
26+
27+
func (c *Client) LastCommit(ctx context.Context) (*git.Commit, error) {
28+
httpClient, err := c.HTTPClient()
29+
if err != nil {
30+
return nil, err
31+
}
32+
repo, err := c.Repo()
33+
if err != nil {
34+
return nil, err
35+
}
36+
37+
var responseData struct {
38+
Repository struct {
39+
DefaultBranchRef struct {
40+
Target struct {
41+
Commit struct {
42+
OID string `graphql:"oid"`
43+
MessageHeadline string `graphql:"messageHeadline"`
44+
} `graphql:"... on Commit"`
45+
}
46+
}
47+
} `graphql:"repository(owner: $owner, name: $repo)"`
48+
}
49+
50+
variables := map[string]interface{}{
51+
"owner": githubv4.String(repo.RepoOwner()),
52+
"repo": githubv4.String(repo.RepoName()),
53+
}
54+
55+
gql := graphql.NewClient(ghinstance.GraphQLEndpoint(repo.RepoHost()), httpClient)
56+
if err := gql.QueryNamed(ctx, "RepositoryLastCommit", &responseData, variables); err != nil {
57+
return nil, err
58+
}
59+
60+
commit := responseData.Repository.DefaultBranchRef.Target.Commit
61+
return &git.Commit{
62+
Sha: commit.OID,
63+
Title: commit.MessageHeadline,
64+
}, nil
65+
}

pkg/cmd/browse/browse.go

Lines changed: 27 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package browse
22

33
import (
4+
"context"
5+
"errors"
46
"fmt"
57
"net/http"
68
"net/url"
@@ -12,6 +14,7 @@ import (
1214
"github.com/MakeNowJust/heredoc"
1315
"github.com/cli/cli/v2/api"
1416
"github.com/cli/cli/v2/git"
17+
"github.com/cli/cli/v2/git/remoterepo"
1518
"github.com/cli/cli/v2/internal/ghrepo"
1619
"github.com/cli/cli/v2/pkg/cmdutil"
1720
"github.com/cli/cli/v2/pkg/iostreams"
@@ -23,13 +26,17 @@ type browser interface {
2326
Browse(string) error
2427
}
2528

29+
type gitClient interface {
30+
LastCommit(context.Context) (*git.Commit, error)
31+
PathPrefix(context.Context) (string, error)
32+
}
33+
2634
type BrowseOptions struct {
27-
BaseRepo func() (ghrepo.Interface, error)
28-
Browser browser
29-
HttpClient func() (*http.Client, error)
30-
IO *iostreams.IOStreams
31-
PathFromRepoRoot func() string
32-
GitClient gitClient
35+
BaseRepo func() (ghrepo.Interface, error)
36+
Browser browser
37+
HttpClient func() (*http.Client, error)
38+
IO *iostreams.IOStreams
39+
GitClient gitClient
3340

3441
SelectorArg string
3542

@@ -43,11 +50,10 @@ type BrowseOptions struct {
4350

4451
func NewCmdBrowse(f *cmdutil.Factory, runF func(*BrowseOptions) error) *cobra.Command {
4552
opts := &BrowseOptions{
46-
Browser: f.Browser,
47-
HttpClient: f.HttpClient,
48-
IO: f.IOStreams,
49-
PathFromRepoRoot: git.PathFromRepoRoot,
50-
GitClient: &localGitClient{},
53+
Browser: f.Browser,
54+
HttpClient: f.HttpClient,
55+
IO: f.IOStreams,
56+
GitClient: &git.LocalRepo{},
5157
}
5258

5359
cmd := &cobra.Command{
@@ -100,7 +106,10 @@ func NewCmdBrowse(f *cmdutil.Factory, runF func(*BrowseOptions) error) *cobra.Co
100106
return err
101107
}
102108
if cmd.Flags().Changed("repo") {
103-
opts.GitClient = &remoteGitClient{opts.BaseRepo, opts.HttpClient}
109+
opts.GitClient = &remoterepo.Client{
110+
Repo: opts.BaseRepo,
111+
HTTPClient: opts.HttpClient,
112+
}
104113
}
105114

106115
if runF != nil {
@@ -128,7 +137,7 @@ func runBrowse(opts *BrowseOptions) error {
128137
}
129138

130139
if opts.CommitFlag {
131-
commit, err := opts.GitClient.LastCommit()
140+
commit, err := opts.GitClient.LastCommit(context.TODO())
132141
if err != nil {
133142
return err
134143
}
@@ -217,7 +226,11 @@ func parseFile(opts BrowseOptions, f string) (p string, start int, end int, err
217226

218227
p = filepath.ToSlash(parts[0])
219228
if !path.IsAbs(p) {
220-
p = path.Join(opts.PathFromRepoRoot(), p)
229+
prefix, err := opts.GitClient.PathPrefix(context.TODO())
230+
if err != nil && !errors.Is(err, remoterepo.ErrNotImplemented) {
231+
return "", 0, 0, err
232+
}
233+
p = path.Join(prefix, p)
221234
if p == "." || strings.HasPrefix(p, "..") {
222235
p = ""
223236
}
@@ -251,33 +264,3 @@ func isNumber(arg string) bool {
251264
_, err := strconv.Atoi(arg)
252265
return err == nil
253266
}
254-
255-
// gitClient is used to implement functions that can be performed on both local and remote git repositories
256-
type gitClient interface {
257-
LastCommit() (*git.Commit, error)
258-
}
259-
260-
type localGitClient struct{}
261-
262-
type remoteGitClient struct {
263-
repo func() (ghrepo.Interface, error)
264-
httpClient func() (*http.Client, error)
265-
}
266-
267-
func (gc *localGitClient) LastCommit() (*git.Commit, error) { return git.LastCommit() }
268-
269-
func (gc *remoteGitClient) LastCommit() (*git.Commit, error) {
270-
httpClient, err := gc.httpClient()
271-
if err != nil {
272-
return nil, err
273-
}
274-
repo, err := gc.repo()
275-
if err != nil {
276-
return nil, err
277-
}
278-
commit, err := api.LastCommit(api.NewClientFromHTTP(httpClient), repo)
279-
if err != nil {
280-
return nil, err
281-
}
282-
return &git.Commit{Sha: commit.OID}, nil
283-
}

0 commit comments

Comments
 (0)