Compare commits

...
Author SHA1 Message Date
Stefan HallerandClaude Opus 5 83450db146 Open the selected diff line in the branch's pull request
Reading a change in lazygit and saying something about it on GitHub
means finding the line again in the browser: open the pull request, find
the commit, find the file, scroll to the line. The line is already under
the cursor here.

Bind G in the focused main view, the key the commits panel opens the
pull request with, to open it at the line the selection is on. The URL
names the commits whose diff is on screen, so that the line numbers of
the diff are the ones the page shows, the file by the SHA-256 of its
repo-relative path, and the line by the side of the diff it is on: R for
the new version of the file, L for the old one, where a deleted line is.
GitHub documents none of that; the form was read off the URLs its own
pages carry.

One commit is named by its hash. A range of them is named by the commit
the range starts after and the commit it ends at, the form the chooser
above a pull request's files uses. The commit a range starts after is
the parent of its oldest commit; where the range starts where the pull
request itself does, that parent is none of the pull request's own
commits, and the keyword BASE stands for it.

Which branch's pull request that is depends on the panel beneath: the
checked-out branch below the commits panel, the branch drilled into
below the sub-commits panel, and whichever of those the commit files
panel was entered from. Panels showing a diff that no pull request has a
view of don't answer, and the command isn't offered over their diffs at
all.

Neither is it offered over a diff that is not the commit's own, where
the line numbers on screen are not the ones the page shows: a diff
against another ref in diffing mode, and the custom patch, whose lines
sit at the numbers the patch gives them.

A pull request holds only the commits of its branch that are pushed, and
its pages say they can't find any other commit. So the command refuses
where a commit of the diff is not one of the pull request's. Amend a
commit in the middle of the branch, and the diffs of the commits below
it still open; the ones above it sit on hashes the remote doesn't have.
A commit from before the branch, in a main branch already, is refused
too.

Only GitHub pull requests are known, since that is where the pull
request data comes from. The whole path can't be exercised headlessly:
no pull request reaches the model without a GitHub token, so the test
covers where the command is offered and the three reasons it refuses.
The URL is unit-tested instead, both the anchor of a line and the way
the commits are named.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-27 18:26:56 +02:00
Stefan HallerandClaude Opus 5 8842f17186 Ask for the panel beneath the focused main view in one place
Two questions the pane answers from the panel beneath it each reach for
it themselves, guard included. Opening a line in a pull request needs it
twice more, for the branch and for the commit.

Extract sidePanelBeneath, which is also where the guard against asking
for the panel beneath an off-stack pane now belongs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-27 18:26:56 +02:00
Stefan HallerandClaude Opus 5 3603883e83 Name a diff line's file in the repo's terms in one place
A diff line carries the absolute path of its file, and both panels
acting on such a line turn it into the repo-relative one git speaks
themselves. Opening a line in a pull request needs that path too, to
name the file to GitHub by it.

Extract repoRelativePath, and have both panels use it. The files panel
gains the check for a path outside the repo that the other one had; a
path it used to pass on matches no file of the working tree either.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-27 18:26:56 +02:00
Stefan HallerandClaude Opus 5 c0efca5ae6 Ask one place whether a branch has a pull request
Two panels offer a branch's pull request today, and each looks it up in
the model's map itself and builds the same disabled reason from the same
string. The focused main view is about to offer a line of the diff in
that pull request, which would make three.

Put the lookup and the disabled reason on the host helper, beside the
pull request URL it already builds, and have both panels ask it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-27 18:26:56 +02:00
25 changed files with 702 additions and 50 deletions
@@ -234,6 +234,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <right>, l `` | Go to next hunk | |
| `` N `` | Go to previous file | |
| `` n `` | Go to next file | |
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
| `` <esc> `` | Exit back to side panel | |
| `` c `` | Commit | Commit staged changes. |
| `` w `` | Commit changes without pre-commit hook | |
@@ -316,6 +317,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <right>, l `` | Go to next hunk | |
| `` N `` | Go to previous file | |
| `` n `` | Go to next file | |
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
| `` <esc> `` | Exit back to side panel | |
| `` c `` | Commit | Commit staged changes. |
| `` w `` | Commit changes without pre-commit hook | |
@@ -203,6 +203,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <right>, l `` | 次のハンクに移動 | |
| `` N `` | Go to previous file | |
| `` n `` | Go to next file | |
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
| `` <esc> `` | サイドパネルに戻る | |
| `` c `` | コミット | ステージされた変更をコミットします。 |
| `` w `` | pre-commitフックなしで変更をコミット | |
@@ -293,6 +294,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <right>, l `` | 次のハンクに移動 | |
| `` N `` | Go to previous file | |
| `` n `` | Go to next file | |
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
| `` <esc> `` | サイドパネルに戻る | |
| `` c `` | コミット | ステージされた変更をコミットします。 |
| `` w `` | pre-commitフックなしで変更をコミット | |
@@ -95,6 +95,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <right>, l `` | 다음 hunk를 선택 | |
| `` N `` | Go to previous file | |
| `` n `` | Go to next file | |
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
| `` <esc> `` | Exit back to side panel | |
| `` c `` | 커밋 변경내용 | 스테이징된 변경 사항 커밋. |
| `` w `` | Commit changes without pre-commit hook | |
@@ -188,6 +189,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <right>, l `` | 다음 hunk를 선택 | |
| `` N `` | Go to previous file | |
| `` n `` | Go to next file | |
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
| `` <esc> `` | Exit back to side panel | |
| `` c `` | 커밋 변경내용 | 스테이징된 변경 사항 커밋. |
| `` w `` | Commit changes without pre-commit hook | |
@@ -242,6 +242,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <right>, l `` | Selecteer de volgende hunk | |
| `` N `` | Go to previous file | |
| `` n `` | Go to next file | |
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
| `` <esc> `` | Exit back to side panel | |
| `` c `` | Commit veranderingen | Commit gestagede wijzigingen. |
| `` w `` | Commit veranderingen zonder pre-commit hook | |
@@ -316,6 +317,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <right>, l `` | Selecteer de volgende hunk | |
| `` N `` | Go to previous file | |
| `` n `` | Go to next file | |
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
| `` <esc> `` | Exit back to side panel | |
| `` c `` | Commit veranderingen | Commit gestagede wijzigingen. |
| `` w `` | Commit veranderingen zonder pre-commit hook | |
@@ -110,6 +110,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <right>, l `` | Idź do następnego fragmentu | |
| `` N `` | Go to previous file | |
| `` n `` | Go to next file | |
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
| `` <esc> `` | Exit back to side panel | |
| `` c `` | Commit | Zatwierdź zmiany zatwierdzone. |
| `` w `` | Zatwierdź zmiany bez hooka pre-commit | |
@@ -211,6 +212,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <right>, l `` | Idź do następnego fragmentu | |
| `` N `` | Go to previous file | |
| `` n `` | Go to next file | |
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
| `` <esc> `` | Exit back to side panel | |
| `` c `` | Commit | Zatwierdź zmiany zatwierdzone. |
| `` w `` | Zatwierdź zmiany bez hooka pre-commit | |
@@ -246,6 +246,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <right>, l `` | Ir para o próximo trecho | |
| `` N `` | Go to previous file | |
| `` n `` | Go to next file | |
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
| `` <esc> `` | Exit back to side panel | |
| `` c `` | Commit | Submeter mudanças em staging |
| `` w `` | Fazer commit de alterações sem pré-commit | |
@@ -325,6 +326,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <right>, l `` | Ir para o próximo trecho | |
| `` N `` | Go to previous file | |
| `` n `` | Go to next file | |
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
| `` <esc> `` | Exit back to side panel | |
| `` c `` | Commit | Submeter mudanças em staging |
| `` w `` | Fazer commit de alterações sem pré-commit | |
@@ -85,6 +85,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <right>, l `` | Выбрать следующую часть | |
| `` N `` | Go to previous file | |
| `` n `` | Go to next file | |
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
| `` <esc> `` | Exit back to side panel | |
| `` c `` | Сохранить изменения | Commit staged changes. |
| `` w `` | Закоммитить изменения без предварительного хука коммита | |
@@ -110,6 +111,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <right>, l `` | Выбрать следующую часть | |
| `` N `` | Go to previous file | |
| `` n `` | Go to next file | |
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
| `` <esc> `` | Exit back to side panel | |
| `` c `` | Сохранить изменения | Commit staged changes. |
| `` w `` | Закоммитить изменения без предварительного хука коммита | |
@@ -281,6 +281,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <right>, l `` | 选择下一个区块 | |
| `` N `` | Go to previous file | |
| `` n `` | Go to next file | |
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
| `` <esc> `` | 退出回到侧边面板 | |
| `` c `` | 提交变更 | 提交暂存文件 |
| `` w `` | 提交变更而无需预先提交钩子 | |
@@ -322,6 +323,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <right>, l `` | 选择下一个区块 | |
| `` N `` | Go to previous file | |
| `` n `` | Go to next file | |
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
| `` <esc> `` | 退出回到侧边面板 | |
| `` c `` | 提交变更 | 提交暂存文件 |
| `` w `` | 提交变更而无需预先提交钩子 | |
@@ -70,6 +70,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <right>, l `` | 選擇下一段 | |
| `` N `` | Go to previous file | |
| `` n `` | Go to next file | |
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
| `` <esc> `` | 退出回到側邊面板 | |
| `` c `` | 提交變更 | 提交暫存區變更 |
| `` w `` | 沒有預提交 hook 就提交更改 | |
@@ -344,6 +345,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <right>, l `` | 選擇下一段 | |
| `` N `` | Go to previous file | |
| `` n `` | Go to next file | |
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
| `` <esc> `` | 退出回到側邊面板 | |
| `` c `` | 提交變更 | 提交暫存區變更 |
| `` w `` | 沒有預提交 hook 就提交更改 | |
+24 -4
View File
@@ -19,16 +19,36 @@ type CommitFilesContext struct {
}
var (
_ types.IListContext = (*CommitFilesContext)(nil)
_ types.DiffableContext = (*CommitFilesContext)(nil)
_ types.IFilterableContext = (*CommitFilesContext)(nil)
_ types.DiffMainViewContext = (*CommitFilesContext)(nil)
_ types.IListContext = (*CommitFilesContext)(nil)
_ types.DiffableContext = (*CommitFilesContext)(nil)
_ types.IFilterableContext = (*CommitFilesContext)(nil)
_ types.DiffMainViewContext = (*CommitFilesContext)(nil)
_ types.PullRequestDiffContext = (*CommitFilesContext)(nil)
)
func (self *CommitFilesContext) GetDiffMainViewType() types.DiffMainViewType {
return types.DiffMainViewTypePatchBuilding
}
// BranchForPullRequest asks the panel this one was entered from: the files are a
// commit's, and which branch's pull request that commit is up for review in is known
// there rather than here.
func (self *CommitFilesContext) BranchForPullRequest() string {
if parent, ok := self.GetParentContext().(types.PullRequestDiffContext); ok {
return parent.BranchForPullRequest()
}
return ""
}
// CommitsForPullRequest asks the panel this one was entered from as well: the files
// listed here are those of the commits selected there.
func (self *CommitFilesContext) CommitsForPullRequest() ([]*models.Commit, string) {
if parent, ok := self.GetParentContext().(types.PullRequestDiffContext); ok {
return parent.CommitsForPullRequest()
}
return nil, ""
}
func NewCommitFilesContext(c *ContextCommon) *CommitFilesContext {
viewModel := filetree.NewCommitFileTreeViewModel(
func() []*models.CommitFile { return c.Model().CommitFiles },
+58 -4
View File
@@ -31,16 +31,70 @@ type commitDropIndicator struct {
}
var (
_ types.IListContext = (*LocalCommitsContext)(nil)
_ types.DiffableContext = (*LocalCommitsContext)(nil)
_ types.ISearchableContext = (*LocalCommitsContext)(nil)
_ types.DiffMainViewContext = (*LocalCommitsContext)(nil)
_ types.IListContext = (*LocalCommitsContext)(nil)
_ types.DiffableContext = (*LocalCommitsContext)(nil)
_ types.ISearchableContext = (*LocalCommitsContext)(nil)
_ types.DiffMainViewContext = (*LocalCommitsContext)(nil)
_ types.PullRequestDiffContext = (*LocalCommitsContext)(nil)
)
func (self *LocalCommitsContext) GetDiffMainViewType() types.DiffMainViewType {
return types.DiffMainViewTypePatchBuilding
}
// BranchForPullRequest returns the checked-out branch: this panel shows its commits,
// so a pull request for it is where they are up for review.
func (self *LocalCommitsContext) BranchForPullRequest() string {
return self.ListContextTrait.c.Model().CheckedOutBranch
}
func (self *LocalCommitsContext) CommitsForPullRequest() ([]*models.Commit, string) {
selectedCommits, _, _ := self.GetSelectedItems()
commits := commitsShownInDiff(selectedCommits, self.GetSelected(), self.GetSelectedRefRangeForDiffFiles())
return commits, pullRequestBaseForCommits(self.GetCommits(), commits)
}
// commitsShownInDiff returns the commits whose combined diff a panel listing a branch's
// commits renders into the main view: the selected range where it has a range to diff,
// and the commit at the cursor otherwise. The panel hands the same two to
// DiffHelper.GetUpdateTaskForRenderingCommitsDiff, so anything acting on the diff on
// screen acts on the commits that diff is of.
func commitsShownInDiff(
selectedCommits []*models.Commit, commitAtCursor *models.Commit, refRange *types.RefRange,
) []*models.Commit {
if refRange != nil {
return selectedCommits
}
if commitAtCursor == nil {
return nil
}
return []*models.Commit{commitAtCursor}
}
// pullRequestBaseForCommits returns the hash of the commit the diff of the given commits
// starts after: the parent of the oldest of them. A pull request holds only the commits
// of the branch that are pushed, so a parent that isn't pushed is none of its own. The
// diff then starts where the pull request itself does, and "" says so.
func pullRequestBaseForCommits(allCommits []*models.Commit, commits []*models.Commit) string {
if len(commits) == 0 {
return ""
}
oldest := commits[len(commits)-1]
if oldest.IsFirstCommit() {
return ""
}
parentHash := oldest.Parents()[0]
parentIsInPullRequest := lo.ContainsBy(allCommits, func(commit *models.Commit) bool {
return commit.Hash() == parentHash && commit.Status == models.StatusPushed
})
if !parentIsInPullRequest {
return ""
}
return parentHash
}
func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
dropIndicator := &commitDropIndicator{insertionIndex: -1}
viewModel := NewLocalCommitsViewModel(
@@ -4,8 +4,12 @@ import (
"testing"
"time"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
)
@@ -51,3 +55,101 @@ func TestAddMovingCommitsIndicator(t *testing.T) {
},
}, items)
}
func TestCommitsShownInDiff(t *testing.T) {
hashPool := &utils.StringPool{}
newer := models.NewCommit(hashPool, models.NewCommitOpts{Hash: "newer"})
older := models.NewCommit(hashPool, models.NewCommitOpts{Hash: "older"})
selected := []*models.Commit{newer, older}
scenarios := []struct {
name string
selectedCommit *models.Commit
refRange *types.RefRange
expected []*models.Commit
}{
{
name: "a range is diffed as a whole",
selectedCommit: newer,
refRange: &types.RefRange{From: older, To: newer},
expected: selected,
},
{
name: "without a range to diff, only the commit at the cursor is",
selectedCommit: newer,
expected: []*models.Commit{newer},
},
{
name: "nothing is diffed while nothing is selected",
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
assert.Equal(t, s.expected, commitsShownInDiff(selected, s.selectedCommit, s.refRange))
})
}
}
func TestPullRequestBaseForCommits(t *testing.T) {
hashPool := &utils.StringPool{}
commit := func(hash string, parent string, status models.CommitStatus) *models.Commit {
return models.NewCommit(hashPool, models.NewCommitOpts{
Hash: hash,
Parents: lo.Ternary(parent == "", []string{}, []string{parent}),
Status: status,
})
}
// A branch of three pushed commits whose tip was amended, on top of a commit that
// is in a main branch already, as the panel lists them: newest first.
amended := commit("amended", "third", models.StatusUnpushed)
third := commit("third", "second", models.StatusPushed)
second := commit("second", "first", models.StatusPushed)
first := commit("first", "merged", models.StatusPushed)
merged := commit("merged", "ancient", models.StatusMerged)
ancient := commit("ancient", "", models.StatusMerged)
allCommits := []*models.Commit{amended, third, second, first, merged, ancient}
scenarios := []struct {
name string
commits []*models.Commit
expected string
}{
{
name: "a single commit starts after its parent",
commits: []*models.Commit{second},
expected: "first",
},
{
name: "a range starts after the parent of its oldest commit",
commits: []*models.Commit{third, second},
expected: "first",
},
{
name: "the pull request's first commit starts where the pull request does",
commits: []*models.Commit{first},
},
{
name: "so does a range reaching down to it",
commits: []*models.Commit{third, second, first},
},
{
name: "and so does the first commit of the repository",
commits: []*models.Commit{ancient},
},
{
name: "a parent the panel doesn't list is none of the pull request's",
commits: []*models.Commit{commit("elsewhere", "unlisted", models.StatusPushed)},
},
{
name: "nothing is shown",
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
assert.Equal(t, s.expected, pullRequestBaseForCommits(allCommits, s.commits))
})
}
}
+21 -4
View File
@@ -21,16 +21,33 @@ type SubCommitsContext struct {
}
var (
_ types.IListContext = (*SubCommitsContext)(nil)
_ types.DiffableContext = (*SubCommitsContext)(nil)
_ types.ISearchableContext = (*SubCommitsContext)(nil)
_ types.DiffMainViewContext = (*SubCommitsContext)(nil)
_ types.IListContext = (*SubCommitsContext)(nil)
_ types.DiffableContext = (*SubCommitsContext)(nil)
_ types.ISearchableContext = (*SubCommitsContext)(nil)
_ types.DiffMainViewContext = (*SubCommitsContext)(nil)
_ types.PullRequestDiffContext = (*SubCommitsContext)(nil)
)
func (self *SubCommitsContext) GetDiffMainViewType() types.DiffMainViewType {
return types.DiffMainViewTypePatchBuilding
}
// BranchForPullRequest returns the branch this panel was entered from, whose commits it
// shows. The panel is also entered from a tag, a remote branch and the reflog, none of
// which a pull request is made from.
func (self *SubCommitsContext) BranchForPullRequest() string {
if branch, ok := self.GetRef().(*models.Branch); ok {
return branch.Name
}
return ""
}
func (self *SubCommitsContext) CommitsForPullRequest() ([]*models.Commit, string) {
selectedCommits, _, _ := self.GetSelectedItems()
commits := commitsShownInDiff(selectedCommits, self.GetSelected(), self.GetSelectedRefRangeForDiffFiles())
return commits, pullRequestBaseForCommits(self.GetCommits(), commits)
}
func NewSubCommitsContext(
c *ContextCommon,
) *SubCommitsContext {
+4 -8
View File
@@ -208,7 +208,7 @@ func (self *BranchesController) GetOnRenderToMain() func() {
rendererTask := types.NewRunDiffRendererTask(cmdObj.GetCmd())
task = rendererTask
pr, ok := self.c.Model().PullRequestsMap[branch.Name]
pr, ok := self.c.Helpers().Host.PullRequestForBranch(branch.Name)
if ok && presentation.ShouldShowPrForBranch(pr, branch.Name, self.c.UserConfig()) {
rendererTask.Prefix = presentation.FormatPullRequestHeader(pr, self.c.Tr)
rendererTask.Prefix += strings.Repeat("─", self.c.Contexts().Normal.GetView().InnerWidth()) + "\n"
@@ -462,7 +462,7 @@ func (self *BranchesController) handleCreatePullRequestMenu(selectedBranch *mode
func (self *BranchesController) getPullRequestURL() (string, error) {
branch := self.context().GetSelected()
if pr, ok := self.c.Model().PullRequestsMap[branch.Name]; ok {
if pr, ok := self.c.Helpers().Host.PullRequestForBranch(branch.Name); ok {
return pr.Url, nil
}
@@ -855,15 +855,11 @@ func (self *BranchesController) branchIsReal(branch *models.Branch) *types.Disab
}
func (self *BranchesController) branchHasPR(branch *models.Branch) *types.DisabledReason {
if _, ok := self.c.Model().PullRequestsMap[branch.Name]; !ok {
return &types.DisabledReason{Text: self.c.Tr.NoPullRequestForBranch, ShowErrorInPanel: true}
}
return nil
return self.c.Helpers().Host.NoPullRequestDisabledReason(branch.Name)
}
func (self *BranchesController) openPRInBrowser(branch *models.Branch) error {
pr, ok := self.c.Model().PullRequestsMap[branch.Name]
pr, ok := self.c.Helpers().Host.PullRequestForBranch(branch.Name)
if !ok {
// Should be guarded against by the DisabledReason check, but be defensive in case
// PullRequestsMap was updated concurrently by a background refresh
+1 -7
View File
@@ -2,8 +2,6 @@ package controllers
import (
"fmt"
"path/filepath"
"strings"
"github.com/jesseduffield/generics/set"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
@@ -443,11 +441,7 @@ func (self *CommitDiffActions) patchEndpoints(target *commitDiffTarget) (string,
// patchBuilderPath turns the absolute path a diff line carries into the repo-relative
// one the patch builder keys a file by, and "" for a path that is no file of this repo.
func (self *CommitDiffActions) patchBuilderPath(path string) string {
relativePath, err := filepath.Rel(self.c.Git().RepoPaths.WorktreePath(), path)
if err != nil || strings.HasPrefix(relativePath, "..") {
return ""
}
return filepath.ToSlash(relativePath)
return repoRelativePath(self.c.Git().RepoPaths.WorktreePath(), path)
}
// indexOfTargetCommit finds the commit the diff belongs to among the commits of the
+12
View File
@@ -2,6 +2,7 @@ package controllers
import (
"path"
"path/filepath"
"strings"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
@@ -130,3 +131,14 @@ func isInDir(path string, dir string) bool {
// "." is the root item, which contains every file
return dir == "." || strings.HasPrefix(path, dir+"/")
}
// repoRelativePath turns the absolute path a diff line carries into the one git speaks
// of the file: relative to the worktree, with forward slashes. It is "" for a path that
// is no file of this repo, which a diff renderer's own naming of a line can produce.
func repoRelativePath(worktreePath string, absolutePath string) string {
relativePath, err := filepath.Rel(worktreePath, absolutePath)
if err != nil || strings.HasPrefix(relativePath, "..") {
return ""
}
return filepath.ToSlash(relativePath)
}
+22 -1
View File
@@ -2,9 +2,12 @@ package helpers
import (
"github.com/jesseduffield/lazygit/pkg/commands/hosting_service"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
// this helper just wraps our hosting_service package
// this helper wraps our hosting_service package, and answers what we know about the
// repo's pull requests on it
type HostHelper struct {
c *HelperCommon
@@ -18,6 +21,24 @@ func NewHostHelper(
}
}
// PullRequestForBranch returns the pull request the given local branch is the head of,
// and false where it has none. That covers knowing of no pull requests at all: the repo
// may not be on GitHub, or the answer may not have arrived yet.
func (self *HostHelper) PullRequestForBranch(branchName string) (*models.GithubPullRequest, bool) {
pr, ok := self.c.Model().PullRequestsMap[branchName]
return pr, ok
}
// NoPullRequestDisabledReason disables a command that acts on a branch's pull request
// while that branch has none.
func (self *HostHelper) NoPullRequestDisabledReason(branchName string) *types.DisabledReason {
if _, ok := self.PullRequestForBranch(branchName); !ok {
return &types.DisabledReason{Text: self.c.Tr.NoPullRequestForBranch, ShowErrorInPanel: true}
}
return nil
}
func (self *HostHelper) GetPullRequestURL(from string, to string) (string, error) {
mgr, err := self.getHostingServiceMgr()
if err != nil {
@@ -667,15 +667,11 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
}
func (self *LocalCommitsController) checkedOutBranchHasPR() *types.DisabledReason {
branch := self.c.Model().CheckedOutBranch
if _, ok := self.c.Model().PullRequestsMap[branch]; !ok {
return &types.DisabledReason{Text: self.c.Tr.NoPullRequestForBranch, ShowErrorInPanel: true}
}
return nil
return self.c.Helpers().Host.NoPullRequestDisabledReason(self.c.Model().CheckedOutBranch)
}
func (self *LocalCommitsController) openPRInBrowser() error {
pr, ok := self.c.Model().PullRequestsMap[self.c.Model().CheckedOutBranch]
pr, ok := self.c.Helpers().Host.PullRequestForBranch(self.c.Model().CheckedOutBranch)
if !ok {
// Should be guarded against by the DisabledReason check, but be defensive in case
// PullRequestsMap was updated concurrently by a background refresh
+178 -12
View File
@@ -1,8 +1,13 @@
package controllers
import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"time"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
@@ -154,6 +159,14 @@ func (self *MainViewController) GetKeybindings(opts types.KeybindingsOpts) []*ty
DescriptionFunc: self.diffSelectionDescriptionText(self.c.Tr.NextFileInDiff),
GetDisabledReason: self.diffSelectionDisabledReason,
},
{
Keys: opts.GetKeys(opts.Config.Commits.OpenPullRequestInBrowser),
Handler: self.openPullRequestAtSelectedLine,
Description: self.c.Tr.OpenPullRequestAtSelectedLine,
DescriptionFunc: self.pullRequestDescription(self.c.Tr.OpenPullRequestAtSelectedLine),
GetDisabledReason: self.openPullRequestDisabledReason,
Tooltip: self.c.Tr.OpenPullRequestAtSelectedLineTooltip,
},
{
Keys: opts.GetKeys(opts.Config.Universal.Return),
Handler: self.escape,
@@ -327,17 +340,22 @@ func (self *MainViewController) isDiffView() bool {
return self.diffMainViewType() != types.DiffMainViewTypeNone
}
// sidePanelBeneath returns the side panel this pane is showing the content of, and nil
// when there is none. The IsInStack guard is essential: NextInStack panics for a context
// that isn't in the stack, and GetKeybindings (which leads here) also runs for off-stack
// panes — at startup and while generating the cheatsheets, where the stack is empty.
func (self *MainViewController) sidePanelBeneath() types.Context {
if !self.c.Context().IsInStack(self.context) {
return nil
}
return self.c.Context().NextInStack(self.context)
}
// diffMainViewType reports what the diff in the focused main view belongs to, taken
// from the side panel beneath it, or DiffMainViewTypeNone when this pane isn't on the
// stack or has no diff panel beneath it. The IsInStack guard is essential:
// NextInStack panics for a context that isn't in the stack, and GetKeybindings (which
// leads here) also runs for off-stack panes — at startup and while generating the
// cheatsheets, where the stack is empty.
// stack or has no diff panel beneath it.
func (self *MainViewController) diffMainViewType() types.DiffMainViewType {
if !self.c.Context().IsInStack(self.context) {
return types.DiffMainViewTypeNone
}
if diffContext, ok := self.c.Context().NextInStack(self.context).(types.DiffMainViewContext); ok {
if diffContext, ok := self.sidePanelBeneath().(types.DiffMainViewContext); ok {
return diffContext.GetDiffMainViewType()
}
return types.DiffMainViewTypeNone
@@ -347,10 +365,7 @@ func (self *MainViewController) diffMainViewType() types.DiffMainViewType {
// hand out the diff it rendered there. nil when this pane isn't on the stack, or the
// panel beneath shows no diff.
func (self *MainViewController) diffSource() types.FocusedMainViewDiffSource {
if !self.c.Context().IsInStack(self.context) {
return nil
}
sidePanel := self.c.Context().NextInStack(self.context)
sidePanel := self.sidePanelBeneath()
if sidePanel == nil {
return nil
}
@@ -437,6 +452,19 @@ func (self *MainViewController) diffActionDescription(staging string, patchBuild
}
}
// pullRequestDescription describes a command that acts on the pull request of the
// branch the diff belongs to. Over a diff that belongs to no branch (the working tree's,
// a stash entry's) it describes it as nothing; this keeps the command out of the
// keybindings menu there.
func (self *MainViewController) pullRequestDescription(description string) func() string {
return self.diffSelectionDescription(func() string {
if self.pullRequestBranch() == "" {
return ""
}
return description
})
}
// copySelection copies the selected diff lines to the clipboard — not as the diff
// renderer drew them, but as they read in the diff itself, which is both what you meant
// to copy and the only form a renderer can't have mangled. A selection that is all
@@ -528,6 +556,46 @@ func (self *MainViewController) discardSelectionDisabledReason() *types.Disabled
return nil
}
// openPullRequestDisabledReason disables opening a line in the pull request where the
// pull request has no view of what is on screen. The branch may have no pull request,
// and the pane may be showing a diff that is not the commit's own: a diff against
// another ref, or the custom patch, whose lines sit at the numbers the patch gives them
// rather than the commit's.
func (self *MainViewController) openPullRequestDisabledReason() *types.DisabledReason {
if reason := self.diffSelectionDisabledReason(); reason != nil {
return reason
}
if self.c.Modes().Diffing.Active() {
return &types.DisabledReason{Text: self.c.Tr.NotAvailableInDiffingMode}
}
if self.c.Helpers().DiffLine.ShowsCustomPatch(self.context.GetView()) {
return &types.DisabledReason{Text: self.c.Tr.NotAvailableForCustomPatch}
}
if reason := self.c.Helpers().Host.NoPullRequestDisabledReason(self.pullRequestBranch()); reason != nil {
return reason
}
return self.commitsOutsidePullRequestDisabledReason()
}
// commitsOutsidePullRequestDisabledReason disables opening a line of a diff whose
// commits the pull request doesn't hold: it holds the commits of its branch that are on
// the remote, so an unpushed commit is none of its own, and neither is one that is in a
// main branch already and so from before the branch. Asked for such a commit, its pages
// say they can't find it.
func (self *MainViewController) commitsOutsidePullRequestDisabledReason() *types.DisabledReason {
commits, _ := self.pullRequestCommits()
if lo.EveryBy(commits, func(commit *models.Commit) bool {
return commit.Status == models.StatusPushed
}) {
return nil
}
if len(commits) == 1 {
return &types.DisabledReason{Text: self.c.Tr.CommitNotInPullRequest}
}
return &types.DisabledReason{Text: self.c.Tr.CommitsNotInPullRequest}
}
func (self *MainViewController) onClickInAlreadyFocusedView(opts gocui.ViewMouseBindingOpts) error {
self.selectClickedDiffLine(opts.Y)
return nil
@@ -1049,6 +1117,104 @@ func (self *MainViewController) editDiffLine(viewLine int, beforeEdit func()) er
return self.c.Helpers().Files.EditFileAtLine(info.Path, lineNumber)
}
// openPullRequestAtSelectedLine opens the pull request of the branch whose commit the
// main view is showing the diff of, at the line the selection is on, so that the line
// can be commented on there.
func (self *MainViewController) openPullRequestAtSelectedLine() error {
pr, ok := self.c.Helpers().Host.PullRequestForBranch(self.pullRequestBranch())
if !ok {
// Guarded against by the disabled reason, but a refresh in the background may
// have taken the pull request away since it was asked.
return errors.New(self.c.Tr.NoPullRequestForBranch)
}
commits, baseHash := self.pullRequestCommits()
if len(commits) == 0 {
return nil
}
view := self.context.GetView()
info, ok := self.c.Helpers().DiffLine.GetDiffLineInfo(view, view.SelectedLineIdx())
if !ok {
return nil
}
relativePath := repoRelativePath(self.c.Git().RepoPaths.WorktreePath(), info.Path)
if relativePath == "" {
return nil
}
self.c.LogAction(self.c.Tr.Actions.OpenPullRequest)
url := githubPullRequestLineURL(pr.Url, githubCommitRange(commits, baseHash), relativePath, info)
return self.c.OS().OpenLink(url)
}
// pullRequestBranch returns the branch whose pull request would show the diff in this
// pane, as the panel beneath names it, and "" where no pull request shows it.
func (self *MainViewController) pullRequestBranch() string {
prContext, ok := self.sidePanelBeneath().(types.PullRequestDiffContext)
if !ok {
return ""
}
return prContext.BranchForPullRequest()
}
// pullRequestCommits returns the commits whose diff the pane is showing, and the commit
// that diff starts after, as the panel beneath names them. The diff's line numbers are
// the ones the pull request's page for those commits shows.
func (self *MainViewController) pullRequestCommits() ([]*models.Commit, string) {
prContext, ok := self.sidePanelBeneath().(types.PullRequestDiffContext)
if !ok {
return nil, ""
}
return prContext.CommitsForPullRequest()
}
// githubPullRequestLineURL builds the URL of a line of a file, in the diff a pull request
// shows for the given commits. The file is named by the SHA-256 of its path as git spells
// it, and the line by which side of the diff it is on.
//
// GitHub documents none of this; the form was read off the URLs its own pages carry (see
// https://github.com/orgs/community/discussions/55764).
func githubPullRequestLineURL(
prURL string, commitRange string, relativePath string, info types.DiffLineInfo,
) string {
pathHash := sha256.Sum256([]byte(relativePath))
anchor := "diff-" + hex.EncodeToString(pathHash[:]) + githubDiffLineSuffix(info)
return fmt.Sprintf("%s/changes/%s#%s", prURL, commitRange, anchor)
}
// githubCommitRange names the commits a pull request is to show the diff of: a single
// commit by its hash, and a range of them as the commit the diff starts after, then the
// commit it ends at. A range that starts where the pull request itself does names BASE
// as the commit it starts after, the keyword its pages use for the commit the pull
// request was opened against; naming that commit by its hash gets a page that says it
// can't find those commits.
func githubCommitRange(commits []*models.Commit, baseHash string) string {
newest := commits[0].Hash()
if len(commits) == 1 {
return newest
}
if baseHash == "" {
baseHash = "BASE"
}
return baseHash + ".." + newest
}
// githubDiffLineSuffix names a line within a file's diff: R for the new version of the
// file, L for the old one, which is where a deleted line is found. Some rows are no line
// of the file at all (the header naming it, or a marker like "\ No newline at end of
// file"); those name none, and the anchor points at the file itself.
func githubDiffLineSuffix(info types.DiffLineInfo) string {
switch info.Type {
case types.DiffLineDeleted:
return fmt.Sprintf("L%d", info.OldLine)
case types.DiffLineAdded, types.DiffLineContext, types.DiffLineHunkHeader:
return fmt.Sprintf("R%d", info.NewLine)
default:
return ""
}
}
func (self *MainViewController) openSearch() error {
if manager := self.c.GetViewBufferManagerForView(self.context.GetView()); manager != nil {
manager.ReadToEnd(func() {
@@ -0,0 +1,115 @@
package controllers
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
)
func TestGithubPullRequestLineURL(t *testing.T) {
const prURL = "https://github.com/jesseduffield/lazygit/pull/1234"
const commitHash = "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b"
// The anchor names the file by the SHA-256 of its repo-relative path, taken over
// exactly those bytes: no leading slash, no trailing newline, forward slashes.
const fileHash = "067980d6efc4249367ceb61b0d93a00bca100a0ddb6d4a72b6dbb0eb9d3825cc" // "dir/file1"
scenarios := []struct {
name string
path string
info types.DiffLineInfo
expected string
}{
{
name: "an added line is on the right side of the diff",
path: "dir/file1",
info: types.DiffLineInfo{Type: types.DiffLineAdded, NewLine: 12},
expected: prURL + "/changes/" + commitHash + "#diff-" + fileHash + "R12",
},
{
name: "a deleted line is on the left side, at the line it sat on",
path: "dir/file1",
// A deletion's NewLine is only where it sits in the new version of the
// file; the line it is, is the old one.
info: types.DiffLineInfo{Type: types.DiffLineDeleted, NewLine: 12, OldLine: 34},
expected: prURL + "/changes/" + commitHash + "#diff-" + fileHash + "L34",
},
{
name: "a context line is on the right side too",
path: "dir/file1",
info: types.DiffLineInfo{Type: types.DiffLineContext, NewLine: 7, OldLine: 5},
expected: prURL + "/changes/" + commitHash + "#diff-" + fileHash + "R7",
},
{
name: "a hunk header points at the first line of its hunk",
path: "dir/file1",
info: types.DiffLineInfo{Type: types.DiffLineHunkHeader, NewLine: 20},
expected: prURL + "/changes/" + commitHash + "#diff-" + fileHash + "R20",
},
{
name: "the header naming a file points at the file alone",
path: "dir/file1",
info: types.DiffLineInfo{Type: types.DiffLineFileHeader},
expected: prURL + "/changes/" + commitHash + "#diff-" + fileHash,
},
{
name: "a row that is no line of the file points at the file alone",
path: "dir/file1",
info: types.DiffLineInfo{Type: types.DiffLineOther},
expected: prURL + "/changes/" + commitHash + "#diff-" + fileHash,
},
{
name: "a file at the root of the repo",
path: "file1",
info: types.DiffLineInfo{Type: types.DiffLineAdded, NewLine: 1},
expected: prURL + "/changes/" + commitHash +
"#diff-c147efcfc2d7ea666a9e4f5187b115c90903f0fc896a56df9a6ef5d8f3fc9f31R1",
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
assert.Equal(t, s.expected, githubPullRequestLineURL(prURL, commitHash, s.path, s.info))
})
}
}
func TestGithubCommitRange(t *testing.T) {
hashPool := &utils.StringPool{}
newest := models.NewCommit(hashPool, models.NewCommitOpts{Hash: "newest"})
oldest := models.NewCommit(hashPool, models.NewCommitOpts{Hash: "oldest"})
scenarios := []struct {
name string
commits []*models.Commit
baseHash string
expected string
}{
{
name: "a single commit is named on its own",
commits: []*models.Commit{newest},
baseHash: "parent",
expected: "newest",
},
{
name: "a range is named as the commits it lies between",
commits: []*models.Commit{newest, oldest},
baseHash: "parent",
expected: "parent..newest",
},
{
name: "a range starting where the pull request does lies above BASE",
commits: []*models.Commit{newest, oldest},
expected: "BASE..newest",
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
assert.Equal(t, s.expected, githubCommitRange(s.commits, s.baseHash))
})
}
}
@@ -2,7 +2,6 @@ package controllers
import (
"fmt"
"path/filepath"
"strings"
"github.com/jesseduffield/generics/set"
@@ -262,11 +261,11 @@ func (self *WorkingTreeDiffActions) applyDiffLineSelection(
// fileForDiffLinePath maps the absolute path a diff line carries to the working tree
// file it belongs to, or nil for a path that is no file of this repo's working tree.
func (self *WorkingTreeDiffActions) fileForDiffLinePath(path string) *models.File {
relativePath, err := filepath.Rel(self.c.Git().RepoPaths.WorktreePath(), path)
if err != nil {
relativePath := repoRelativePath(self.c.Git().RepoPaths.WorktreePath(), path)
if relativePath == "" {
return nil
}
return self.context().FileTreeViewModel.GetFile(filepath.ToSlash(relativePath))
return self.context().FileTreeViewModel.GetFile(relativePath)
}
// applyDiffLines applies the given change lines of one file — a line, a hunk, a range —
+24
View File
@@ -1,6 +1,7 @@
package types
import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
@@ -213,6 +214,29 @@ const (
DiffMainViewTypePatchBuilding
)
// PullRequestDiffContext is implemented by the side panel contexts that show, in
// their focused main view, the diff of a commit of a branch: the commits panel and
// the sub-commits panel, and the commit files panel entered from either of them. A
// pull request for that branch has a view of that commit, so a line of the diff can
// be pointed at in it. A panel showing a diff that no pull request has a view of (the
// working tree's, a stash entry's, a reflog entry's) doesn't implement this.
type PullRequestDiffContext interface {
Context
// BranchForPullRequest returns the local branch whose pull request would show
// the diff in the main view, and "" where no branch does: the panel may have
// nothing selected, HEAD may be detached, or what was drilled into may be a tag
// or a remote branch rather than a local one.
BranchForPullRequest() string
// CommitsForPullRequest returns the commits whose combined diff the main view is
// showing, newest first as the panel lists them, together with the hash of the
// commit that diff starts after: the parent of the oldest of them, where the
// pull request has that parent as one of its own commits, and "" where the diff
// starts where the pull request itself does.
CommitsForPullRequest() ([]*models.Commit, string)
}
// DiffPaneContext is one of the two panes the main section can show, as the thing
// that holds a diff with a selection in it. The panels that act on such a selection
// are handed the pane it was made in, and speak to it through this.
+12
View File
@@ -293,6 +293,8 @@ type TranslationSet struct {
UnsupportedGitService string
CopyPullRequestURL string
OpenPullRequestInBrowser string
OpenPullRequestAtSelectedLine string
OpenPullRequestAtSelectedLineTooltip string
NoPullRequestForBranch string
NoBranchOnRemote string
Fetch string
@@ -310,6 +312,10 @@ type TranslationSet struct {
ToggleSelectHunk string
SelectHunk string
NothingToSelectInDiff string
NotAvailableInDiffingMode string
NotAvailableForCustomPatch string
CommitNotInPullRequest string
CommitsNotInPullRequest string
SelectLineByLine string
ToggleSelectHunkTooltip string
ToggleSelectionForPatch string
@@ -1467,6 +1473,8 @@ func EnglishTranslationSet() *TranslationSet {
CreatePullRequest: `Create pull request`,
CopyPullRequestURL: `Copy pull request URL to clipboard`,
OpenPullRequestInBrowser: `Open pull request in browser`,
OpenPullRequestAtSelectedLine: `Open pull request at selected line`,
OpenPullRequestAtSelectedLineTooltip: "Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found.",
NoPullRequestForBranch: `No pull request found for this branch`,
NoBranchOnRemote: `This branch doesn't exist on remote. You need to push it to remote first.`,
Fetch: `Fetch`,
@@ -1485,6 +1493,10 @@ func EnglishTranslationSet() *TranslationSet {
DismissRangeSelect: "Dismiss range select",
ToggleSelectHunk: "Toggle hunk selection",
NothingToSelectInDiff: "There is nothing to select here",
NotAvailableInDiffingMode: "Not available in diffing mode",
NotAvailableForCustomPatch: "Not available for the custom patch",
CommitNotInPullRequest: "This commit is not part of the pull request",
CommitsNotInPullRequest: "Not all of these commits are part of the pull request",
SelectHunk: "Select hunks",
SelectLineByLine: "Select line-by-line",
ToggleSelectHunkTooltip: "Toggle line-by-line vs. hunk selection mode.",
@@ -0,0 +1,105 @@
package main_view
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var OpenPullRequestOnlyOverACommitsDiff = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Opening a diff line in the pull request is offered over a commit's own diff, and refused over the other diffs the main view shows",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
config.GetUserConfig().Gui.UseHunkModeInDiffView = false
},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("file1", "one\ntwo\nthree\n")
shell.Commit("first commit")
shell.UpdateFileAndAdd("file1", "one\nTWO\nthree\n")
shell.Commit("second commit")
shell.UpdateFile("file1", "one\nTWO\nTHREE\n")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
// The working tree's diff is no commit of a branch, so no pull request has a
// view of it and the command isn't offered there at all.
t.Views().Files().
Focus().
SelectedLine(Contains("file1")).
Press(keys.Universal.FocusMainView)
t.Views().Main().
IsFocused().
Press(keys.Universal.OptionMenu)
t.ExpectPopup().Menu().
Title(Equals("Keybindings")).
Tap(func() {
// The command is bound right below the one asserted here, so a menu
// showing that one would be showing this one too if it had it.
t.Views().Menu().
Content(Contains("Go to next file")).
Content(DoesNotContain("Open pull request at selected line"))
}).
Cancel()
// Over a commit's diff it is offered, and says so where the branch has no pull
// request to open.
t.Views().Commits().
Focus().
SelectedLine(Contains("second commit")).
Press(keys.Universal.FocusMainView)
t.Views().Main().
IsFocused().
SelectedLines(
Contains("-two"),
).
Press(keys.Commits.OpenPullRequestInBrowser)
t.ExpectPopup().Alert().
Title(Equals("Error")).
Content(Contains("No pull request found for this branch")).
Confirm()
// The pane previewing the custom patch shows the patch's lines at the numbers
// the patch gives them, which are not the ones the pull request shows.
t.Views().Main().
IsFocused().
PressPrimaryAction().
Press(keys.Universal.TogglePanel)
t.Views().Secondary().
IsFocused().
SelectedLines(
Contains("-two"),
).
Press(keys.Commits.OpenPullRequestInBrowser)
t.ExpectToast(Contains("Not available for the custom patch"))
// In diffing mode the main view shows a diff against another ref rather than
// the commit's own, and the pull request has no view of that either.
t.Views().Commits().
Focus().
Press(keys.Universal.DiffingMenu)
t.ExpectPopup().Menu().
Title(Equals("Diffing")).
Select(MatchesRegexp(`Diff \w+`)).
Confirm()
t.Views().Commits().
SelectNextItem().
SelectedLine(Contains("first commit")).
Press(keys.Universal.FocusMainView)
t.Views().Main().
IsFocused().
SelectionIsActive().
Press(keys.Commits.OpenPullRequestInBrowser)
t.ExpectToast(Contains("Not available in diffing mode"))
},
})
+1
View File
@@ -442,6 +442,7 @@ var tests = []*components.IntegrationTest{
main_view.NoSelectionOverACommitLog,
main_view.NoSelectionOverAConflictHint,
main_view.NoSelectionWhenNoChanges,
main_view.OpenPullRequestOnlyOverACommitsDiff,
main_view.PatchMarksFollowARendererSwitch,
main_view.PatchMarksShowWhileTheDiffIsFocused,
main_view.RangeSelectDiffLines,