Skip to content

Commit e3cab97

Browse files
committed
add restore upstream branch menu item
Restore an upstream branch that was deleted on the remote by pushing the local branch back to its configured upstream, removing the '(upstream gone)' state.
1 parent a133dc0 commit e3cab97

5 files changed

Lines changed: 107 additions & 0 deletions

File tree

pkg/gui/controllers/branches_controller.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,14 @@ func (self *BranchesController) viewUpstreamOptions(selectedBranch *models.Branc
332332
Keys: menuKey('s'),
333333
}
334334

335+
restoreUpstreamItem := &types.MenuItem{
336+
LabelColumns: []string{self.c.Tr.RestoreUpstreamBranch},
337+
OnPress: func() error {
338+
return self.pushBranchToUpstream(selectedBranch)
339+
},
340+
Keys: menuKey('p'),
341+
}
342+
335343
upstreamResetOptions := utils.ResolvePlaceholderString(
336344
self.c.Tr.ViewUpstreamResetOptions,
337345
map[string]string{"upstream": upstream},
@@ -388,11 +396,22 @@ func (self *BranchesController) viewUpstreamOptions(selectedBranch *models.Branc
388396
upstreamRebaseItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.UpstreamNotSetError}
389397
}
390398

399+
// We can only restore an upstream that still has a tracking configuration
400+
// but whose remote branch has been deleted (i.e. it shows "upstream gone").
401+
if !selectedBranch.UpstreamGone {
402+
disabledReason := self.c.Tr.UpstreamNotSetError
403+
if selectedBranch.IsTrackingRemote() {
404+
disabledReason = self.c.Tr.UpstreamNotGoneError
405+
}
406+
restoreUpstreamItem.DisabledReason = &types.DisabledReason{Text: disabledReason}
407+
}
408+
391409
options := []*types.MenuItem{
392410
viewDivergenceItem,
393411
viewDivergenceFromBaseBranchItem,
394412
unsetUpstreamItem,
395413
setUpstreamItem,
414+
restoreUpstreamItem,
396415
upstreamResetItem,
397416
upstreamRebaseItem,
398417
}
@@ -754,6 +773,27 @@ func (self *BranchesController) fastForward(branch *models.Branch) error {
754773
})
755774
}
756775

776+
// pushBranchToUpstream pushes the given branch to its configured upstream,
777+
// recreating a remote branch that was deleted (e.g. on GitHub) so the branch
778+
// no longer shows as "upstream gone".
779+
func (self *BranchesController) pushBranchToUpstream(branch *models.Branch) error {
780+
return self.c.WithInlineStatus(branch, types.ItemOperationPushing, context.LOCAL_BRANCHES_CONTEXT_KEY, func(task gocui.Task) error {
781+
self.c.LogAction(self.c.Tr.Actions.RestoreUpstreamBranch)
782+
err := self.c.Git().Sync.Push(
783+
task,
784+
git_commands.PushOpts{
785+
CurrentBranch: branch.Name,
786+
UpstreamRemote: branch.UpstreamRemote,
787+
UpstreamBranch: branch.UpstreamBranch,
788+
})
789+
if err != nil {
790+
return err
791+
}
792+
self.c.RefreshFromWorker(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS}})
793+
return nil
794+
})
795+
}
796+
757797
func (self *BranchesController) createTag(branch *models.Branch) error {
758798
return self.c.Helpers().Tags.OpenCreateTagPrompt(branch.FullRefName(), func() {})
759799
}

pkg/i18n/english.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,7 @@ type TranslationSet struct {
575575
SetAsUpstreamTooltip string
576576
SetUpstream string
577577
UnsetUpstream string
578+
RestoreUpstreamBranch string
578579
ViewDivergenceFromUpstream string
579580
ViewDivergenceFromBaseBranch string
580581
CouldNotDetermineBaseBranch string
@@ -646,6 +647,7 @@ type TranslationSet struct {
646647
ViewBranchUpstreamOptions string
647648
ViewBranchUpstreamOptionsTooltip string
648649
UpstreamNotSetError string
650+
UpstreamNotGoneError string
649651
UpstreamsNotSetError string
650652
NewGitFlowBranchPrompt string
651653
RenameBranchWarning string
@@ -1024,6 +1026,7 @@ type Actions struct {
10241026
RenameBranch string
10251027
CreateBranch string
10261028
FastForwardBranch string
1029+
RestoreUpstreamBranch string
10271030
AutoForwardBranches string
10281031
CherryPick string
10291032
CheckoutFile string
@@ -1740,6 +1743,7 @@ func EnglishTranslationSet() *TranslationSet {
17401743
SetAsUpstreamTooltip: "Set the selected remote branch as the upstream of the checked-out branch.",
17411744
SetUpstream: "Set upstream of selected branch",
17421745
UnsetUpstream: "Unset upstream of selected branch",
1746+
RestoreUpstreamBranch: "Restore upstream branch",
17431747
ViewDivergenceFromUpstream: "View divergence from upstream",
17441748
ViewDivergenceFromBaseBranch: "View divergence from base branch ({{.baseBranch}})",
17451749
CouldNotDetermineBaseBranch: "Couldn't determine base branch",
@@ -1807,6 +1811,7 @@ func EnglishTranslationSet() *TranslationSet {
18071811
ViewBranchUpstreamOptions: "View upstream options",
18081812
ViewBranchUpstreamOptionsTooltip: "View options relating to the branch's upstream e.g. setting/unsetting the upstream and resetting to the upstream.",
18091813
UpstreamNotSetError: "The selected branch has no upstream (or the upstream is not stored locally)",
1814+
UpstreamNotGoneError: "The selected branch's upstream still exists",
18101815
UpstreamsNotSetError: "Some of the selected branches have no upstream (or the upstream is not stored locally)",
18111816
Upstream: "Upstream",
18121817
NewBranchNamePrompt: "Enter new branch name for branch",
@@ -2241,6 +2246,7 @@ func EnglishTranslationSet() *TranslationSet {
22412246
MixedReset: "Mixed reset",
22422247
HardReset: "Hard reset",
22432248
FastForwardBranch: "Fast forward branch",
2249+
RestoreUpstreamBranch: "Restore upstream branch",
22442250
AutoForwardBranches: "Auto-forward branches",
22452251
Undo: "Undo",
22462252
Redo: "Redo",

pkg/integration/components/git.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ func (self *Git) RemoteTagDeleted(ref string, tagName string) *Git {
2727
})
2828
}
2929

30+
// AssertRemoteBranchExists asserts that the given branch still exists on the
31+
// given remote, i.e. it has been pushed.
32+
func (self *Git) AssertRemoteBranchExists(ref string, branchName string) *Git {
33+
return self.expect([]string{"git", "ls-remote", ref, fmt.Sprintf("refs/heads/%s", branchName)}, func(s string) (bool, string) {
34+
return len(s) > 0, fmt.Sprintf("Expected branch %s to still exist on %s", branchName, ref)
35+
})
36+
}
37+
3038
func (self *Git) assert(cmdArgs []string, expected string) *Git {
3139
self.expect(cmdArgs, func(output string) (bool, string) {
3240
return output == expected, fmt.Sprintf("Expected current branch name to be '%s', but got '%s'", expected, output)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package branch
2+
3+
import (
4+
"github.com/jesseduffield/lazygit/pkg/config"
5+
. "github.com/jesseduffield/lazygit/pkg/integration/components"
6+
)
7+
8+
var RestoreUpstreamBranch = NewIntegrationTest(NewIntegrationTestArgs{
9+
Description: "Restore an upstream branch that was deleted on the remote",
10+
ExtraCmdArgs: []string{},
11+
Skip: false,
12+
SetupConfig: func(config *config.AppConfig) {},
13+
SetupRepo: func(shell *Shell) {
14+
shell.
15+
CloneIntoRemote("origin").
16+
EmptyCommit("base commit").
17+
NewBranch("feature").
18+
EmptyCommit("on feature").
19+
PushBranchAndSetUpstream("origin", "feature").
20+
Checkout("master").
21+
RunCommand([]string{"git", "-C", "../origin", "branch", "-D", "feature"}).
22+
RunCommand([]string{"git", "fetch", "origin", "--prune"})
23+
},
24+
Run: func(t *TestDriver, keys config.KeybindingConfig) {
25+
t.Views().Branches().
26+
Focus().
27+
Lines(
28+
Contains("master").IsSelected(),
29+
Contains("feature").Contains("upstream gone"),
30+
)
31+
32+
t.Views().Branches().
33+
NavigateToLine(Contains("feature")).
34+
Press(keys.Branches.SetUpstream).
35+
Tap(func() {
36+
t.ExpectPopup().
37+
Menu().
38+
Title(Equals("Upstream options")).
39+
Select(Contains("Restore upstream branch")).
40+
Confirm()
41+
})
42+
43+
// the "upstream gone" message is gone and the remote branch is recreated
44+
t.Views().Branches().
45+
Lines(
46+
Contains("master"),
47+
Contains("feature").DoesNotContain("upstream gone"),
48+
)
49+
50+
t.Git().AssertRemoteBranchExists("origin", "feature")
51+
},
52+
})

pkg/integration/tests/test_list.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ var tests = []*components.IntegrationTest{
8181
branch.ResetToDuplicateNamedUpstream,
8282
branch.ResetToUpstream,
8383
branch.RestoreDeletedBranch,
84+
branch.RestoreUpstreamBranch,
8485
branch.SelectCommitsOfCurrentBranch,
8586
branch.SetUpstream,
8687
branch.ShowDivergenceFromBaseBranch,

0 commit comments

Comments
 (0)