mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-10-12 02:16:50 +00:00
Open the selected diff line in the branch's pull request (#6042)
11th PR in the stack of PRs towards staging hunks directly from the main view. This one is stacked on #6041. With a commit's diff focused, open the branch's pull request in your browser at the line you have selected. GitHub only, the anchor format being GitHub's. Which branch's pull request that is depends on where you are. The commits panel answers with the checked-out branch, the sub-commits panel with the ref you entered it from where that is a local branch, and the commit files panel by asking the panel it was entered from. The files panel, the stash and the reflog have no pull request to point at, and the command isn't offered there at all. The anchor format is `<pr url>/changes/<sha>#diff-<sha256 of the path>R<line>`. It is derived by experiment; GitHub doesn't document it.
This commit is contained in:
@@ -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 就提交更改 | |
|
||||
|
||||
@@ -19,16 +19,27 @@ 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
|
||||
}
|
||||
|
||||
// PullRequestDiff asks the panel this one was entered from. The files listed here are
|
||||
// those of the commits selected there, and that panel knows which branch's pull request
|
||||
// those commits are in.
|
||||
func (self *CommitFilesContext) PullRequestDiff() types.PullRequestDiff {
|
||||
if parent, ok := self.GetParentContext().(types.PullRequestDiffContext); ok {
|
||||
return parent.PullRequestDiff()
|
||||
}
|
||||
return types.PullRequestDiff{}
|
||||
}
|
||||
|
||||
func NewCommitFilesContext(c *ContextCommon) *CommitFilesContext {
|
||||
viewModel := filetree.NewCommitFileTreeViewModel(
|
||||
func() []*models.CommitFile { return c.Model().CommitFiles },
|
||||
|
||||
@@ -32,16 +32,129 @@ 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
|
||||
}
|
||||
|
||||
// This panel shows the commits of the checked-out branch, and of the branches below it
|
||||
// in a stack. PullRequestDiff looks for their pull request among those branches.
|
||||
func (self *LocalCommitsContext) PullRequestDiff() types.PullRequestDiff {
|
||||
_, selectionStart, selectionEnd := self.GetSelectedItems()
|
||||
startIdx, endIdx := commitRangeShownInDiff(
|
||||
selectionStart, selectionEnd, self.GetSelectedLineIdx(), self.GetSelectedRefRangeForDiffFiles())
|
||||
model := self.ListContextTrait.c.Model()
|
||||
return pullRequestDiff(
|
||||
self.GetCommits(), startIdx, endIdx, model.CheckedOutBranch, model.Branches, model.PullRequestsMap)
|
||||
}
|
||||
|
||||
// commitRangeShownInDiff returns the indices of the newest and the oldest of 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 selection to DiffHelper.GetUpdateTaskForRenderingCommitsDiff,
|
||||
// so anything acting on the diff on screen acts on the commits that diff is of.
|
||||
func commitRangeShownInDiff(
|
||||
selectionStart int, selectionEnd int, cursor int, refRange *types.RefRange,
|
||||
) (int, int) {
|
||||
if refRange != nil {
|
||||
return selectionStart, selectionEnd
|
||||
}
|
||||
return cursor, cursor
|
||||
}
|
||||
|
||||
// pullRequestDiff works out which branch's pull request would show the diff of the
|
||||
// commits from startIdx to endIdx of a panel listing the commits of listedBranch, and
|
||||
// which commit that diff starts after.
|
||||
//
|
||||
// That commit is the parent of the oldest of the commits. A pull request holds only the
|
||||
// commits of its branch that are pushed, so a parent that isn't pushed is none of its
|
||||
// own. A parent on the branch below in a stack isn't either, because the pull request
|
||||
// was opened against that branch. The diff then starts where the pull request itself
|
||||
// does, and an empty BaseHash says so.
|
||||
func pullRequestDiff(
|
||||
allCommits []*models.Commit,
|
||||
startIdx int,
|
||||
endIdx int,
|
||||
listedBranch string,
|
||||
branches []*models.Branch,
|
||||
pullRequests map[string]*models.GithubPullRequest,
|
||||
) types.PullRequestDiff {
|
||||
if listedBranch == "" || startIdx < 0 || endIdx >= len(allCommits) {
|
||||
return types.PullRequestDiff{}
|
||||
}
|
||||
|
||||
heads := pullRequestBranchHeads(branches, pullRequests)
|
||||
branchAt := func(idx int) string {
|
||||
return pullRequestBranchAt(allCommits, idx, heads, listedBranch)
|
||||
}
|
||||
|
||||
branch := branchAt(startIdx)
|
||||
diff := types.PullRequestDiff{
|
||||
Branch: branch,
|
||||
SpansBranches: branchAt(endIdx) != branch,
|
||||
Commits: allCommits[startIdx : endIdx+1],
|
||||
}
|
||||
|
||||
oldest := allCommits[endIdx]
|
||||
if oldest.IsFirstCommit() {
|
||||
return diff
|
||||
}
|
||||
parentHash := oldest.Parents()[0]
|
||||
_, parentIdx, found := lo.FindIndexOf(allCommits, func(commit *models.Commit) bool {
|
||||
return commit.Hash() == parentHash
|
||||
})
|
||||
if found && allCommits[parentIdx].Status == models.StatusPushed && branchAt(parentIdx) == branch {
|
||||
diff.BaseHash = parentHash
|
||||
}
|
||||
return diff
|
||||
}
|
||||
|
||||
// pullRequestBranchHeads maps the head commit of each branch that has a pull request to
|
||||
// that branch. Where several share a head, the first of them in the list wins. The
|
||||
// checked-out branch comes first in the list, so it wins over the others.
|
||||
func pullRequestBranchHeads(
|
||||
branches []*models.Branch, pullRequests map[string]*models.GithubPullRequest,
|
||||
) map[string]string {
|
||||
heads := map[string]string{}
|
||||
for _, branch := range branches {
|
||||
if _, hasPullRequest := pullRequests[branch.Name]; !hasPullRequest || branch.CommitHash == "" {
|
||||
continue
|
||||
}
|
||||
if _, taken := heads[branch.CommitHash]; !taken {
|
||||
heads[branch.CommitHash] = branch.Name
|
||||
}
|
||||
}
|
||||
return heads
|
||||
}
|
||||
|
||||
// pullRequestBranchAt returns the branch whose pull request holds the commit at the given
|
||||
// index: the nearest branch with a pull request whose head is that commit or one listed
|
||||
// above it. A stack of branches lists the commits of each branch above those of the
|
||||
// branch it is based on, so this is the branch of the stack that the commit is on.
|
||||
// Commits that are in a main branch already are skipped, because a branch whose head is
|
||||
// one of them has been merged and doesn't belong to the stack. If no branch with a pull
|
||||
// request is found, it is the branch the panel lists, whether or not that one has a pull
|
||||
// request.
|
||||
func pullRequestBranchAt(
|
||||
commits []*models.Commit, idx int, pullRequestBranchHeads map[string]string, listedBranch string,
|
||||
) string {
|
||||
for i := idx; i >= 0; i-- {
|
||||
if commits[i].Status == models.StatusMerged {
|
||||
continue
|
||||
}
|
||||
if branch, ok := pullRequestBranchHeads[commits[i].Hash()]; ok {
|
||||
return branch
|
||||
}
|
||||
}
|
||||
return listedBranch
|
||||
}
|
||||
|
||||
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,184 @@ func TestAddMovingCommitsIndicator(t *testing.T) {
|
||||
},
|
||||
}, items)
|
||||
}
|
||||
|
||||
func TestCommitRangeShownInDiff(t *testing.T) {
|
||||
hashPool := &utils.StringPool{}
|
||||
newer := models.NewCommit(hashPool, models.NewCommitOpts{Hash: "newer"})
|
||||
older := models.NewCommit(hashPool, models.NewCommitOpts{Hash: "older"})
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
refRange *types.RefRange
|
||||
expectedStart int
|
||||
expectedEnd int
|
||||
}{
|
||||
{
|
||||
name: "a range is diffed as a whole",
|
||||
refRange: &types.RefRange{From: older, To: newer},
|
||||
expectedStart: 1,
|
||||
expectedEnd: 3,
|
||||
},
|
||||
{
|
||||
name: "without a range to diff, only the commit at the cursor is",
|
||||
expectedStart: 3,
|
||||
expectedEnd: 3,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
startIdx, endIdx := commitRangeShownInDiff(1, 3, 3, s.refRange)
|
||||
assert.Equal(t, s.expectedStart, startIdx)
|
||||
assert.Equal(t, s.expectedEnd, endIdx)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPullRequestDiff(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 stack of two branches, as the panel lists them: newest first. The checked-out
|
||||
// branch "upper" has had its tip amended since it was pushed; it is based on
|
||||
// "lower", which is based on a commit that is in a main branch already.
|
||||
upperAmended := commit("upper-amended", "upper-2", models.StatusUnpushed)
|
||||
upper2 := commit("upper-2", "upper-1", models.StatusPushed)
|
||||
upper1 := commit("upper-1", "lower-2", models.StatusPushed)
|
||||
lower2 := commit("lower-2", "lower-1", models.StatusPushed)
|
||||
lower1 := commit("lower-1", "merged", models.StatusPushed)
|
||||
merged := commit("merged", "ancient", models.StatusMerged)
|
||||
ancient := commit("ancient", "", models.StatusMerged)
|
||||
allCommits := []*models.Commit{upperAmended, upper2, upper1, lower2, lower1, merged, ancient}
|
||||
elsewhere := commit("elsewhere", "unlisted", models.StatusPushed)
|
||||
|
||||
branches := []*models.Branch{
|
||||
{Name: "upper", CommitHash: "upper-amended"},
|
||||
{Name: "lower", CommitHash: "lower-2"},
|
||||
// A branch without a pull request in the middle of "upper", and one whose
|
||||
// pull request was merged.
|
||||
{Name: "no-pull-request", CommitHash: "upper-1"},
|
||||
{Name: "merged-feature", CommitHash: "merged"},
|
||||
}
|
||||
pullRequests := map[string]*models.GithubPullRequest{
|
||||
"upper": {Number: 2},
|
||||
"lower": {Number: 1},
|
||||
"merged-feature": {Number: 0},
|
||||
}
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
commits []*models.Commit
|
||||
startIdx int
|
||||
endIdx int
|
||||
listsNoBranch bool
|
||||
withoutPullRequests bool
|
||||
expected types.PullRequestDiff
|
||||
}{
|
||||
{
|
||||
name: "a commit of the checked-out branch starts after its parent",
|
||||
startIdx: 1,
|
||||
endIdx: 1,
|
||||
expected: types.PullRequestDiff{Branch: "upper", Commits: []*models.Commit{upper2}, BaseHash: "upper-1"},
|
||||
},
|
||||
{
|
||||
name: "a range starts after the parent of its oldest commit",
|
||||
startIdx: 0,
|
||||
endIdx: 1,
|
||||
expected: types.PullRequestDiff{
|
||||
Branch: "upper", Commits: []*models.Commit{upperAmended, upper2}, BaseHash: "upper-1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "a commit of the branch below is in that branch's pull request",
|
||||
startIdx: 3,
|
||||
endIdx: 3,
|
||||
expected: types.PullRequestDiff{Branch: "lower", Commits: []*models.Commit{lower2}, BaseHash: "lower-1"},
|
||||
},
|
||||
{
|
||||
name: "the first commit of a branch starts where its pull request does",
|
||||
startIdx: 4,
|
||||
endIdx: 4,
|
||||
expected: types.PullRequestDiff{Branch: "lower", Commits: []*models.Commit{lower1}},
|
||||
},
|
||||
{
|
||||
name: "and so does the first commit of a branch based on another",
|
||||
startIdx: 2,
|
||||
endIdx: 2,
|
||||
expected: types.PullRequestDiff{Branch: "upper", Commits: []*models.Commit{upper1}},
|
||||
},
|
||||
{
|
||||
name: "so does a range reaching down to it",
|
||||
startIdx: 1,
|
||||
endIdx: 2,
|
||||
expected: types.PullRequestDiff{Branch: "upper", Commits: []*models.Commit{upper2, upper1}},
|
||||
},
|
||||
{
|
||||
name: "a range reaching down into the branch below spans both",
|
||||
startIdx: 2,
|
||||
endIdx: 3,
|
||||
expected: types.PullRequestDiff{
|
||||
Branch: "upper", SpansBranches: true, Commits: []*models.Commit{upper1, lower2},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "a range down from the head of the branch below is that branch's",
|
||||
startIdx: 3,
|
||||
endIdx: 4,
|
||||
expected: types.PullRequestDiff{Branch: "lower", Commits: []*models.Commit{lower2, lower1}},
|
||||
},
|
||||
{
|
||||
name: "the head of a branch that is in a main branch already doesn't count",
|
||||
startIdx: 5,
|
||||
endIdx: 5,
|
||||
expected: types.PullRequestDiff{Branch: "lower", Commits: []*models.Commit{merged}},
|
||||
},
|
||||
{
|
||||
name: "the first commit of the repository starts where the pull request does",
|
||||
startIdx: 6,
|
||||
endIdx: 6,
|
||||
expected: types.PullRequestDiff{Branch: "lower", Commits: []*models.Commit{ancient}},
|
||||
},
|
||||
{
|
||||
name: "a parent the panel doesn't list is none of the pull request's",
|
||||
commits: []*models.Commit{elsewhere},
|
||||
startIdx: 0,
|
||||
endIdx: 0,
|
||||
expected: types.PullRequestDiff{Branch: "upper", Commits: []*models.Commit{elsewhere}},
|
||||
},
|
||||
{
|
||||
name: "without any pull request, a commit is the listed branch's",
|
||||
startIdx: 3,
|
||||
endIdx: 3,
|
||||
withoutPullRequests: true,
|
||||
expected: types.PullRequestDiff{Branch: "upper", Commits: []*models.Commit{lower2}, BaseHash: "lower-1"},
|
||||
},
|
||||
{
|
||||
name: "a panel listing no local branch's commits has no pull request",
|
||||
startIdx: 1,
|
||||
endIdx: 1,
|
||||
listsNoBranch: true,
|
||||
},
|
||||
{
|
||||
name: "nothing is shown",
|
||||
commits: []*models.Commit{},
|
||||
startIdx: -1,
|
||||
endIdx: -1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
commits := lo.Ternary(s.commits == nil, allCommits, s.commits)
|
||||
listedBranch := lo.Ternary(s.listsNoBranch, "", "upper")
|
||||
prs := lo.Ternary(s.withoutPullRequests, nil, pullRequests)
|
||||
assert.Equal(t, s.expected, pullRequestDiff(commits, s.startIdx, s.endIdx, listedBranch, branches, prs))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,16 +21,34 @@ 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
|
||||
}
|
||||
|
||||
// This panel shows the commits of the branch it was entered from, and of the branches
|
||||
// below it in a stack. PullRequestDiff looks for their pull request among those
|
||||
// branches. 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) PullRequestDiff() types.PullRequestDiff {
|
||||
branch, ok := self.GetRef().(*models.Branch)
|
||||
if !ok {
|
||||
return types.PullRequestDiff{}
|
||||
}
|
||||
|
||||
_, selectionStart, selectionEnd := self.GetSelectedItems()
|
||||
startIdx, endIdx := commitRangeShownInDiff(
|
||||
selectionStart, selectionEnd, self.GetSelectedLineIdx(), self.GetSelectedRefRangeForDiffFiles())
|
||||
return pullRequestDiff(
|
||||
self.GetCommits(), startIdx, endIdx, branch.Name, self.c.Model().Branches, self.c.Model().PullRequestsMap)
|
||||
}
|
||||
|
||||
func NewSubCommitsContext(
|
||||
c *ContextCommon,
|
||||
) *SubCommitsContext {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
@@ -319,17 +332,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
|
||||
@@ -339,10 +357,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
|
||||
}
|
||||
@@ -438,6 +453,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.pullRequestDiff().Branch == "" {
|
||||
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
|
||||
@@ -532,6 +560,50 @@ 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,
|
||||
// the commits may be on several branches of a stack and so in several pull requests,
|
||||
// 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}
|
||||
}
|
||||
diff := self.pullRequestDiff()
|
||||
if diff.SpansBranches {
|
||||
return &types.DisabledReason{Text: self.c.Tr.CommitsInSeveralPullRequests}
|
||||
}
|
||||
if reason := self.c.Helpers().Host.NoPullRequestDisabledReason(diff.Branch); reason != nil {
|
||||
return reason
|
||||
}
|
||||
return self.commitsOutsidePullRequestDisabledReason(diff.Commits)
|
||||
}
|
||||
|
||||
// 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(commits []*models.Commit) *types.DisabledReason {
|
||||
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 +1121,95 @@ 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 {
|
||||
diff := self.pullRequestDiff()
|
||||
pr, ok := self.c.Helpers().Host.PullRequestForBranch(diff.Branch)
|
||||
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)
|
||||
}
|
||||
|
||||
if len(diff.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(diff.Commits, diff.BaseHash), relativePath, info)
|
||||
return self.c.OS().OpenLink(url)
|
||||
}
|
||||
|
||||
// pullRequestDiff returns the commits whose diff this pane is showing, and the branch
|
||||
// whose pull request would show that diff, as the panel beneath names them. The diff's
|
||||
// line numbers are the ones the pull request's page for those commits shows. Where no
|
||||
// pull request shows the diff, the branch is "".
|
||||
func (self *MainViewController) pullRequestDiff() types.PullRequestDiff {
|
||||
prContext, ok := self.sidePanelBeneath().(types.PullRequestDiffContext)
|
||||
if !ok {
|
||||
return types.PullRequestDiff{}
|
||||
}
|
||||
return prContext.PullRequestDiff()
|
||||
}
|
||||
|
||||
// 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 —
|
||||
|
||||
@@ -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,45 @@ 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
|
||||
|
||||
// PullRequestDiff returns the commits whose combined diff the main view is
|
||||
// showing, and the branch whose pull request would show that diff.
|
||||
PullRequestDiff() PullRequestDiff
|
||||
}
|
||||
|
||||
// PullRequestDiff is the diff of one or more commits of a branch, as the pull request
|
||||
// for that branch shows it.
|
||||
type PullRequestDiff struct {
|
||||
// Branch is the local branch whose pull request would show the diff, and "" where
|
||||
// no branch's would: 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.
|
||||
// In a stack of branches, each with a pull request of its own, it is the branch
|
||||
// of the stack that the newest of the commits is on.
|
||||
Branch string
|
||||
|
||||
// SpansBranches is true where the commits are on more than one branch of such a
|
||||
// stack. Branch is then the branch of the newest of them, and its pull request
|
||||
// doesn't hold all of them.
|
||||
SpansBranches bool
|
||||
|
||||
// Commits are the commits whose combined diff the main view is showing, newest
|
||||
// first as the panel lists them.
|
||||
Commits []*models.Commit
|
||||
|
||||
// BaseHash is the hash of the commit the diff starts after: the parent of the
|
||||
// oldest of the commits, where the pull request has that parent as one of its own
|
||||
// commits, and "" where the diff starts where the pull request itself does.
|
||||
BaseHash 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.
|
||||
|
||||
@@ -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,11 @@ type TranslationSet struct {
|
||||
ToggleSelectHunk string
|
||||
SelectHunk string
|
||||
NothingToSelectInDiff string
|
||||
NotAvailableInDiffingMode string
|
||||
NotAvailableForCustomPatch string
|
||||
CommitNotInPullRequest string
|
||||
CommitsNotInPullRequest string
|
||||
CommitsInSeveralPullRequests string
|
||||
SelectLineByLine string
|
||||
ToggleSelectHunkTooltip string
|
||||
ToggleSelectionForPatch string
|
||||
@@ -1467,6 +1474,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 +1494,11 @@ 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",
|
||||
CommitsInSeveralPullRequests: "These commits are not all in the same 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"))
|
||||
},
|
||||
})
|
||||
@@ -444,6 +444,7 @@ var tests = []*components.IntegrationTest{
|
||||
main_view.NoSelectionOverACommitLog,
|
||||
main_view.NoSelectionOverAConflictHint,
|
||||
main_view.NoSelectionWhenNoChanges,
|
||||
main_view.OpenPullRequestOnlyOverACommitsDiff,
|
||||
main_view.PatchMarksFollowARendererSwitch,
|
||||
main_view.PatchMarksShowWheneverTheirDiffIsOnScreen,
|
||||
main_view.RangeSelectDiffLines,
|
||||
|
||||
Reference in New Issue
Block a user