diff --git a/pkg/cmd/browse/browse.go b/pkg/cmd/browse/browse.go index a85b8ab7d..d05713956 100644 --- a/pkg/cmd/browse/browse.go +++ b/pkg/cmd/browse/browse.go @@ -44,6 +44,7 @@ type BrowseOptions struct { SettingsFlag bool WikiFlag bool ActionsFlag bool + BlameFlag bool NoBrowserFlag bool HasRepoOverride bool } @@ -91,6 +92,9 @@ func NewCmdBrowse(f *cmdutil.Factory, runF func(*BrowseOptions) error) *cobra.Co # Open main.go at line 312 $ gh browse main.go:312 + # Open blame view for main.go at line 312 + $ gh browse main.go:312 --blame + # Open main.go with the repository at head of bug-fix branch $ gh browse main.go --branch bug-fix @@ -141,6 +145,10 @@ func NewCmdBrowse(f *cmdutil.Factory, runF func(*BrowseOptions) error) *cobra.Co return err } + if opts.BlameFlag && opts.SelectorArg == "" { + return cmdutil.FlagErrorf("`--blame` requires a file path argument") + } + if (isNumber(opts.SelectorArg) || isCommit(opts.SelectorArg)) && (opts.Branch != "" || opts.Commit != "") { return cmdutil.FlagErrorf("%q is an invalid argument when using `--branch` or `--commit`", opts.SelectorArg) } @@ -163,6 +171,7 @@ func NewCmdBrowse(f *cmdutil.Factory, runF func(*BrowseOptions) error) *cobra.Co cmd.Flags().BoolVarP(&opts.WikiFlag, "wiki", "w", false, "Open repository wiki") cmd.Flags().BoolVarP(&opts.ActionsFlag, "actions", "a", false, "Open repository actions") cmd.Flags().BoolVarP(&opts.SettingsFlag, "settings", "s", false, "Open repository settings") + cmd.Flags().BoolVar(&opts.BlameFlag, "blame", false, "Open blame view for a file") cmd.Flags().BoolVarP(&opts.NoBrowserFlag, "no-browser", "n", false, "Print destination URL instead of opening the browser") cmd.Flags().StringVarP(&opts.Commit, "commit", "c", "", "Select another commit by passing in the commit SHA, default is the last commit") cmd.Flags().StringVarP(&opts.Branch, "branch", "b", "", "Select another branch by passing in the branch name") @@ -272,9 +281,16 @@ func parseSection(baseRepo ghrepo.Interface, opts *BrowseOptions) (string, error } else { rangeFragment = fmt.Sprintf("L%d", rangeStart) } + if opts.BlameFlag { + return fmt.Sprintf("blame/%s/%s#%s", escapePath(ref), escapePath(filePath), rangeFragment), nil + } return fmt.Sprintf("blob/%s/%s?plain=1#%s", escapePath(ref), escapePath(filePath), rangeFragment), nil } + if opts.BlameFlag { + return fmt.Sprintf("blame/%s/%s", escapePath(ref), escapePath(filePath)), nil + } + return strings.TrimSuffix(fmt.Sprintf("tree/%s/%s", escapePath(ref), escapePath(filePath)), "/"), nil } diff --git a/pkg/cmd/browse/browse_test.go b/pkg/cmd/browse/browse_test.go index 6d4476f3a..c321fbdbb 100644 --- a/pkg/cmd/browse/browse_test.go +++ b/pkg/cmd/browse/browse_test.go @@ -207,6 +207,29 @@ func TestNewCmdBrowse(t *testing.T) { cli: "de07febc26e19000f8c9e821207f3bc34a3c8038 --commit=12a4", wantsErr: true, }, + { + name: "blame flag", + cli: "main.go --blame", + wants: BrowseOptions{ + BlameFlag: true, + SelectorArg: "main.go", + }, + wantsErr: false, + }, + { + name: "blame flag without file argument", + cli: "--blame", + wantsErr: true, + }, + { + name: "blame flag with line number", + cli: "main.go:312 --blame", + wants: BrowseOptions{ + BlameFlag: true, + SelectorArg: "main.go:312", + }, + wantsErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -239,6 +262,7 @@ func TestNewCmdBrowse(t *testing.T) { assert.Equal(t, tt.wants.SettingsFlag, opts.SettingsFlag) assert.Equal(t, tt.wants.ActionsFlag, opts.ActionsFlag) assert.Equal(t, tt.wants.Commit, opts.Commit) + assert.Equal(t, tt.wants.BlameFlag, opts.BlameFlag) }) } } @@ -595,6 +619,61 @@ func Test_runBrowse(t *testing.T) { expectedURL: "https://github.com/bchadwic/test/tree/trunk/77507cd94ccafcf568f8560cfecde965fcfa63e7.txt", wantsErr: false, }, + { + name: "file with blame flag", + opts: BrowseOptions{ + SelectorArg: "path/to/file.txt", + BlameFlag: true, + }, + baseRepo: ghrepo.New("owner", "repo"), + defaultBranch: "main", + expectedURL: "https://github.com/owner/repo/blame/main/path/to/file.txt", + wantsErr: false, + }, + { + name: "file with blame flag and line number", + opts: BrowseOptions{ + SelectorArg: "path/to/file.txt:42", + BlameFlag: true, + }, + baseRepo: ghrepo.New("owner", "repo"), + defaultBranch: "main", + expectedURL: "https://github.com/owner/repo/blame/main/path/to/file.txt#L42", + wantsErr: false, + }, + { + name: "file with blame flag and line range", + opts: BrowseOptions{ + SelectorArg: "path/to/file.txt:10-20", + BlameFlag: true, + }, + baseRepo: ghrepo.New("owner", "repo"), + defaultBranch: "main", + expectedURL: "https://github.com/owner/repo/blame/main/path/to/file.txt#L10-L20", + wantsErr: false, + }, + { + name: "file with blame flag and branch", + opts: BrowseOptions{ + SelectorArg: "main.go:100", + BlameFlag: true, + Branch: "feature-branch", + }, + baseRepo: ghrepo.New("owner", "repo"), + expectedURL: "https://github.com/owner/repo/blame/feature-branch/main.go#L100", + wantsErr: false, + }, + { + name: "file with blame flag and commit", + opts: BrowseOptions{ + SelectorArg: "src/app.js:50", + BlameFlag: true, + Commit: "abc123", + }, + baseRepo: ghrepo.New("owner", "repo"), + expectedURL: "https://github.com/owner/repo/blame/abc123/src/app.js#L50", + wantsErr: false, + }, } for _, tt := range tests {