Repository: jesseduffield/lazygit (language: go)

Issue:
Support resetting to branch upstream via 'u' menu in branches view
**Is your feature request related to a problem? Please describe.**
Currently, in order to reset (hard/mixed/soft) into the upstream branch you need to focus the files view and hit 'g'. This doesn't really make sense, and it means you can't reset onto branches other than the checked out branch.

**Describe the solution you'd like**
We already open up an upstream-related menu when you press 'u' on a branch. I want an option there to reset onto the upstream (keybinding: 'g' for consistency), which itself takes you to a menu showing you the hard/mixed/soft options (similar to what we already have in the files view when you press 'g').

**Describe alternatives you've considered**
None

**Additional context**
None



Discussion / hints:
@AzraelSec here's a good one if you want it :)
@jesseduffield, it works for me! Feel free to assign it to me! :) 
Hey @jesseduffield - I looked into this issue, and I'd like to discuss it with you. The way it's currently implemented matches how git handles the reset command: you can only operate on the branch that is currently checked out (as far as I know). There is no syntax to say `git reset <branch> @{u}`. The only way to do that would be to implement a shortcut that:
1. checks out the target branch
2. resets it
3. checks out the previous branch

I fear that this would result in a very weak approach. Do you have any suggestions? Do you have in mind a more solid implementation for this feature? Thanks!
Ah sorry, what I meant with this issue was much simpler: we just want to reset the _checked out_ branch onto the selected branch's upstream. For resetting the selected branch onto its own upstream, that would require the user manually checking it out first.
I'm having a bit of trouble understanding what the use case for this is (we already have a lot of reset options, so I'd like to understand the reason for adding another one).

As far as I can tell, I can already reset the current branch to a selected upstream branch from the branches window:

https://github.com/jesseduffield/lazygit/assets/191335/cdd9e4f9-401b-4b7b-b38c-7360e8312a1b



I had the same question but didn't dare to ask. 😅 

My take is that this command is really only useful for resetting the checked-out branch to _its own_ upstream. (We already have a way to do that, by hitting `g` in the files panel; the problem is that this might not the most intuitive place to look for it, depending on why you want to reset: if you want to do a hard reset to get rid of your local modifications, then it is, but if you want to reset because your local branch diverged from upstream and you want to put it back, it's not.)

Personally I have never had the need to reset some branch onto _another_ branch's upstream; I'd say in the rare case where you need that, it's ok to require the user to navigate to the remote branches tab and go find the branch there.
> My take is that this command is really only useful for resetting the checked-out branch to its own upstream. (We already have a way to do that, by hitting g in the files panel; the problem is that this might not the most intuitive place to look for it, depending on why you want to reset: if you want to do a hard reset to get rid of your local modifications, then it is, but if you want to reset because your local branch diverged from upstream and you want to put it back, it's not.)

I was also thinking this may be the case. I'm pretty used to this workflow now, but having it visible elsewhere does seem logical, ie. in this reset popup:

![iTerm2 2023-08-17 at 09 31 48](https://github.com/jesseduffield/lazygit/assets/191335/058eb572-2f95-41d5-84e4-1711a94050f7)

I came from the PR which seems to be implementing as a new process, which has me a bit confused and wanting more information 😅.
I'm open to a better UX than the one I proposed. I think we can all agree that the existing two approaches for resetting to a branch's upstream are not ideal:
* Resetting to upstream via files view is unintuitive
* Going through the remote branches view is imo too much effort

I want a way to easily reset the current branch to it's upstream via the branches view. My proposal is that you press 'u' to view options related to the selected branch's upstream, and one of those options is to reset the checked out branch into that upstream.

Given that:
* It's rare that you would want to reset onto a random branch's upstream
* If you want to do that, you can go through the remote branches view

, I'm open to a UX that optimises for just the use case of resetting onto the checked out branch's upstream. But in my mind we lose nothing by supporting that flow for any selected branch's upstream (whether it's 'r' 'u' or 'u' 'r' I'm not strongly opinionated)
I do think that it could be confusing to offer the command for any (non-checkout-out) branch; people might think that this will reset the _selected_ branch onto its upstream, not the current one. We had this confusion in this very PR ([see above](https://github.com/jesseduffield/lazygit/issues/2920#issuecomment-1676771706)).

One option that we could consider is to only show the command if the selected branch is the checked-out one.
I get @jesseduffield's point (as we are implementing a feature for a specific branch, why don't we implement it generically for all of them?), but I also agree with the others about the risk of confusing the users. I tried to make the operation result as explicit as possible on the menu item, but it could still not be enough.

> One option that we could consider is to only show the command if the selected branch is the checked-out one.

This suggestion would provide a good and more understandable UX, IMHO. We could begin with the suggested implementation and change it in the future based on the users' feedback.

Fix patch:
diff --git a/docs/keybindings/Keybindings_en.md b/docs/keybindings/Keybindings_en.md
index 718c2d2be94..976ea81936c 100644
--- a/docs/keybindings/Keybindings_en.md
+++ b/docs/keybindings/Keybindings_en.md
@@ -155,7 +155,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>T</kbd>: Create tag
   <kbd>g</kbd>: View reset options
   <kbd>R</kbd>: Rename branch
-  <kbd>u</kbd>: Set/Unset upstream
+  <kbd>u</kbd>: View upstream options
   <kbd>w</kbd>: View worktree options
   <kbd>&lt;enter&gt;</kbd>: View commits
   <kbd>/</kbd>: Filter the current view by text
diff --git a/docs/keybindings/Keybindings_ja.md b/docs/keybindings/Keybindings_ja.md
index 02e6af23fcf..419beb5dd17 100644
--- a/docs/keybindings/Keybindings_ja.md
+++ b/docs/keybindings/Keybindings_ja.md
@@ -227,7 +227,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>T</kbd>: タグを作成
   <kbd>g</kbd>: View reset options
   <kbd>R</kbd>: ブランチ名を変更
-  <kbd>u</kbd>: Set/Unset upstream
+  <kbd>u</kbd>: View upstream options
   <kbd>w</kbd>: View worktree options
   <kbd>&lt;enter&gt;</kbd>: コミットを閲覧
   <kbd>/</kbd>: Filter the current view by text
diff --git a/docs/keybindings/Keybindings_ko.md b/docs/keybindings/Keybindings_ko.md
index 4af5fb1a3b6..71c57714e81 100644
--- a/docs/keybindings/Keybindings_ko.md
+++ b/docs/keybindings/Keybindings_ko.md
@@ -190,7 +190,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>T</kbd>: 태그를 생성
   <kbd>g</kbd>: View reset options
   <kbd>R</kbd>: 브랜치 이름 변경
-  <kbd>u</kbd>: Set/Unset upstream
+  <kbd>u</kbd>: View upstream options
   <kbd>w</kbd>: View worktree options
   <kbd>&lt;enter&gt;</kbd>: 커밋 보기
   <kbd>/</kbd>: Filter the current view by text
diff --git a/docs/keybindings/Keybindings_nl.md b/docs/keybindings/Keybindings_nl.md
index 7d366fcdabd..13a868817ea 100644
--- a/docs/keybindings/Keybindings_nl.md
+++ b/docs/keybindings/Keybindings_nl.md
@@ -97,7 +97,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>T</kbd>: Creëer tag
   <kbd>g</kbd>: Bekijk reset opties
   <kbd>R</kbd>: Hernoem branch
-  <kbd>u</kbd>: Set/Unset upstream
+  <kbd>u</kbd>: View upstream options
   <kbd>w</kbd>: View worktree options
   <kbd>&lt;enter&gt;</kbd>: Bekijk commits
   <kbd>/</kbd>: Filter the current view by text
diff --git a/docs/keybindings/Keybindings_pl.md b/docs/keybindings/Keybindings_pl.md
index f4f59228efb..eae0951158f 100644
--- a/docs/keybindings/Keybindings_pl.md
+++ b/docs/keybindings/Keybindings_pl.md
@@ -113,7 +113,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>T</kbd>: Create tag
   <kbd>g</kbd>: Wyświetl opcje resetu
   <kbd>R</kbd>: Rename branch
-  <kbd>u</kbd>: Set/Unset upstream
+  <kbd>u</kbd>: View upstream options
   <kbd>w</kbd>: View worktree options
   <kbd>&lt;enter&gt;</kbd>: View commits
   <kbd>/</kbd>: Filter the current view by text
diff --git a/docs/keybindings/Keybindings_ru.md b/docs/keybindings/Keybindings_ru.md
index 9a03d2935a6..ff533b60545 100644
--- a/docs/keybindings/Keybindings_ru.md
+++ b/docs/keybindings/Keybindings_ru.md
@@ -188,7 +188,7 @@ _Связки клавиш_
   <kbd>T</kbd>: Создать тег
   <kbd>g</kbd>: Просмотреть параметры сброса
   <kbd>R</kbd>: Переименовать ветку
-  <kbd>u</kbd>: Установить/убрать upstream-ветку
+  <kbd>u</kbd>: View upstream options
   <kbd>w</kbd>: View worktree options
   <kbd>&lt;enter&gt;</kbd>: Просмотреть коммиты
   <kbd>/</kbd>: Filter the current view by text
diff --git a/docs/keybindings/Keybindings_zh-CN.md b/docs/keybindings/Keybindings_zh-CN.md
index 6f60cb644c1..620c77495b2 100644
--- a/docs/keybindings/Keybindings_zh-CN.md
+++ b/docs/keybindings/Keybindings_zh-CN.md
@@ -91,7 +91,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>T</kbd>: 创建标签
   <kbd>g</kbd>: 查看重置选项
   <kbd>R</kbd>: 重命名分支
-  <kbd>u</kbd>: Set/Unset upstream
+  <kbd>u</kbd>: View upstream options
   <kbd>w</kbd>: View worktree options
   <kbd>&lt;enter&gt;</kbd>: 查看提交
   <kbd>/</kbd>: Filter the current view by text
diff --git a/docs/keybindings/Keybindings_zh-TW.md b/docs/keybindings/Keybindings_zh-TW.md
index eb82367b539..186d02864ce 100644
--- a/docs/keybindings/Keybindings_zh-TW.md
+++ b/docs/keybindings/Keybindings_zh-TW.md
@@ -263,7 +263,7 @@ _說明：`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B，`B`表示 Shift+B_
   <kbd>T</kbd>: 建立標籤
   <kbd>g</kbd>: 檢視重設選項
   <kbd>R</kbd>: 重新命名分支
-  <kbd>u</kbd>: 設定/取消設定上游
+  <kbd>u</kbd>: View upstream options
   <kbd>w</kbd>: View worktree options
   <kbd>&lt;enter&gt;</kbd>: 檢視提交
   <kbd>/</kbd>: Filter the current view by text
diff --git a/pkg/gui/controllers/branches_controller.go b/pkg/gui/controllers/branches_controller.go
index 67174439c99..99cb3d63f65 100644
--- a/pkg/gui/controllers/branches_controller.go
+++ b/pkg/gui/controllers/branches_controller.go
@@ -108,7 +108,8 @@ func (self *BranchesController) GetKeybindings(opts types.KeybindingsOpts) []*ty
 		{
 			Key:         opts.GetKey(opts.Config.Branches.SetUpstream),
 			Handler:     self.checkSelected(self.setUpstream),
-			Description: self.c.Tr.SetUnsetUpstream,
+			Description: self.c.Tr.ViewBranchUpstreamOptions,
+			Tooltip:     self.c.Tr.ViewBranchUpstreamOptionsTooltip,
 			OpensMenu:   true,
 		},
 	}
@@ -139,34 +140,57 @@ func (self *BranchesController) GetOnRenderToMain() func() error {
 }
 
 func (self *BranchesController) setUpstream(selectedBranch *models.Branch) error {
-	return self.c.Menu(types.CreateMenuOptions{
-		Title: self.c.Tr.Actions.SetUnsetUpstream,
-		Items: []*types.MenuItem{
-			{
-				LabelColumns: []string{self.c.Tr.ViewDivergenceFromUpstream},
-				OnPress: func() error {
-					branch := self.context().GetSelected()
-					if branch == nil {
-						return nil
-					}
+	options := []*types.MenuItem{
+		{
+			LabelColumns: []string{self.c.Tr.ViewDivergenceFromUpstream},
+			OnPress: func() error {
+				branch := self.context().GetSelected()
+				if branch == nil {
+					return nil
+				}
 
-					if !branch.RemoteBranchStoredLocally() {
-						return self.c.ErrorMsg(self.c.Tr.DivergenceNoUpstream)
-					}
-					return self.c.Helpers().SubCommits.ViewSubCommits(helpers.ViewSubCommitsOpts{
-						Ref:                     branch,
-						TitleRef:                fmt.Sprintf("%s <-> %s", branch.RefName(), branch.ShortUpstreamRefName()),
-						RefToShowDivergenceFrom: branch.FullUpstreamRefName(),
-						Context:                 self.context(),
-						ShowBranchHeads:         false,
-					})
-				},
-				Key: 'v',
+				if !branch.RemoteBranchStoredLocally() {
+					return self.c.ErrorMsg(self.c.Tr.DivergenceNoUpstream)
+				}
+				return self.c.Helpers().SubCommits.ViewSubCommits(helpers.ViewSubCommitsOpts{
+					Ref:                     branch,
+					TitleRef:                fmt.Sprintf("%s <-> %s", branch.RefName(), branch.ShortUpstreamRefName()),
+					RefToShowDivergenceFrom: branch.FullUpstreamRefName(),
+					Context:                 self.context(),
+					ShowBranchHeads:         false,
+				})
 			},
-			{
-				LabelColumns: []string{self.c.Tr.UnsetUpstream},
-				OnPress: func() error {
-					if err := self.c.Git().Branch.UnsetUpstream(selectedBranch.Name); err != nil {
+			Key: 'v',
+		},
+		{
+			LabelColumns: []string{self.c.Tr.UnsetUpstream},
+			OnPress: func() error {
+				if err := self.c.Git().Branch.UnsetUpstream(selectedBranch.Name); err != nil {
+					return self.c.Error(err)
+				}
+				if err := self.c.Refresh(types.RefreshOptions{
+					Mode: types.SYNC,
+					Scope: []types.RefreshableView{
+						types.BRANCHES,
+						types.COMMITS,
+					},
+				}); err != nil {
+					return self.c.Error(err)
+				}
+				return nil
+			},
+			Key: 'u',
+		},
+		{
+			LabelColumns: []string{self.c.Tr.SetUpstream},
+			OnPress: func() error {
+				return self.c.Helpers().Upstream.PromptForUpstreamWithoutInitialContent(selectedBranch, func(upstream string) error {
+					upstreamRemote, upstreamBranch, err := self.c.Helpers().Upstream.ParseUpstream(upstream)
+					if err != nil {
+						return self.c.Error(err)
+					}
+
+					if err := self.c.Git().Branch.SetUpstream(upstreamRemote, upstreamBranch, selectedBranch.Name); err != nil {
 						return self.c.Error(err)
 					}
 					if err := self.c.Refresh(types.RefreshOptions{
@@ -179,36 +203,55 @@ func (self *BranchesController) setUpstream(selectedBranch *models.Branch) error
 						return self.c.Error(err)
 					}
 					return nil
-				},
-				Key: 'u',
+				})
 			},
-			{
-				LabelColumns: []string{self.c.Tr.SetUpstream},
-				OnPress: func() error {
-					return self.c.Helpers().Upstream.PromptForUpstreamWithoutInitialContent(selectedBranch, func(upstream string) error {
-						upstreamRemote, upstreamBranch, err := self.c.Helpers().Upstream.ParseUpstream(upstream)
-						if err != nil {
-							return self.c.Error(err)
-						}
+			Key: 's',
+		},
+	}
 
-						if err := self.c.Git().Branch.SetUpstream(upstreamRemote, upstreamBranch, selectedBranch.Name); err != nil {
-							return self.c.Error(err)
-						}
-						if err := self.c.Refresh(types.RefreshOptions{
-							Mode: types.SYNC,
-							Scope: []types.RefreshableView{
-								types.BRANCHES,
-								types.COMMITS,
-							},
-						}); err != nil {
-							return self.c.Error(err)
-						}
-						return nil
-					})
-				},
-				Key: 's',
+	if selectedBranch.IsTrackingRemote() {
+		upstream := fmt.Sprintf("%s/%s", selectedBranch.UpstreamRemote, selectedBranch.Name)
+		upstreamResetOptions := utils.ResolvePlaceholderString(
+			self.c.Tr.ViewUpstreamResetOptions,
+			map[string]string{"upstream": upstream},
+		)
+		upstreamResetTooltip := utils.ResolvePlaceholderString(
+			self.c.Tr.ViewUpstreamResetOptionsTooltip,
+			map[string]string{"upstream": upstream},
+		)
+
+		options = append(options, &types.MenuItem{
+			LabelColumns: []string{upstreamResetOptions},
+			OpensMenu:    true,
+			OnPress: func() error {
+				if selectedBranch.RemoteBranchNotStoredLocally() {
+					return self.c.ErrorMsg(self.c.Tr.UpstreamNotStoredLocallyError)
+				}
+
+				err := self.c.Helpers().Refs.CreateGitResetMenu(upstream)
+				if err != nil {
+					return self.c.Error(err)
+				}
+				return nil
 			},
-		},
+			Tooltip: upstreamResetTooltip,
+			Key:     'g',
+		})
+	} else {
+		options = append(options, &types.MenuItem{
+			LabelColumns: []string{self.c.Tr.ViewUpstreamDisabledResetOptions},
+			OpensMenu:    true,
+			OnPress: func() error {
+				return self.c.ErrorMsg(self.c.Tr.UpstreamNotSetError)
+			},
+			Tooltip: self.c.Tr.UpstreamNotSetError,
+			Key:     'g',
+		})
+	}
+
+	return self.c.Menu(types.CreateMenuOptions{
+		Title: self.c.Tr.BranchUpstreamOptionsTitle,
+		Items: options,
 	})
 }
 
diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go
index 5c7f99b55f6..87ca8c8ca5a 100644
--- a/pkg/i18n/english.go
+++ b/pkg/i18n/english.go
@@ -352,6 +352,9 @@ type TranslationSet struct {
 	DivergenceNoUpstream                string
 	DivergenceSectionHeaderLocal        string
 	DivergenceSectionHeaderRemote       string
+	ViewUpstreamResetOptions            string
+	ViewUpstreamResetOptionsTooltip     string
+	ViewUpstreamDisabledResetOptions    string
 	SetUpstreamTitle                    string
 	SetUpstreamMessage                  string
 	EditRemote                          string
@@ -396,7 +399,11 @@ type TranslationSet struct {
 	KeybindingsMenuSectionGlobal        string
 	KeybindingsMenuSectionNavigation    string
 	RenameBranch                        string
-	SetUnsetUpstream                    string
+	ViewBranchUpstreamOptions           string
+	BranchUpstreamOptionsTitle          string
+	ViewBranchUpstreamOptionsTooltip    string
+	UpstreamNotStoredLocallyError       string
+	UpstreamNotSetError                 string
 	NewGitFlowBranchPrompt              string
 	RenameBranchWarning                 string
 	OpenMenu                            string
@@ -654,7 +661,6 @@ type Actions struct {
 	Merge                             string
 	RebaseBranch                      string
 	RenameBranch                      string
-	SetUnsetUpstream                  string
 	CreateBranch                      string
 	FastForwardBranch                 string
 	CherryPick                        string
@@ -1137,6 +1143,9 @@ func EnglishTranslationSet() TranslationSet {
 		DivergenceNoUpstream:                "Cannot show divergence of a branch that has no (locally tracked) upstream",
 		DivergenceSectionHeaderLocal:        "Local",
 		DivergenceSectionHeaderRemote:       "Remote",
+		ViewUpstreamResetOptions:            "Reset checked-out branch onto {{.upstream}}",
+		ViewUpstreamResetOptionsTooltip:     "View options for resetting the checked-out branch onto {{upstream}}. Note: this will not reset the selected branch onto the upstream, it will reset the checked-out branch onto the upstream",
+		ViewUpstreamDisabledResetOptions:    "Reset checked-out branch onto upstream of selected branch",
 		SetUpstreamTitle:                    "Set upstream branch",
 		SetUpstreamMessage:                  "Are you sure you want to set the upstream branch of '{{.checkedOut}}' to '{{.selected}}'",
 		EditRemote:                          "Edit remote",
@@ -1166,51 +1175,55 @@ func EnglishTranslationSet() TranslationSet {
 		NotAGitFlowBranch:                   "This does not seem to be a git flow branch",
 		NewGitFlowBranchPrompt:              "New {{.branchType}} name:",
 
-		IgnoreTracked:              "Ignore tracked file",
-		IgnoreTrackedPrompt:        "Are you sure you want to ignore a tracked file?",
-		ExcludeTracked:             "Exclude tracked file",
-		ExcludeTrackedPrompt:       "Are you sure you want to exclude a tracked file?",
-		ViewResetToUpstreamOptions: "View upstream reset options",
-		NextScreenMode:             "Next screen mode (normal/half/fullscreen)",
-		PrevScreenMode:             "Prev screen mode",
-		StartSearch:                "Search the current view by text",
-		StartFilter:                "Filter the current view by text",
-		Panel:                      "Panel",
-		KeybindingsLegend:          "Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b",
-		RenameBranch:               "Rename branch",
-		SetUnsetUpstream:           "Set/Unset upstream",
-		NewBranchNamePrompt:        "Enter new branch name for branch",
-		RenameBranchWarning:        "This branch is tracking a remote. This action will only rename the local branch name, not the name of the remote branch. Continue?",
-		OpenMenu:                   "Open menu",
-		ResetCherryPick:            "Reset cherry-picked (copied) commits selection",
-		NextTab:                    "Next tab",
-		PrevTab:                    "Previous tab",
-		CantUndoWhileRebasing:      "Can't undo while rebasing",
-		CantRedoWhileRebasing:      "Can't redo while rebasing",
-		MustStashWarning:           "Pulling a patch out into the index requires stashing and unstashing your changes. If something goes wrong, you'll be able to access your files from the stash. Continue?",
-		MustStashTitle:             "Must stash",
-		ConfirmationTitle:          "Confirmation panel",
-		PrevPage:                   "Previous page",
-		NextPage:                   "Next page",
-		GotoTop:                    "Scroll to top",
-		GotoBottom:                 "Scroll to bottom",
-		FilteringBy:                "Filtering by",
-		ResetInParentheses:         "(Reset)",
-		OpenFilteringMenu:          "View filter-by-path options",
-		FilterBy:                   "Filter by",
-		ExitFilterMode:             "Stop filtering by path",
-		FilterPathOption:           "Enter path to filter by",
-		EnterFileName:              "Enter path:",
-		FilteringMenuTitle:         "Filtering",
-		MustExitFilterModeTitle:    "Command not available",
-		MustExitFilterModePrompt:   "Command not available in filter-by-path mode. Exit filter-by-path mode?",
-		Diff:                       "Diff",
-		EnterRefToDiff:             "Enter ref to diff",
-		EnterRefName:               "Enter ref:",
-		ExitDiffMode:               "Exit diff mode",
-		DiffingMenuTitle:           "Diffing",
-		SwapDiff:                   "Reverse diff direction",
-		OpenDiffingMenu:            "Open diff menu",
+		IgnoreTracked:                    "Ignore tracked file",
+		IgnoreTrackedPrompt:              "Are you sure you want to ignore a tracked file?",
+		ExcludeTracked:                   "Exclude tracked file",
+		ExcludeTrackedPrompt:             "Are you sure you want to exclude a tracked file?",
+		ViewResetToUpstreamOptions:       "View upstream reset options",
+		NextScreenMode:                   "Next screen mode (normal/half/fullscreen)",
+		PrevScreenMode:                   "Prev screen mode",
+		StartSearch:                      "Search the current view by text",
+		StartFilter:                      "Filter the current view by text",
+		Panel:                            "Panel",
+		KeybindingsLegend:                "Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b",
+		RenameBranch:                     "Rename branch",
+		BranchUpstreamOptionsTitle:       "Upstream options",
+		ViewBranchUpstreamOptionsTooltip: "View options relating to the branch's upstream e.g. setting/unsetting the upstream and resetting to the upstream",
+		UpstreamNotStoredLocallyError:    "Cannot reset to upstream branch because it is not stored locally",
+		UpstreamNotSetError:              "The selected branch has no upstream",
+		ViewBranchUpstreamOptions:        "View upstream options",
+		NewBranchNamePrompt:              "Enter new branch name for branch",
+		RenameBranchWarning:              "This branch is tracking a remote. This action will only rename the local branch name, not the name of the remote branch. Continue?",
+		OpenMenu:                         "Open menu",
+		ResetCherryPick:                  "Reset cherry-picked (copied) commits selection",
+		NextTab:                          "Next tab",
+		PrevTab:                          "Previous tab",
+		CantUndoWhileRebasing:            "Can't undo while rebasing",
+		CantRedoWhileRebasing:            "Can't redo while rebasing",
+		MustStashWarning:                 "Pulling a patch out into the index requires stashing and unstashing your changes. If something goes wrong, you'll be able to access your files from the stash. Continue?",
+		MustStashTitle:                   "Must stash",
+		ConfirmationTitle:                "Confirmation panel",
+		PrevPage:                         "Previous page",
+		NextPage:                         "Next page",
+		GotoTop:                          "Scroll to top",
+		GotoBottom:                       "Scroll to bottom",
+		FilteringBy:                      "Filtering by",
+		ResetInParentheses:               "(Reset)",
+		OpenFilteringMenu:                "View filter-by-path options",
+		FilterBy:                         "Filter by",
+		ExitFilterMode:                   "Stop filtering by path",
+		FilterPathOption:                 "Enter path to filter by",
+		EnterFileName:                    "Enter path:",
+		FilteringMenuTitle:               "Filtering",
+		MustExitFilterModeTitle:          "Command not available",
+		MustExitFilterModePrompt:         "Command not available in filter-by-path mode. Exit filter-by-path mode?",
+		Diff:                             "Diff",
+		EnterRefToDiff:                   "Enter ref to diff",
+		EnterRefName:                     "Enter ref:",
+		ExitDiffMode:                     "Exit diff mode",
+		DiffingMenuTitle:                 "Diffing",
+		SwapDiff:                         "Reverse diff direction",
+		OpenDiffingMenu:                  "Open diff menu",
 		// the actual view is the extras view which I intend to give more tabs in future but for now we'll only mention the command log part
 		OpenExtrasMenu:                      "Open command log menu",
 		ShowingGitDiff:                      "Showing output for:",
@@ -1394,7 +1407,6 @@ func EnglishTranslationSet() TranslationSet {
 			Merge:                             "Merge",
 			RebaseBranch:                      "Rebase branch",
 			RenameBranch:                      "Rename branch",
-			SetUnsetUpstream:                  "Set/Unset upstream",
 			CreateBranch:                      "Create branch",
 			CherryPick:                        "(Cherry-pick) paste commits",
 			CheckoutFile:                      "Checkout file",
diff --git a/pkg/i18n/korean.go b/pkg/i18n/korean.go
index 32c29db14e2..9c859ec6043 100644
--- a/pkg/i18n/korean.go
+++ b/pkg/i18n/korean.go
@@ -476,7 +476,6 @@ func koreanTranslationSet() TranslationSet {
 			Merge:                             "병합",
 			RebaseBranch:                      "브랜치 리베이스",
 			RenameBranch:                      "브랜치 이름 변경",
-			SetUnsetUpstream:                  "Set/Unset upstream",
 			CreateBranch:                      "브랜치 생성",
 			CherryPick:                        "(Cherry-pick) 커밋 붙여넣기",
 			CheckoutFile:                      "체크아웃 파일",
diff --git a/pkg/i18n/russian.go b/pkg/i18n/russian.go
index fc8806e7386..967e91c121f 100644
--- a/pkg/i18n/russian.go
+++ b/pkg/i18n/russian.go
@@ -395,7 +395,6 @@ func RussianTranslationSet() TranslationSet {
 		Panel:                               "Панель",
 		KeybindingsLegend:                   "Связки клавиш",
 		RenameBranch:                        "Переименовать ветку",
-		SetUnsetUpstream:                    "Установить/убрать upstream-ветку",
 		NewBranchNamePrompt:                 "Введите новое название ветки",
 		RenameBranchWarning:                 "Эта ветвь отслеживает удалённый репозитории. Это действие переименует только имя локальной ветки, а не имя удалённой ветки. Продолжать?",
 		OpenMenu:                            "Открыть меню",
@@ -566,7 +565,6 @@ func RussianTranslationSet() TranslationSet {
 			Merge:                             "Слить",
 			RebaseBranch:                      "Перебазировать ветку",
 			RenameBranch:                      "Переименовать ветку",
-			SetUnsetUpstream:                  "Установить/убрать upstream-ветку",
 			CreateBranch:                      "Создать ветку",
 			CherryPick:                        "(Cherry-pick) Вставить коммиты",
 			CheckoutFile:                      "Переключить файл",
diff --git a/pkg/i18n/traditional_chinese.go b/pkg/i18n/traditional_chinese.go
index 8c5dc959958..896441784ce 100644
--- a/pkg/i18n/traditional_chinese.go
+++ b/pkg/i18n/traditional_chinese.go
@@ -421,7 +421,6 @@ func traditionalChineseTranslationSet() TranslationSet {
 		Panel:                               "面板",
 		KeybindingsLegend:                   "說明：`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B，`B`表示 Shift+B",
 		RenameBranch:                        "重新命名分支",
-		SetUnsetUpstream:                    "設定/取消設定上游",
 		NewBranchNamePrompt:                 "為分支輸入新名稱",
 		RenameBranchWarning:                 "此分支正在追蹤遠端分支。此操作僅會重新命名本地分支名稱，而不是遠端分支的名稱。是否繼續？",
 		OpenMenu:                            "開啟選單",
@@ -592,7 +591,6 @@ func traditionalChineseTranslationSet() TranslationSet {
 			Merge:                             "合併",
 			RebaseBranch:                      "變基分支",
 			RenameBranch:                      "重新命名分支",
-			SetUnsetUpstream:                  "設置/取消上游",
 			CreateBranch:                      "建立分支",
 			CherryPick:                        "（Cherry-pick）粘貼提交",
 			CheckoutFile:                      "檢出檔案",


Test patch:
diff --git a/pkg/integration/tests/branch/reset_to_upstream.go b/pkg/integration/tests/branch/reset_to_upstream.go
new file mode 100644
index 00000000000..0c749ee2d94
--- /dev/null
+++ b/pkg/integration/tests/branch/reset_to_upstream.go
@@ -0,0 +1,102 @@
+package branch
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/config"
+	. "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var ResetToUpstream = NewIntegrationTest(NewIntegrationTestArgs{
+	Description:  "Hard reset the current branch to the selected branch upstream",
+	ExtraCmdArgs: []string{},
+	Skip:         false,
+	SetupConfig:  func(config *config.AppConfig) {},
+	SetupRepo: func(shell *Shell) {
+		shell.
+			CloneIntoRemote("origin").
+			NewBranch("hard-branch").
+			EmptyCommit("hard commit").
+			PushBranch("origin", "hard-branch").
+			NewBranch("soft-branch").
+			EmptyCommit("soft commit").
+			PushBranch("origin", "soft-branch").
+			NewBranch("base").
+			EmptyCommit("base-branch commit").
+			CreateFile("file-1", "content").
+			GitAdd("file-1").
+			Commit("commit with file").
+			CreateFile("file-2", "content").
+			GitAdd("file-2")
+	},
+	Run: func(t *TestDriver, keys config.KeybindingConfig) {
+		// soft reset
+		t.Views().Branches().
+			Focus().
+			Lines(
+				Contains("base").IsSelected(),
+				Contains("soft-branch"),
+				Contains("hard-branch"),
+			).
+			Press(keys.Branches.SetUpstream).
+			Tap(func() {
+				t.ExpectPopup().Menu().
+					Title(Equals("Upstream options")).
+					Select(Contains("Reset checked-out branch onto upstream of selected branch")).
+					Tooltip(Contains("The selected branch has no upstream")).
+					Confirm()
+				t.ExpectPopup().Alert().
+					Title(Equals("Error")).
+					Content(Equals("The selected branch has no upstream")).
+					Confirm()
+			}).
+			SelectNextItem().
+			Lines(
+				Contains("base"),
+				Contains("soft-branch").IsSelected(),
+				Contains("hard-branch"),
+			).
+			Press(keys.Branches.SetUpstream).
+			Tap(func() {
+				t.ExpectPopup().Menu().
+					Title(Equals("Upstream options")).
+					Select(Contains("Reset checked-out branch onto origin/soft-branch...")).
+					Confirm()
+
+				t.ExpectPopup().Menu().
+					Title(Equals("Reset to origin/soft-branch")).
+					Select(Contains("Soft reset")).
+					Confirm()
+			})
+		t.Views().Commits().Lines(
+			Contains("soft commit"),
+			Contains("hard commit"),
+		)
+		t.Views().Files().Lines(
+			Contains("file-1").Contains("A"),
+			Contains("file-2").Contains("A"),
+		)
+
+		// hard reset
+		t.Views().Branches().
+			Focus().
+			Lines(
+				Contains("base"),
+				Contains("soft-branch").IsSelected(),
+				Contains("hard-branch"),
+			).
+			NavigateToLine(Contains("hard-branch")).
+			Press(keys.Branches.SetUpstream).
+			Tap(func() {
+				t.ExpectPopup().Menu().
+					Title(Equals("Upstream options")).
+					Select(Contains("Reset checked-out branch onto origin/hard-branch...")).
+					Confirm()
+
+				t.ExpectPopup().Menu().
+					Title(Equals("Reset to origin/hard-branch")).
+					Select(Contains("Hard reset")).
+					Confirm()
+			})
+		t.Views().Commits().Lines(Contains("hard commit"))
+		t.Views().Files().IsEmpty()
+	},
+})
diff --git a/pkg/integration/tests/branch/reset_upstream.go b/pkg/integration/tests/branch/reset_upstream.go
index 0fe11767d1e..70300c31201 100644
--- a/pkg/integration/tests/branch/reset_upstream.go
+++ b/pkg/integration/tests/branch/reset_upstream.go
@@ -25,7 +25,7 @@ var ResetUpstream = NewIntegrationTest(NewIntegrationTestArgs{
 			Press(keys.Branches.SetUpstream).
 			Tap(func() {
 				t.ExpectPopup().Menu().
-					Title(Equals("Set/Unset upstream")).
+					Title(Equals("Upstream options")).
 					Select(Contains("Unset upstream of selected branch")).
 					Confirm()
 			}).
diff --git a/pkg/integration/tests/branch/set_upstream.go b/pkg/integration/tests/branch/set_upstream.go
index 37117b0d717..16faeb7e34a 100644
--- a/pkg/integration/tests/branch/set_upstream.go
+++ b/pkg/integration/tests/branch/set_upstream.go
@@ -24,7 +24,7 @@ var SetUpstream = NewIntegrationTest(NewIntegrationTestArgs{
 			Press(keys.Branches.SetUpstream).
 			Tap(func() {
 				t.ExpectPopup().Menu().
-					Title(Equals("Set/Unset upstream")).
+					Title(Equals("Upstream options")).
 					Select(Contains(" Set upstream of selected branch")). // using leading space to disambiguate from the 'reset' option
 					Confirm()
 
diff --git a/pkg/integration/tests/branch/show_divergence_from_upstream.go b/pkg/integration/tests/branch/show_divergence_from_upstream.go
index 10fc299862b..8aff21ca9ae 100644
--- a/pkg/integration/tests/branch/show_divergence_from_upstream.go
+++ b/pkg/integration/tests/branch/show_divergence_from_upstream.go
@@ -38,7 +38,7 @@ var ShowDivergenceFromUpstream = NewIntegrationTest(NewIntegrationTestArgs{
 			Lines(Contains("master")).
 			Press(keys.Branches.SetUpstream)
 
-		t.ExpectPopup().Menu().Title(Contains("upstream")).Select(Contains("View divergence from upstream")).Confirm()
+		t.ExpectPopup().Menu().Title(Contains("Upstream")).Select(Contains("View divergence from upstream")).Confirm()
 
 		t.Views().SubCommits().
 			IsFocused().
diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go
index c669bbae391..19447b6a18e 100644
--- a/pkg/integration/tests/test_list.go
+++ b/pkg/integration/tests/test_list.go
@@ -47,6 +47,7 @@ var tests = []*components.IntegrationTest{
 	branch.RebaseDoesNotAutosquash,
 	branch.RebaseFromMarkedBase,
 	branch.Reset,
+	branch.ResetToUpstream,
 	branch.ResetUpstream,
 	branch.SetUpstream,
 	branch.ShowDivergenceFromUpstream,