mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-28 10:16:22 -04:00
Compare commits
136
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a21c98b8d4 | ||
|
|
851d17f456 | ||
|
|
c26a9218a1 | ||
|
|
c08a5fe4e7 | ||
|
|
92dd80c3e3 | ||
|
|
899e25b208 | ||
|
|
906d21f3ce | ||
|
|
7780f1264a | ||
|
|
f3718ddfb0 | ||
|
|
f9ba2dac9d | ||
|
|
6cb2ac6fcc | ||
|
|
6b9cf72e79 | ||
|
|
92f13fc56e | ||
|
|
06496ccd17 | ||
|
|
03a075c223 | ||
|
|
eef70dbdf6 | ||
|
|
f5329440fc | ||
|
|
4f6d3fb592 | ||
|
|
187a2f0cc2 | ||
|
|
f6457c4def | ||
|
|
66a04e4273 | ||
|
|
ffedd84e92 | ||
|
|
ab0b0da850 | ||
|
|
b85687797d | ||
|
|
a8921a13cb | ||
|
|
837f7456ab | ||
|
|
ddf5e24499 | ||
|
|
ca6d88080c | ||
|
|
1b7ded6df3 | ||
|
|
36a4696573 | ||
|
|
343db7b3f1 | ||
|
|
4ac77f4575 | ||
|
|
373b1970ca | ||
|
|
5b613f5bc7 | ||
|
|
8c385731f7 | ||
|
|
e79b4259e4 | ||
|
|
f4d922bc80 | ||
|
|
b2011dca35 | ||
|
|
19d0048cc4 | ||
|
|
7c51ec21bf | ||
|
|
39ea5d9ab1 | ||
|
|
205357a44f | ||
|
|
c9c556beba | ||
|
|
b82c72b63d | ||
|
|
dbdabb34f3 | ||
|
|
c1a65546ad | ||
|
|
2c9bca8b57 | ||
|
|
880528b2e4 | ||
|
|
e1b4d625c7 | ||
|
|
a957c5542f | ||
|
|
f085d10c46 | ||
|
|
1269938ee6 | ||
|
|
557dfc5a6d | ||
|
|
c5baa5da3a | ||
|
|
3284d69886 | ||
|
|
116c18e957 | ||
|
|
e93617b1de | ||
|
|
aac2535104 | ||
|
|
993d66a8ff | ||
|
|
ab797fe986 | ||
|
|
4bfda1a78c | ||
|
|
91cb33db85 | ||
|
|
7492521829 | ||
|
|
35af886f55 | ||
|
|
f45ecbc19a | ||
|
|
51eb6d7c7d | ||
|
|
38ec4e646f | ||
|
|
ef1daf314a | ||
|
|
ab643749e8 | ||
|
|
2d0c96466a | ||
|
|
f6ace8380f | ||
|
|
b4aa68d0b3 | ||
|
|
4a97fb80f3 | ||
|
|
8f1a04d095 | ||
|
|
e5f4de8d34 | ||
|
|
ce3fe37787 | ||
|
|
6cf6941a77 | ||
|
|
0fbed71f15 | ||
|
|
22a38c9f50 | ||
|
|
6343fb57d6 | ||
|
|
5af0ea85fc | ||
|
|
a4e9181a6b | ||
|
|
f5d57f7cc7 | ||
|
|
fc7b10f557 | ||
|
|
b75c177c31 | ||
|
|
9b152d7619 | ||
|
|
b98ae1c773 | ||
|
|
7d787afb2c | ||
|
|
af842e40d4 | ||
|
|
6fcb7eb8bb | ||
|
|
c5cf1b2428 | ||
|
|
d890c68cd0 | ||
|
|
0aba686f97 | ||
|
|
b91b40ba4d | ||
|
|
c4927e21c5 | ||
|
|
6afcc5bda8 | ||
|
|
9fc7a5177b | ||
|
|
fdff2dec79 | ||
|
|
9124d8dbaa | ||
|
|
4404aacdee | ||
|
|
8f5ce14dc0 | ||
|
|
866e80529b | ||
|
|
010b0ae923 | ||
|
|
a7041cf492 | ||
|
|
da3e0f7147 | ||
|
|
269d01233f | ||
|
|
615ff09afa | ||
|
|
772388294a | ||
|
|
e09b38153c | ||
|
|
cd62c719f1 | ||
|
|
36e7524146 | ||
|
|
189f39de2b | ||
|
|
5b80c0c792 | ||
|
|
d8b3c0e568 | ||
|
|
f69eb6dc48 | ||
|
|
e0a2d97f0f | ||
|
|
66d0ce841c | ||
|
|
6bb8c180b2 | ||
|
|
5558d873af | ||
|
|
380855d4e8 | ||
|
|
88e7c44552 | ||
|
|
ac0524f7cb | ||
|
|
10e29ce7dd | ||
|
|
618fe533f8 | ||
|
|
01ff18dd92 | ||
|
|
af0897f18f | ||
|
|
b7673577a2 | ||
|
|
00c55d5711 | ||
|
|
0677a58e9f | ||
|
|
b3a60ce407 | ||
|
|
b63321a302 | ||
|
|
a4354ccdfb | ||
|
|
aa81e191e2 | ||
|
|
496308e023 | ||
|
|
98c569749f | ||
|
|
047380f311 |
@@ -6,7 +6,7 @@
|
||||
* [ ] Code has been formatted (see [here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
|
||||
* [ ] Tests have been added/updated (see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md) for the integration test guide)
|
||||
* [ ] Text is internationalised (see [here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
|
||||
* [ ] Docs (specifically `docs/Config.md`) have been updated if necessary
|
||||
* [ ] Docs have been updated if necessary
|
||||
* [ ] You've read through your own file changes for silly mistakes etc
|
||||
|
||||
<!--
|
||||
|
||||
@@ -3,7 +3,7 @@ name: Continuous Delivery
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.21.x
|
||||
go-version: 1.22.x
|
||||
- name: Run goreleaser
|
||||
uses: goreleaser/goreleaser-action@v4
|
||||
with:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: Continuous Integration
|
||||
|
||||
env:
|
||||
GO_VERSION: 1.21
|
||||
GO_VERSION: 1.22
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.21.x
|
||||
go-version: 1.22.x
|
||||
- name: Test code
|
||||
# we're passing -short so that we skip the integration tests, which will be run in parallel below
|
||||
run: |
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.21.x
|
||||
go-version: 1.22.x
|
||||
- name: Print git version
|
||||
run: git --version
|
||||
- name: Test code
|
||||
@@ -115,7 +115,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.21.x
|
||||
go-version: 1.22.x
|
||||
- name: Build linux binary
|
||||
run: |
|
||||
GOOS=linux go build
|
||||
@@ -142,7 +142,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.21.x
|
||||
go-version: 1.22.x
|
||||
- name: Check Vendor Directory
|
||||
# ensure our vendor directory matches up with our go modules
|
||||
run: |
|
||||
@@ -168,11 +168,11 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.21.x
|
||||
go-version: 1.22.x
|
||||
- name: Lint
|
||||
uses: golangci/golangci-lint-action@v3.7.0
|
||||
with:
|
||||
version: latest
|
||||
version: v1.58
|
||||
- name: errors
|
||||
run: golangci-lint run
|
||||
if: ${{ failure() }}
|
||||
@@ -188,11 +188,17 @@ jobs:
|
||||
upload-coverage:
|
||||
# List all jobs that produce coverage files
|
||||
needs: [unit-tests, integration-tests]
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.22.x
|
||||
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
|
||||
+7
-2
@@ -14,10 +14,15 @@ linters:
|
||||
- exhaustive
|
||||
- makezero
|
||||
- nakedret
|
||||
# - goconst # TODO: enable and fix issues
|
||||
- copyloopvar
|
||||
fast: false
|
||||
|
||||
linters-settings:
|
||||
copyloopvar:
|
||||
# Check all assigning the loop variable to another variable.
|
||||
# Default: false
|
||||
# If true, an assignment like `a := x` will be detected as an error.
|
||||
check-alias: true
|
||||
exhaustive:
|
||||
default-signifies-exhaustive: true
|
||||
staticcheck:
|
||||
@@ -30,5 +35,5 @@ linters-settings:
|
||||
max-func-lines: 0
|
||||
|
||||
run:
|
||||
go: '1.21'
|
||||
go: '1.22'
|
||||
timeout: 10m
|
||||
|
||||
+1
-25
@@ -154,31 +154,7 @@ If you want to trigger a debug session from VSCode, you can use the following sn
|
||||
|
||||
## Profiling
|
||||
|
||||
If you want to investigate what's contributing to CPU usage you can add the following to the top of the `main()` function in `main.go`
|
||||
|
||||
```go
|
||||
import "runtime/pprof"
|
||||
|
||||
func main() {
|
||||
f, err := os.Create("cpu.prof")
|
||||
if err != nil {
|
||||
log.Fatal("could not create CPU profile: ", err)
|
||||
}
|
||||
defer f.Close()
|
||||
if err := pprof.StartCPUProfile(f); err != nil {
|
||||
log.Fatal("could not start CPU profile: ", err)
|
||||
}
|
||||
defer pprof.StopCPUProfile()
|
||||
...
|
||||
```
|
||||
|
||||
Then run lazygit, and afterwards, from your terminal, run:
|
||||
|
||||
```sh
|
||||
go tool pprof --web cpu.prof
|
||||
```
|
||||
|
||||
That should open an application which allows you to view the breakdown of CPU usage.
|
||||
If you want to investigate what's contributing to CPU or memory usage, see [this separate document](docs/dev/Profiling.md).
|
||||
|
||||
## Testing
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
# docker build -t lazygit .
|
||||
# docker run -it lazygit:latest /bin/sh
|
||||
|
||||
FROM golang:1.21 as build
|
||||
FROM golang:1.22 as build
|
||||
WORKDIR /go/src/github.com/jesseduffield/lazygit/
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
@@ -38,6 +38,10 @@ generate:
|
||||
format:
|
||||
gofumpt -l -w .
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
golangci-lint run
|
||||
|
||||
# For more details about integration test, see https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md.
|
||||
.PHONY: integration-test-tui
|
||||
integration-test-tui:
|
||||
|
||||
+508
-202
@@ -29,267 +29,564 @@ to the top of your config file or via [Visual Studio Code settings.json config][
|
||||
|
||||
## Default
|
||||
|
||||
<!-- START CONFIG YAML: AUTOMATICALLY GENERATED with `go generate ./..., DO NOT UPDATE MANUALLY -->
|
||||
```yaml
|
||||
# Config relating to the Lazygit UI
|
||||
gui:
|
||||
# stuff relating to the UI
|
||||
windowSize: 'normal' # one of 'normal' | 'half' | 'full' default is 'normal'
|
||||
scrollHeight: 2 # how many lines you scroll by
|
||||
scrollPastBottom: true # enable scrolling past the bottom
|
||||
scrollOffMargin: 2 # how many lines to keep before/after the cursor when it reaches the top/bottom of the view; see 'Scroll-off Margin' section below
|
||||
scrollOffBehavior: 'margin' # one of 'margin' | 'jump'; see 'Scroll-off Margin' section below
|
||||
sidePanelWidth: 0.3333 # number from 0 to 1
|
||||
# The number of lines you scroll by when scrolling the main window
|
||||
scrollHeight: 2
|
||||
|
||||
# If true, allow scrolling past the bottom of the content in the main window
|
||||
scrollPastBottom: true
|
||||
|
||||
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#scroll-off-margin
|
||||
scrollOffMargin: 2
|
||||
|
||||
# One of: 'margin' (default) | 'jump'
|
||||
scrollOffBehavior: margin
|
||||
|
||||
# If true, capture mouse events.
|
||||
# When mouse events are captured, it's a little harder to select text: e.g. requiring you to hold the option key when on macOS.
|
||||
mouseEvents: true
|
||||
|
||||
# If true, do not show a warning when discarding changes in the staging view.
|
||||
skipDiscardChangeWarning: false
|
||||
|
||||
# If true, do not show warning when applying/popping the stash
|
||||
skipStashWarning: false
|
||||
|
||||
# If true, do not show a warning when attempting to commit without any staged files; instead stage all unstaged files.
|
||||
skipNoStagedFilesWarning: false
|
||||
|
||||
# If true, do not show a warning when rewording a commit via an external editor
|
||||
skipRewordInEditorWarning: false
|
||||
|
||||
# Fraction of the total screen width to use for the left side section. You may want to pick a small number (e.g. 0.2) if you're using a narrow screen, so that you can see more of the main section.
|
||||
# Number from 0 to 1.0.
|
||||
sidePanelWidth: 0.3333
|
||||
|
||||
# If true, increase the height of the focused side window; creating an accordion effect.
|
||||
expandFocusedSidePanel: false
|
||||
mainPanelSplitMode: 'flexible' # one of 'horizontal' | 'flexible' | 'vertical'
|
||||
enlargedSideViewLocation: 'left' # one of 'left' | 'top'
|
||||
language: 'auto' # one of 'auto' | 'en' | 'zh-CN' | 'zh-TW' | 'pl' | 'nl' | 'ja' | 'ko' | 'ru'
|
||||
timeFormat: '02 Jan 06' # https://pkg.go.dev/time#Time.Format
|
||||
shortTimeFormat: '3:04PM'
|
||||
|
||||
# The weight of the expanded side panel, relative to the other panels. 2 means
|
||||
# twice as tall as the other panels. Only relevant if `expandFocusedSidePanel` is true.
|
||||
expandedSidePanelWeight: 2
|
||||
|
||||
# Sometimes the main window is split in two (e.g. when the selected file has both staged and unstaged changes). This setting controls how the two sections are split.
|
||||
# Options are:
|
||||
# - 'horizontal': split the window horizontally
|
||||
# - 'vertical': split the window vertically
|
||||
# - 'flexible': (default) split the window horizontally if the window is wide enough, otherwise split vertically
|
||||
mainPanelSplitMode: flexible
|
||||
|
||||
# How the window is split when in half screen mode (i.e. after hitting '+' once).
|
||||
# Possible values:
|
||||
# - 'left': split the window horizontally (side panel on the left, main view on the right)
|
||||
# - 'top': split the window vertically (side panel on top, main view below)
|
||||
enlargedSideViewLocation: left
|
||||
|
||||
# One of 'auto' (default) | 'en' | 'zh-CN' | 'zh-TW' | 'pl' | 'nl' | 'ja' | 'ko' | 'ru'
|
||||
language: auto
|
||||
|
||||
# Format used when displaying time e.g. commit time.
|
||||
# Uses Go's time format syntax: https://pkg.go.dev/time#Time.Format
|
||||
timeFormat: 02 Jan 06
|
||||
|
||||
# Format used when displaying time if the time is less than 24 hours ago.
|
||||
# Uses Go's time format syntax: https://pkg.go.dev/time#Time.Format
|
||||
shortTimeFormat: 3:04PM
|
||||
|
||||
# Config relating to colors and styles.
|
||||
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#color-attributes
|
||||
theme:
|
||||
# Border color of focused window
|
||||
activeBorderColor:
|
||||
- green
|
||||
- bold
|
||||
|
||||
# Border color of non-focused windows
|
||||
inactiveBorderColor:
|
||||
- white
|
||||
- default
|
||||
|
||||
# Border color of focused window when searching in that window
|
||||
searchingActiveBorderColor:
|
||||
- cyan
|
||||
- bold
|
||||
|
||||
# Color of keybindings help text in the bottom line
|
||||
optionsTextColor:
|
||||
- blue
|
||||
|
||||
# Background color of selected line.
|
||||
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#highlighting-the-selected-line
|
||||
selectedLineBgColor:
|
||||
- blue # set to `default` to have no background colour
|
||||
cherryPickedCommitBgColor:
|
||||
- cyan
|
||||
- blue
|
||||
|
||||
# Foreground color of copied commit
|
||||
cherryPickedCommitFgColor:
|
||||
- blue
|
||||
|
||||
# Background color of copied commit
|
||||
cherryPickedCommitBgColor:
|
||||
- cyan
|
||||
|
||||
# Foreground color of marked base commit (for rebase)
|
||||
markedBaseCommitFgColor:
|
||||
- blue
|
||||
|
||||
# Background color of marked base commit (for rebase)
|
||||
markedBaseCommitBgColor:
|
||||
- yellow
|
||||
|
||||
# Color for file with unstaged changes
|
||||
unstagedChangesColor:
|
||||
- red
|
||||
|
||||
# Default text color
|
||||
defaultFgColor:
|
||||
- default
|
||||
|
||||
# Config relating to the commit length indicator
|
||||
commitLength:
|
||||
# If true, show an indicator of commit message length
|
||||
show: true
|
||||
mouseEvents: true
|
||||
skipDiscardChangeWarning: false
|
||||
skipStashWarning: false
|
||||
showFileTree: true # for rendering changes files in a tree format
|
||||
showListFooter: true # for seeing the '5 of 20' message in list panels
|
||||
|
||||
# If true, show the '5 of 20' footer at the bottom of list views
|
||||
showListFooter: true
|
||||
|
||||
# If true, display the files in the file views as a tree. If false, display the files as a flat list.
|
||||
# This can be toggled from within Lazygit with the '~' key, but that will not change the default.
|
||||
showFileTree: true
|
||||
|
||||
# If true, show a random tip in the command log when Lazygit starts
|
||||
showRandomTip: true
|
||||
showBranchCommitHash: false # show commit hashes alongside branch names
|
||||
showBottomLine: true # for hiding the bottom information line (unless it has important information to tell you)
|
||||
showPanelJumps: true # for showing the jump-to-panel keybindings as panel subtitles
|
||||
|
||||
# If true, show the command log
|
||||
showCommandLog: true
|
||||
showIcons: false # deprecated: use nerdFontsVersion instead
|
||||
nerdFontsVersion: "" # nerd fonts version to use ("2" or "3"); empty means don't show nerd font icons
|
||||
showFileIcons: true # for hiding file icons in the file views
|
||||
|
||||
# If true, show the bottom line that contains keybinding info and useful buttons. If false, this line will be hidden except to display a loader for an in-progress action.
|
||||
showBottomLine: true
|
||||
|
||||
# If true, show jump-to-window keybindings in window titles.
|
||||
showPanelJumps: true
|
||||
|
||||
# Deprecated: use nerdFontsVersion instead
|
||||
showIcons: false
|
||||
|
||||
# Nerd fonts version to use.
|
||||
# One of: '2' | '3' | empty string (default)
|
||||
# If empty, do not show icons.
|
||||
nerdFontsVersion: ""
|
||||
|
||||
# If true (default), file icons are shown in the file views. Only relevant if NerdFontsVersion is not empty.
|
||||
showFileIcons: true
|
||||
|
||||
# Length of commit hash in commits view. 0 shows '*' if NF icons aren't on.
|
||||
commitHashLength: 8
|
||||
|
||||
# If true, show commit hashes alongside branch names in the branches view.
|
||||
showBranchCommitHash: false
|
||||
|
||||
# Whether to show the divergence from the base branch in the branches view.
|
||||
# One of: 'none' | 'onlyArrow' | 'arrowAndNumber'
|
||||
showDivergenceFromBaseBranch: none
|
||||
|
||||
# Height of the command log view
|
||||
commandLogSize: 8
|
||||
splitDiff: 'auto' # one of 'auto' | 'always'
|
||||
skipRewordInEditorWarning: false # for skipping the confirmation before launching the reword editor
|
||||
border: 'rounded' # one of 'single' | 'double' | 'rounded' | 'hidden'
|
||||
animateExplosion: true # shows an explosion animation when nuking the working tree
|
||||
portraitMode: 'auto' # one of 'auto' | 'never' | 'always'
|
||||
filterMode: 'substring' # one of 'substring' | 'fuzzy'; see 'Filtering' section below
|
||||
|
||||
# Whether to split the main window when viewing file changes.
|
||||
# One of: 'auto' | 'always'
|
||||
# If 'auto', only split the main window when a file has both staged and unstaged changes
|
||||
splitDiff: auto
|
||||
|
||||
# Default size for focused window. Window size can be changed from within Lazygit with '+' and '_' (but this won't change the default).
|
||||
# One of: 'normal' (default) | 'half' | 'full'
|
||||
windowSize: normal
|
||||
|
||||
# Window border style.
|
||||
# One of 'rounded' (default) | 'single' | 'double' | 'hidden'
|
||||
border: rounded
|
||||
|
||||
# If true, show a seriously epic explosion animation when nuking the working tree.
|
||||
animateExplosion: true
|
||||
|
||||
# Whether to stack UI components on top of each other.
|
||||
# One of 'auto' (default) | 'always' | 'never'
|
||||
portraitMode: auto
|
||||
|
||||
# How things are filtered when typing '/'.
|
||||
# One of 'substring' (default) | 'fuzzy'
|
||||
filterMode: substring
|
||||
|
||||
# Config relating to the spinner.
|
||||
spinner:
|
||||
frames: ['|', '/', '-', '\\']
|
||||
rate: 50 # spinner rate in milliseconds
|
||||
statusPanelView: 'dashboard' # one of 'dashboard' | 'allBranchesLog'
|
||||
# The frames of the spinner animation.
|
||||
frames:
|
||||
- '|'
|
||||
- /
|
||||
- '-'
|
||||
- \
|
||||
|
||||
# The "speed" of the spinner in milliseconds.
|
||||
rate: 50
|
||||
|
||||
# Status panel view.
|
||||
# One of 'dashboard' (default) | 'allBranchesLog'
|
||||
statusPanelView: dashboard
|
||||
|
||||
# Config relating to git
|
||||
git:
|
||||
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Custom_Pagers.md
|
||||
paging:
|
||||
# Value of the --color arg in the git diff command. Some pagers want this to be set to 'always' and some want it set to 'never'
|
||||
colorArg: always
|
||||
|
||||
# e.g.
|
||||
# diff-so-fancy
|
||||
# delta --dark --paging=never
|
||||
# ydiff -p cat -s --wrap --width={{columnWidth}}
|
||||
pager: ""
|
||||
|
||||
# If true, Lazygit will use whatever pager is specified in `$GIT_PAGER`, `$PAGER`, or your *git config*. If the pager ends with something like ` | less` we will strip that part out, because less doesn't play nice with our rendering approach. If the custom pager uses less under the hood, that will also break rendering (hence the `--paging=never` flag for the `delta` pager).
|
||||
useConfig: false
|
||||
|
||||
# e.g. 'difft --color=always'
|
||||
externalDiffCommand: ""
|
||||
|
||||
# Config relating to committing
|
||||
commit:
|
||||
# If true, pass '--signoff' flag when committing
|
||||
signOff: false
|
||||
autoWrapCommitMessage: true # automatic WYSIWYG wrapping of the commit message as you type
|
||||
autoWrapWidth: 72 # if autoWrapCommitMessage is true, the width to wrap to
|
||||
|
||||
# Automatic WYSIWYG wrapping of the commit message as you type
|
||||
autoWrapCommitMessage: true
|
||||
|
||||
# If autoWrapCommitMessage is true, the width to wrap to
|
||||
autoWrapWidth: 72
|
||||
|
||||
# Config relating to merging
|
||||
merging:
|
||||
# only applicable to unix users
|
||||
# If true, run merges in a subprocess so that if a commit message is required, Lazygit will not hang
|
||||
# Only applicable to unix users.
|
||||
manualCommit: false
|
||||
# extra args passed to `git merge`, e.g. --no-ff
|
||||
args: ''
|
||||
|
||||
# Extra args passed to `git merge`, e.g. --no-ff
|
||||
args: ""
|
||||
|
||||
# list of branches that are considered 'main' branches, used when displaying commits
|
||||
mainBranches:
|
||||
- master
|
||||
- main
|
||||
|
||||
# Prefix to use when skipping hooks. E.g. if set to 'WIP', then pre-commit hooks will be skipped when the commit message starts with 'WIP'
|
||||
skipHookPrefix: WIP
|
||||
|
||||
# If true, periodically fetch from remote
|
||||
autoFetch: true
|
||||
|
||||
# If true, periodically refresh files and submodules
|
||||
autoRefresh: true
|
||||
|
||||
# If true, pass the --all arg to git fetch
|
||||
fetchAll: true
|
||||
|
||||
# Command used when displaying the current branch git log in the main window
|
||||
branchLogCmd: git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --
|
||||
|
||||
# Command used to display git log of all branches in the main window
|
||||
allBranchesLogCmd: git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium
|
||||
|
||||
# If true, do not spawn a separate process when using GPG
|
||||
overrideGpg: false
|
||||
|
||||
# If true, do not allow force pushes
|
||||
disableForcePushing: false
|
||||
|
||||
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#predefined-commit-message-prefix
|
||||
commitPrefix:
|
||||
# pattern to match on. E.g. for 'feature/AB-123' to match on the AB-123 use "^\\w+\\/(\\w+-\\w+).*"
|
||||
pattern: ""
|
||||
|
||||
# Replace directive. E.g. for 'feature/AB-123' to start the commit message with 'AB-123 ' use "[$1] "
|
||||
replace: ""
|
||||
|
||||
# If true, parse emoji strings in commit messages e.g. render :rocket: as 🚀
|
||||
# (This should really be under 'gui', not 'git')
|
||||
parseEmoji: false
|
||||
|
||||
# Config for showing the log in the commits view
|
||||
log:
|
||||
# one of date-order, author-date-order, topo-order or default.
|
||||
# topo-order makes it easier to read the git log graph, but commits may not
|
||||
# appear chronologically. See https://git-scm.com/docs/git-log#_commit_ordering
|
||||
# One of: 'date-order' | 'author-date-order' | 'topo-order' | 'default'
|
||||
# 'topo-order' makes it easier to read the git log graph, but commits may not
|
||||
# appear chronologically. See https://git-scm.com/docs/
|
||||
#
|
||||
# Deprecated: Configure this with `Log menu -> Commit sort order` (<c-l> in the commits window by default).
|
||||
order: 'topo-order'
|
||||
# one of always, never, when-maximised
|
||||
# this determines whether the git graph is rendered in the commits panel
|
||||
order: topo-order
|
||||
|
||||
# This determines whether the git graph is rendered in the commits panel
|
||||
# One of 'always' | 'never' | 'when-maximised'
|
||||
#
|
||||
# Deprecated: Configure this with `Log menu -> Show git graph` (<c-l> in the commits window by default).
|
||||
showGraph: 'always'
|
||||
# displays the whole git graph by default in the commits panel (equivalent to passing the `--all` argument to `git log`)
|
||||
showGraph: always
|
||||
|
||||
# displays the whole git graph by default in the commits view (equivalent to passing the `--all` argument to `git log`)
|
||||
showWholeGraph: false
|
||||
skipHookPrefix: WIP
|
||||
# The main branches. We colour commits green if they belong to one of these branches,
|
||||
# so that you can easily see which commits are unique to your branch (coloured in yellow)
|
||||
mainBranches: [master, main]
|
||||
autoFetch: true
|
||||
autoRefresh: true
|
||||
fetchAll: true # Pass --all flag when running git fetch. Set to false to fetch only origin (or the current branch's upstream remote if there is one)
|
||||
branchLogCmd: 'git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --'
|
||||
allBranchesLogCmd: 'git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium'
|
||||
overrideGpg: false # prevents lazygit from spawning a separate process when using GPG
|
||||
disableForcePushing: false
|
||||
parseEmoji: false
|
||||
truncateCopiedCommitHashesTo: 12 # When copying commit hashes to the clipboard, truncate them to this length. Set to 40 to disable truncation.
|
||||
os:
|
||||
copyToClipboardCmd: '' # See 'Custom Command for Copying to Clipboard' section
|
||||
editPreset: '' # see 'Configuring File Editing' section
|
||||
edit: ''
|
||||
editAtLine: ''
|
||||
editAtLineAndWait: ''
|
||||
open: ''
|
||||
openLink: ''
|
||||
refresher:
|
||||
refreshInterval: 10 # File/submodule refresh interval in seconds. Auto-refresh can be disabled via option 'git.autoRefresh'.
|
||||
fetchInterval: 60 # Re-fetch interval in seconds. Auto-fetch can be disabled via option 'git.autoFetch'.
|
||||
|
||||
# When copying commit hashes to the clipboard, truncate them to this
|
||||
# length. Set to 40 to disable truncation.
|
||||
truncateCopiedCommitHashesTo: 12
|
||||
|
||||
# Periodic update checks
|
||||
update:
|
||||
method: prompt # can be: prompt | background | never
|
||||
days: 14 # how often an update is checked for
|
||||
# One of: 'prompt' (default) | 'background' | 'never'
|
||||
method: prompt
|
||||
|
||||
# Period in days between update checks
|
||||
days: 14
|
||||
|
||||
# Background refreshes
|
||||
refresher:
|
||||
# File/submodule refresh interval in seconds.
|
||||
# Auto-refresh can be disabled via option 'git.autoRefresh'.
|
||||
refreshInterval: 10
|
||||
|
||||
# Re-fetch interval in seconds.
|
||||
# Auto-fetch can be disabled via option 'git.autoFetch'.
|
||||
fetchInterval: 60
|
||||
|
||||
# If true, show a confirmation popup before quitting Lazygit
|
||||
confirmOnQuit: false
|
||||
# determines whether hitting 'esc' will quit the application when there is nothing to cancel/close
|
||||
|
||||
# If true, exit Lazygit when the user presses escape in a context where there is nothing to cancel/close
|
||||
quitOnTopLevelReturn: false
|
||||
|
||||
# Config relating to things outside of Lazygit like how files are opened, copying to clipboard, etc
|
||||
os:
|
||||
# Command for editing a file. Should contain "{{filename}}".
|
||||
edit: ""
|
||||
|
||||
# Command for editing a file at a given line number. Should contain
|
||||
# "{{filename}}", and may optionally contain "{{line}}".
|
||||
editAtLine: ""
|
||||
|
||||
# Same as EditAtLine, except that the command needs to wait until the
|
||||
# window is closed.
|
||||
editAtLineAndWait: ""
|
||||
|
||||
# For opening a directory in an editor
|
||||
openDirInEditor: ""
|
||||
|
||||
# A built-in preset that sets all of the above settings. Supported presets
|
||||
# are defined in the getPreset function in editor_presets.go.
|
||||
editPreset: ""
|
||||
|
||||
# Command for opening a file, as if the file is double-clicked. Should
|
||||
# contain "{{filename}}", but doesn't support "{{line}}".
|
||||
open: ""
|
||||
|
||||
# Command for opening a link. Should contain "{{link}}".
|
||||
openLink: ""
|
||||
|
||||
# EditCommand is the command for editing a file.
|
||||
# Deprecated: use Edit instead. Note that semantics are different:
|
||||
# EditCommand is just the command itself, whereas Edit contains a
|
||||
# "{{filename}}" variable.
|
||||
editCommand: ""
|
||||
|
||||
# EditCommandTemplate is the command template for editing a file
|
||||
# Deprecated: use EditAtLine instead.
|
||||
editCommandTemplate: ""
|
||||
|
||||
# OpenCommand is the command for opening a file
|
||||
# Deprecated: use Open instead.
|
||||
openCommand: ""
|
||||
|
||||
# OpenLinkCommand is the command for opening a link
|
||||
# Deprecated: use OpenLink instead.
|
||||
openLinkCommand: ""
|
||||
|
||||
# CopyToClipboardCmd is the command for copying to clipboard.
|
||||
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-command-for-copying-to-clipboard
|
||||
copyToClipboardCmd: ""
|
||||
|
||||
# If true, don't display introductory popups upon opening Lazygit.
|
||||
disableStartupPopups: false
|
||||
notARepository: 'prompt' # one of: 'prompt' | 'create' | 'skip' | 'quit'
|
||||
promptToReturnFromSubprocess: true # display confirmation when subprocess terminates
|
||||
|
||||
# What to do when opening Lazygit outside of a git repo.
|
||||
# - 'prompt': (default) ask whether to initialize a new repo or open in the most recent repo
|
||||
# - 'create': initialize a new repo
|
||||
# - 'skip': open most recent repo
|
||||
# - 'quit': exit Lazygit
|
||||
notARepository: prompt
|
||||
|
||||
# If true, display a confirmation when subprocess terminates. This allows you to view the output of the subprocess before returning to Lazygit.
|
||||
promptToReturnFromSubprocess: true
|
||||
|
||||
# Keybindings
|
||||
keybinding:
|
||||
universal:
|
||||
quit: 'q'
|
||||
quit-alt1: '<c-c>' # alternative/alias of quit
|
||||
return: '<esc>' # return to previous menu, will quit if there's nowhere to return
|
||||
quitWithoutChangingDirectory: 'Q'
|
||||
togglePanel: '<tab>' # goto the next panel
|
||||
prevItem: '<up>' # go one line up
|
||||
nextItem: '<down>' # go one line down
|
||||
prevItem-alt: 'k' # go one line up
|
||||
nextItem-alt: 'j' # go one line down
|
||||
prevPage: ',' # go to next page in list
|
||||
nextPage: '.' # go to previous page in list
|
||||
gotoTop: '<' # go to top of list
|
||||
gotoBottom: '>' # go to bottom of list
|
||||
scrollLeft: 'H' # scroll left within list view
|
||||
scrollRight: 'L' # scroll right within list view
|
||||
prevBlock: '<left>' # goto the previous block / panel
|
||||
nextBlock: '<right>' # goto the next block / panel
|
||||
prevBlock-alt: 'h' # goto the previous block / panel
|
||||
nextBlock-alt: 'l' # goto the next block / panel
|
||||
jumpToBlock: ['1', '2', '3', '4', '5'] # goto the Nth block / panel
|
||||
nextMatch: 'n'
|
||||
prevMatch: 'N'
|
||||
optionMenu: <disabled> # show help menu
|
||||
optionMenu-alt1: '?' # show help menu
|
||||
select: '<space>'
|
||||
goInto: '<enter>'
|
||||
openRecentRepos: '<c-r>'
|
||||
confirm: '<enter>'
|
||||
remove: 'd'
|
||||
new: 'n'
|
||||
edit: 'e'
|
||||
openFile: 'o'
|
||||
scrollUpMain: '<pgup>' # main panel scroll up
|
||||
scrollDownMain: '<pgdown>' # main panel scroll down
|
||||
scrollUpMain-alt1: 'K' # main panel scroll up
|
||||
scrollDownMain-alt1: 'J' # main panel scroll down
|
||||
scrollUpMain-alt2: '<c-u>' # main panel scroll up
|
||||
scrollDownMain-alt2: '<c-d>' # main panel scroll down
|
||||
quit: q
|
||||
quit-alt1: <c-c>
|
||||
return: <esc>
|
||||
quitWithoutChangingDirectory: Q
|
||||
togglePanel: <tab>
|
||||
prevItem: <up>
|
||||
nextItem: <down>
|
||||
prevItem-alt: k
|
||||
nextItem-alt: j
|
||||
prevPage: ','
|
||||
nextPage: .
|
||||
scrollLeft: H
|
||||
scrollRight: L
|
||||
gotoTop: <
|
||||
gotoBottom: '>'
|
||||
toggleRangeSelect: v
|
||||
rangeSelectDown: <s-down>
|
||||
rangeSelectUp: <s-up>
|
||||
prevBlock: <left>
|
||||
nextBlock: <right>
|
||||
prevBlock-alt: h
|
||||
nextBlock-alt: l
|
||||
nextBlock-alt2: <tab>
|
||||
prevBlock-alt2: <backtab>
|
||||
jumpToBlock:
|
||||
- "1"
|
||||
- "2"
|
||||
- "3"
|
||||
- "4"
|
||||
- "5"
|
||||
nextMatch: "n"
|
||||
prevMatch: "N"
|
||||
startSearch: /
|
||||
optionMenu: <disabled>
|
||||
optionMenu-alt1: '?'
|
||||
select: <space>
|
||||
goInto: <enter>
|
||||
confirm: <enter>
|
||||
confirmInEditor: <a-enter>
|
||||
remove: d
|
||||
new: "n"
|
||||
edit: e
|
||||
openFile: o
|
||||
scrollUpMain: <pgup>
|
||||
scrollDownMain: <pgdown>
|
||||
scrollUpMain-alt1: K
|
||||
scrollDownMain-alt1: J
|
||||
scrollUpMain-alt2: <c-u>
|
||||
scrollDownMain-alt2: <c-d>
|
||||
executeCustomCommand: ':'
|
||||
createRebaseOptionsMenu: 'm'
|
||||
pushFiles: 'P'
|
||||
pullFiles: 'p'
|
||||
refresh: 'R'
|
||||
createPatchOptionsMenu: '<c-p>'
|
||||
createRebaseOptionsMenu: m
|
||||
|
||||
# 'Files' appended for legacy reasons
|
||||
pushFiles: P
|
||||
|
||||
# 'Files' appended for legacy reasons
|
||||
pullFiles: p
|
||||
refresh: R
|
||||
createPatchOptionsMenu: <c-p>
|
||||
nextTab: ']'
|
||||
prevTab: '['
|
||||
nextScreenMode: '+'
|
||||
prevScreenMode: '_'
|
||||
undo: 'z'
|
||||
redo: '<c-z>'
|
||||
filteringMenu: '<c-s>'
|
||||
diffingMenu: 'W'
|
||||
diffingMenu-alt: '<c-e>' # deprecated
|
||||
copyToClipboard: '<c-o>'
|
||||
submitEditorText: '<enter>'
|
||||
nextScreenMode: +
|
||||
prevScreenMode: _
|
||||
undo: z
|
||||
redo: <c-z>
|
||||
filteringMenu: <c-s>
|
||||
diffingMenu: W
|
||||
diffingMenu-alt: <c-e>
|
||||
copyToClipboard: <c-o>
|
||||
openRecentRepos: <c-r>
|
||||
submitEditorText: <enter>
|
||||
extrasMenu: '@'
|
||||
toggleWhitespaceInDiffView: '<c-w>'
|
||||
toggleWhitespaceInDiffView: <c-w>
|
||||
increaseContextInDiffView: '}'
|
||||
decreaseContextInDiffView: '{'
|
||||
toggleRangeSelect: 'v'
|
||||
rangeSelectUp: '<s-up>'
|
||||
rangeSelectDown: '<s-down>'
|
||||
openDiffTool: <c-t>
|
||||
status:
|
||||
checkForUpdate: 'u'
|
||||
recentRepos: '<enter>'
|
||||
checkForUpdate: u
|
||||
recentRepos: <enter>
|
||||
allBranchesLogGraph: a
|
||||
files:
|
||||
commitChanges: 'c'
|
||||
commitChangesWithoutHook: 'w' # commit changes without pre-commit hook
|
||||
amendLastCommit: 'A'
|
||||
commitChangesWithEditor: 'C'
|
||||
findBaseCommitForFixup: '<c-f>'
|
||||
confirmDiscard: 'x'
|
||||
ignoreFile: 'i'
|
||||
refreshFiles: 'r'
|
||||
stashAllChanges: 's'
|
||||
viewStashOptions: 'S'
|
||||
toggleStagedAll: 'a' # stage/unstage all
|
||||
viewResetOptions: 'D'
|
||||
fetch: 'f'
|
||||
commitChanges: c
|
||||
commitChangesWithoutHook: w
|
||||
amendLastCommit: A
|
||||
commitChangesWithEditor: C
|
||||
findBaseCommitForFixup: <c-f>
|
||||
confirmDiscard: x
|
||||
ignoreFile: i
|
||||
refreshFiles: r
|
||||
stashAllChanges: s
|
||||
viewStashOptions: S
|
||||
toggleStagedAll: a
|
||||
viewResetOptions: D
|
||||
fetch: f
|
||||
toggleTreeView: '`'
|
||||
openMergeTool: 'M'
|
||||
openStatusFilter: '<c-b>'
|
||||
openMergeTool: M
|
||||
openStatusFilter: <c-b>
|
||||
copyFileInfoToClipboard: "y"
|
||||
branches:
|
||||
createPullRequest: 'o'
|
||||
viewPullRequestOptions: 'O'
|
||||
checkoutBranchByName: 'c'
|
||||
forceCheckoutBranch: 'F'
|
||||
rebaseBranch: 'r'
|
||||
renameBranch: 'R'
|
||||
mergeIntoCurrentBranch: 'M'
|
||||
viewGitFlowOptions: 'i'
|
||||
fastForward: 'f' # fast-forward this branch from its upstream
|
||||
createTag: 'T'
|
||||
pushTag: 'P'
|
||||
setUpstream: 'u' # set as upstream of checked-out branch
|
||||
fetchRemote: 'f'
|
||||
createPullRequest: o
|
||||
viewPullRequestOptions: O
|
||||
copyPullRequestURL: <c-y>
|
||||
checkoutBranchByName: c
|
||||
forceCheckoutBranch: F
|
||||
rebaseBranch: r
|
||||
renameBranch: R
|
||||
mergeIntoCurrentBranch: M
|
||||
viewGitFlowOptions: i
|
||||
fastForward: f
|
||||
createTag: T
|
||||
pushTag: P
|
||||
setUpstream: u
|
||||
fetchRemote: f
|
||||
sortOrder: s
|
||||
worktrees:
|
||||
viewWorktreeOptions: w
|
||||
commits:
|
||||
squashDown: 's'
|
||||
renameCommit: 'r'
|
||||
renameCommitWithEditor: 'R'
|
||||
viewResetOptions: 'g'
|
||||
markCommitAsFixup: 'f'
|
||||
createFixupCommit: 'F' # create fixup commit for this commit
|
||||
squashAboveCommits: 'S'
|
||||
moveDownCommit: '<c-j>' # move commit down one
|
||||
moveUpCommit: '<c-k>' # move commit up one
|
||||
amendToCommit: 'A'
|
||||
amendAttributeMenu: 'a'
|
||||
pickCommit: 'p' # pick commit (when mid-rebase)
|
||||
revertCommit: 't'
|
||||
cherryPickCopy: 'C'
|
||||
pasteCommits: 'V'
|
||||
tagCommit: 'T'
|
||||
checkoutCommit: '<space>'
|
||||
resetCherryPick: '<c-R>'
|
||||
copyCommitMessageToClipboard: '<c-y>'
|
||||
openLogMenu: '<c-l>'
|
||||
viewBisectOptions: 'b'
|
||||
stash:
|
||||
popStash: 'g'
|
||||
renameStash: 'r'
|
||||
commitFiles:
|
||||
checkoutCommitFile: 'c'
|
||||
main:
|
||||
toggleSelectHunk: 'a'
|
||||
pickBothHunks: 'b'
|
||||
submodules:
|
||||
init: 'i'
|
||||
update: 'u'
|
||||
bulkMenu: 'b'
|
||||
commitMessage:
|
||||
commitMenu: '<c-o>'
|
||||
squashDown: s
|
||||
renameCommit: r
|
||||
renameCommitWithEditor: R
|
||||
viewResetOptions: g
|
||||
markCommitAsFixup: f
|
||||
createFixupCommit: F
|
||||
squashAboveCommits: S
|
||||
moveDownCommit: <c-j>
|
||||
moveUpCommit: <c-k>
|
||||
amendToCommit: A
|
||||
resetCommitAuthor: a
|
||||
pickCommit: p
|
||||
revertCommit: t
|
||||
cherryPickCopy: C
|
||||
pasteCommits: V
|
||||
markCommitAsBaseForRebase: B
|
||||
tagCommit: T
|
||||
checkoutCommit: <space>
|
||||
resetCherryPick: <c-R>
|
||||
copyCommitAttributeToClipboard: "y"
|
||||
openLogMenu: <c-l>
|
||||
openInBrowser: o
|
||||
viewBisectOptions: b
|
||||
startInteractiveRebase: i
|
||||
amendAttribute:
|
||||
addCoAuthor: 'c'
|
||||
resetAuthor: 'a'
|
||||
setAuthor: 'A'
|
||||
resetAuthor: a
|
||||
setAuthor: A
|
||||
addCoAuthor: c
|
||||
stash:
|
||||
popStash: g
|
||||
renameStash: r
|
||||
commitFiles:
|
||||
checkoutCommitFile: c
|
||||
main:
|
||||
toggleSelectHunk: a
|
||||
pickBothHunks: b
|
||||
editSelectHunk: E
|
||||
submodules:
|
||||
init: i
|
||||
update: u
|
||||
bulkMenu: b
|
||||
commitMessage:
|
||||
commitMenu: <c-o>
|
||||
```
|
||||
<!-- END CONFIG YAML -->
|
||||
|
||||
## Platform Defaults
|
||||
|
||||
@@ -548,6 +845,15 @@ Example:
|
||||
- Branch name: feature/AB-123
|
||||
- Commit message: [AB-123] Adding feature
|
||||
|
||||
```yaml
|
||||
git:
|
||||
commitPrefix:
|
||||
pattern: "^\\w+\\/(\\w+-\\w+).*"
|
||||
replace: '[$1] '
|
||||
```
|
||||
|
||||
If you want repository-specific prefixes, you can map them with `commitPrefixes`. If you have both `commitPrefixes` defined and an entry in `commitPrefixes` for the current repo, the `commitPrefixes` entry is given higher precedence. Repository folder names must be an exact match.
|
||||
|
||||
```yaml
|
||||
git:
|
||||
commitPrefixes:
|
||||
|
||||
@@ -59,6 +59,7 @@ For a given custom command, here are the allowed fields:
|
||||
| description | Label for the custom command when displayed in the keybindings menu | no |
|
||||
| stream | Whether you want to stream the command's output to the Command Log panel | no |
|
||||
| showOutput | Whether you want to show the command's output in a popup within Lazygit | no |
|
||||
| outputTitle | The title to display in the popup panel if showOutput is true. If left unset, the command will be used as the title. | no |
|
||||
| after | Actions to take after the command has completed | no |
|
||||
|
||||
Here are the options for the `after` key:
|
||||
@@ -305,7 +306,7 @@ SelectedWorktree
|
||||
CheckedOutBranch
|
||||
```
|
||||
|
||||
To see what fields are available on e.g. the `SelectedFile`, see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/commands/models/file.go) (all the modelling lives in the same directory). Note that the custom commands feature does not guarantee backwards compatibility (until we hit Lazygit version 1.0 of course) which means a field you're accessing on an object may no longer be available from one release to the next. Typically however, all you'll need is `{{.SelectedFile.Name}}`, `{{.SelectedLocalCommit.Hash}}` and `{{.SelectedLocalBranch.Name}}`. In the future we will likely introduce a tighter interface that exposes a limited set of fields for each model.
|
||||
To see what fields are available on e.g. the `SelectedFile`, see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/gui/services/custom_commands/models.go) (all the modelling lives in the same file).
|
||||
|
||||
## Keybinding collisions
|
||||
|
||||
|
||||
+7
-19
@@ -56,22 +56,10 @@ base commit in the Commits view automatically. From there, you can either press
|
||||
shift-F to create a fixup commit for it, or shift-A to amend your changes into
|
||||
the commit if you haven't published your branch yet.
|
||||
|
||||
This command works in many cases, and when it does it almost feels like magic,
|
||||
but it's important to understand its limitations because it doesn't always work.
|
||||
The way it works is that it looks at the deleted lines of your current
|
||||
modifications, blames them to find out which commit those lines come from, and
|
||||
if they all come from the same commit, it selects it. So here are cases where it
|
||||
doesn't work:
|
||||
|
||||
- Your current diff has only added lines, but no deleted lines. In this case
|
||||
there's no way for lazygit to know which commit you want to add them to.
|
||||
- The deleted lines belong to multiple different commits. In this case you can
|
||||
help lazygit by staging a set of files or hunks that all belong to the same
|
||||
commit; if some changes are staged, the ctrl-f command works only on those.
|
||||
- The found commit is already on master; in this case, lazygit refuses to select
|
||||
it, because it doesn't make sense to create fixups for it, let alone amend to
|
||||
it.
|
||||
|
||||
To sum it up: the command works great if you are changing code again that you
|
||||
changed or added earlier in the same branch. This is a common enough case to
|
||||
make the command useful.
|
||||
If you have many modifications in your working copy, it is a good idea to stage
|
||||
related changes that are meant to go into the same fixup commit; if no changes
|
||||
are staged, ctrl-f works on all unstaged modifications, and then it might show
|
||||
an error if it finds multiple different base commits. If you are interested in
|
||||
what the command does to do its magic, and how you can help it work better, you
|
||||
may want to read the [design document](dev/Find_Base_Commit_For_Fixup_Design.md)
|
||||
that describes this.
|
||||
|
||||
@@ -13,6 +13,6 @@ includes interactive rebases, so for example amending a commit in the first
|
||||
branch of the stack will "just work" in the sense that it keeps the other
|
||||
branches properly stacked onto it.
|
||||
|
||||
Lazygit visualizes the invidual branch heads in the stack by marking them with a
|
||||
Lazygit visualizes the individual branch heads in the stack by marking them with a
|
||||
cyan asterisk (or a cyan branch symbol if you are using [nerd
|
||||
fonts](Config.md#display-nerd-fonts-icons)).
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
# About the mechanics of lazygit's "Find base commit for fixup" command
|
||||
|
||||
## Background
|
||||
|
||||
Lazygit has a command called "Find base commit for fixup" that helps with
|
||||
creating fixup commits. (It is bound to "ctrl-f" by default, and I'll call it
|
||||
simply "the ctrl-f command" throughout the rest of this text for brevity.)
|
||||
|
||||
It's a heuristic that needs to make a few assumptions; it tends to work well in
|
||||
practice if users are aware of its limitations. The user-facing side of the
|
||||
topic is explained [here](../Fixup_Commits.md). In this document we describe how
|
||||
it works internally, and the design decisions behind it.
|
||||
|
||||
It is also interesting to compare it to the standalone tool
|
||||
[git-absorb](https://github.com/tummychow/git-absorb) which does a very similar
|
||||
thing, but made different decisions in some cases. We'll explore these
|
||||
differences in this document.
|
||||
|
||||
## Design goals
|
||||
|
||||
I'll start with git-absorb's design goals (my interpretation, since I can't
|
||||
speak for git-absorb's maintainer of course): its main goal seems to be minimum
|
||||
user interaction required. The idea is that you have a PR in review, the
|
||||
reviewer requested a bunch of changes, you make all these changes, so you have a
|
||||
working copy with lots of modified files, and then you fire up git-absorb and it
|
||||
creates all the necessary fixup commits automatically with no further user
|
||||
intervention.
|
||||
|
||||
While this sounds attractive, it conflicts with ctrl-f's main design goal, which
|
||||
is to support creating high-quality fixups. My philosophy is that fixup commits
|
||||
should have the same high quality standards as normal commits; in particular:
|
||||
|
||||
- they should be atomic. This means that multiple diff hunks that belong
|
||||
together to form one logical change should be in the same fixup commit. (Not
|
||||
always possible if the logical change needs to be fixed up into several
|
||||
different base commits.)
|
||||
- they should be minimal. Every fixup commit should ideally contain only one
|
||||
logical change, not several unrelated ones.
|
||||
|
||||
Why is this important? Because fixup commits are mainly a tool for reviewing (if
|
||||
they weren't, you might as well squash the changes into their base commits right
|
||||
away). And reviewing fixup commits is easier if they are well-structured, just
|
||||
like normal commits.
|
||||
|
||||
The only way to achieve this with git-absorb is to set the `oneFixupPerCommit`
|
||||
config option (for the first goal), and then manually stage the changes that
|
||||
belong together (for the second). This is close to what you have to do with
|
||||
ctrl-f, with one exception that we'll get to below.
|
||||
|
||||
But ctrl-f enforces this by refusing to do the job if the staged hunks belong to
|
||||
more than one base commit. Git-absorb will happily create multiple fixup commits
|
||||
in this case; ctrl-f doesn't, to enforce that you pay attention to how you group
|
||||
the changes. There's another reason for this behavior: ctrl-f doesn't create
|
||||
fixup commits itself (unlike git-absorb), instead it just selects the found base
|
||||
commit so that the user can decide whether to amend the changes right in, or
|
||||
create a fixup commit from there (both are single-key commands in lazygit). And
|
||||
lazygit doesn't support non-contiguous multiselections of commits, but even if
|
||||
it did, it wouldn't help much in this case.
|
||||
|
||||
## The mechanics
|
||||
|
||||
### General approach
|
||||
|
||||
Git-absorb uses a relatively simple approach, and the benefit is of course that
|
||||
it is easy to understand: it looks at every diff hunk separately, and for every
|
||||
hunk it looks at all commits (starting from the newest one backwards) to find
|
||||
the earliest commit that the change can be amended to without conflicts.
|
||||
|
||||
It is important to realize that "diff hunk" doesn't necessarily mean what you
|
||||
see in the diff view. Git-absorb and ctrl-f both use a context of 0 when diffing
|
||||
your code, so they often see more and smaller hunks than users do. For example,
|
||||
moving a line of code down by one line is a single hunk for users, but it's two
|
||||
separate hunks for git-absorb and ctrl-f; one for deleting the line at the old
|
||||
place, and another one for adding the line at the new place, even if it's only
|
||||
one line further down.
|
||||
|
||||
From this, it follows that there's one big problem with git-absorb's approach:
|
||||
when moving code, it doesn't realize that the two related hunks of deleting the
|
||||
code from the old place and inserting it at the new place belong together, and
|
||||
often it will manage to create a fixup commit for the first hunk, but leave the
|
||||
other hunk in your working copy as "don't know what to do with this". As an
|
||||
example, suppose your PR is adding a line of code to an existing function, maybe
|
||||
one that declares a new variable, and a reviewer suggests to move this line down
|
||||
a bit, closer to where some other related variables are declared. Moving the
|
||||
line down results in two diff hunks (from the perspective of git-absorb and
|
||||
ctrl-f, as they both use a context of 0 when diffing), and when looking at the
|
||||
second diff hunk in isolation there's no way to find a base commit in your PR
|
||||
for it, because the surrounding code is already on main.
|
||||
|
||||
To solve this, the ctrl-f command makes a distinction between hunks that have
|
||||
deleted lines and hunks that have only added lines. If the whole diff contains
|
||||
any hunks that have deleted lines, it uses only those hunks to determine the
|
||||
base commit, and then assumes that all the hunks that have only added lines
|
||||
belong into the same commit. This nicely solves the above example of moving
|
||||
code, but also other examples such as the following:
|
||||
|
||||
<details>
|
||||
<summary>Click to show example</summary>
|
||||
|
||||
Suppose you have a PR in which you added the following function:
|
||||
|
||||
```go
|
||||
func findCommit(hash string) (*models.Commit, int, bool) {
|
||||
for i, commit := range self.c.Model().Commits {
|
||||
if commit.Hash == hash {
|
||||
return commit, i, true
|
||||
}
|
||||
}
|
||||
|
||||
return nil, -1, false
|
||||
}
|
||||
```
|
||||
|
||||
A reviewer suggests to replace the manual `for` loop with a call to
|
||||
`lo.FindIndexOf` since that's less code and more idiomatic. So your modification
|
||||
is this:
|
||||
|
||||
```diff
|
||||
--- a/my_file.go
|
||||
+++ b/my_file.go
|
||||
@@ -12,2 +12,3 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
+ "github.com/samber/lo"
|
||||
"golang.org/x/sync/errgroup"
|
||||
@@ -308,9 +309,5 @@ func (self *FixupHelper) blameAddedLines(addedLineHunks []*hunk) ([]string, erro
|
||||
func findCommit(hash string) (*models.Commit, int, bool) {
|
||||
- for i, commit := range self.c.Model().Commits {
|
||||
- if commit.Hash == hash {
|
||||
- return commit, i, true
|
||||
- }
|
||||
- }
|
||||
-
|
||||
- return nil, -1, false
|
||||
+ return lo.FindIndexOf(self.c.Model().Commits, func(commit *models.Commit) bool {
|
||||
+ return commit.Hash == hash
|
||||
+ })
|
||||
}
|
||||
```
|
||||
|
||||
If we were to look at these two hunks separately, we'd easily find the base
|
||||
commit for the second one, but we wouldn't find the one for the first hunk
|
||||
because the imports around the added import have been on main for a long time.
|
||||
In fact, git-absorb leaves this hunk in the working copy because it doesn't know
|
||||
what to do with it.
|
||||
|
||||
</details>
|
||||
|
||||
Only if there are no hunks with deleted lines does ctrl-f look at the hunks with
|
||||
only added lines and determines the base commit for them. This solves cases like
|
||||
adding a comment above a function that you added in your PR.
|
||||
|
||||
The downside of this more complicated approach is that it relies on the user
|
||||
staging related hunks correctly. However, in my experience this is easy to do
|
||||
and not very error-prone, as long as users are aware of this behavior. Lazygit
|
||||
tries to help making them aware of it by showing a warning whenever there are
|
||||
hunks with only added lines in addition to hunks with deleted lines.
|
||||
|
||||
### Finding the base commit for a given hunk
|
||||
|
||||
As explained above, git-absorb finds the base commit by walking the commits
|
||||
backwards until it finds one that conflicts with the hunk, and then the found
|
||||
base commit is the one just before that one. This works reliably, but it is
|
||||
slow.
|
||||
|
||||
Ctrl-f uses a different approach that is usually much faster, but should always
|
||||
yield the same result. Again, it makes a distinction between hunks with deleted
|
||||
lines and hunks with only added lines. For hunks with deleted lines it performs
|
||||
a line range blame for all the deleted lines (e.g. `git blame -L42,+3 --
|
||||
filename`), and if the result is the same for all deleted lines, then that's the
|
||||
base commit; otherwise it returns an error.
|
||||
|
||||
For hunks with only added lines, it gets a little more complicated. We blame the
|
||||
single lines just before and just after the hunk (I'll ignore the edge cases of
|
||||
either of those not existing because the hunk is at the beginning or end of the
|
||||
file; read the code to see how we handle these cases). If the blame result is
|
||||
the same for both, then that's the base commit. This is the case of adding a
|
||||
line in the middle of a block of code that was added in the PR. Otherwise, the
|
||||
base commit is the more recent of the two (and in this case it doesn't matter if
|
||||
the other one is an earlier commit in the current branch, or a possibly very old
|
||||
commit that's already on main). This covers the common case of adding a comment
|
||||
to a function that was added in the PR, but also adding another line at the end
|
||||
of a block of code that was added in the base commit.
|
||||
|
||||
It's interesting to discuss what "more recent" means here. You could say if
|
||||
commit A is an ancestor of commit B (or in other words, A is reachable from B)
|
||||
then B is the more recent one. And if none of the two commits is reachable from
|
||||
the other, you have an error case because it's unclear which of the two should
|
||||
be considered the base commit. The scenario in which this happens is a commit
|
||||
history like this:
|
||||
|
||||
```
|
||||
C---D
|
||||
/ \
|
||||
A---B---E---F---G
|
||||
```
|
||||
|
||||
where, for instance, D and E are the two blame results.
|
||||
|
||||
Unfortunately, determining the ancestry relationship between two commits using
|
||||
git commands is a bit expensive and not totally straightforward. Fortunately,
|
||||
it's not necessary in lazygit because lazygit has the most recent 300 commits
|
||||
cached in memory, and can simply search its linear list of commits to see which
|
||||
one is closer to the beginning of the list. If only one of the two commits is
|
||||
found within those 300 commits, then that's the more recent one; if neither is
|
||||
found, we assume that both commits are on main and error out. In the merge
|
||||
scenario pictured above, we arbitrarily return one of the two commits (this will
|
||||
depend on the log order), but that's probably fine as this scenario should be
|
||||
extremely rare in practice; in most cases, feature branches are simply linear.
|
||||
|
||||
### Knowing where to stop searching
|
||||
|
||||
Git-absorb needs to know when to stop walking backwards searching for commits,
|
||||
since it doesn't make sense to create fixups for commits that are already on
|
||||
main. However, it doesn't know where the current branch ends and main starts, so
|
||||
it needs to rely on user input for this. By default it searches the most recent
|
||||
10 commits, but this can be overridden with a config setting. In longer branches
|
||||
this is often not enough for finding the base commit; but setting it to a higher
|
||||
value causes the command to take longer to complete when the base commit can't
|
||||
be found.
|
||||
|
||||
Lazygit doesn't have this problem. For a given blame result it needs to
|
||||
determine whether that commit is already on main, and if it can find the commit
|
||||
in its cached list of the first 300 commits it can get that information from
|
||||
there, because lazygit knows what the user's configured main branches are
|
||||
(`master` and `main` by default, but it could also include branches like `devel`
|
||||
or `1.0-hotfixes`), and so it can tell for each commit whether it's contained in
|
||||
one of those main branches. And if it can't find it among the first 300 commits,
|
||||
it assumes the commit already on main, on the assumption that no feature branch
|
||||
has more than 300 commits.
|
||||
@@ -0,0 +1,69 @@
|
||||
# Profiling Lazygit
|
||||
|
||||
If you want to investigate what's contributing to CPU or memory usage, start
|
||||
lazygit with the `-profile` command line flag. This tells it to start an
|
||||
integrated web server that listens for profiling requests.
|
||||
|
||||
## Save profile data
|
||||
|
||||
### CPU
|
||||
|
||||
While lazygit is running with the `-profile` flag, perform a CPU profile and
|
||||
save it to a file by running this command in another terminal window:
|
||||
|
||||
```sh
|
||||
curl -o cpu.out http://127.0.0.1:6060/debug/pprof/profile
|
||||
```
|
||||
|
||||
By default, it profiles for 30 seconds. To change the duration, use
|
||||
|
||||
```sh
|
||||
curl -o cpu.out 'http://127.0.0.1:6060/debug/pprof/profile?seconds=60'
|
||||
```
|
||||
|
||||
### Memory
|
||||
|
||||
To save a heap profile (containing information about all memory allocated so
|
||||
far since startup), use
|
||||
|
||||
```sh
|
||||
curl -o mem.out http://127.0.0.1:6060/debug/pprof/heap
|
||||
```
|
||||
|
||||
Sometimes it can be useful to get a delta log, i.e. to see how memory usage
|
||||
developed from one point in time to another. For that, use
|
||||
|
||||
```sh
|
||||
curl -o mem.out 'http://127.0.0.1:6060/debug/pprof/heap?seconds=20'
|
||||
```
|
||||
|
||||
This will log the memory usage difference between now and 20 seconds later, so
|
||||
it gives you 20 seconds to perform the action in lazygit that you are interested
|
||||
in measuring.
|
||||
|
||||
## View profile data
|
||||
|
||||
To display the profile data, you can either use speedscope.app, or the pprof
|
||||
tool that comes with go. I prefer the former because it has a nicer UI and is a
|
||||
little more powerful; however, I have seen cases where it wasn't able to load a
|
||||
profile for some reason, in which case it's good to have the pprof tool as a
|
||||
fallback.
|
||||
|
||||
### Speedscope.app
|
||||
|
||||
Go to https://www.speedscope.app/ in your browser, and drag the saved profile
|
||||
onto the browser window. Refer to [the
|
||||
documentation](https://github.com/jlfwong/speedscope?tab=readme-ov-file#usage)
|
||||
for how to navigate the data.
|
||||
|
||||
### Pprof tool
|
||||
|
||||
To view a profile that you saved as `cpu.out`, use
|
||||
|
||||
```sh
|
||||
go tool pprof -http=:8080 cpu.out
|
||||
```
|
||||
|
||||
By default this shows the graph view, which I don't find very useful myself.
|
||||
Choose "Flame Graph" from the View menu to show a much more useful
|
||||
representation of the data.
|
||||
@@ -4,3 +4,5 @@
|
||||
* [Busy/Idle Tracking](./Busy.md)
|
||||
* [Integration Tests](../../pkg/integration/README.md)
|
||||
* [Demo Recordings](./Demo_Recordings.md)
|
||||
* [Find base commit for fixup design](Find_Base_Commit_For_Fixup_Design.md)
|
||||
* [Profiling](Profiling.md)
|
||||
|
||||
@@ -80,7 +80,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
| `` <c-r> `` | Reset copied (cherry-picked) commits selection | |
|
||||
| `` b `` | View bisect options | |
|
||||
| `` s `` | Squash | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. |
|
||||
| `` f `` | Fixup | Meld the selected commit into the commit below it. Similar to fixup, but the selected commit's message will be discarded. |
|
||||
| `` f `` | Fixup | Meld the selected commit into the commit below it. Similar to squash, but the selected commit's message will be discarded. |
|
||||
| `` r `` | Reword | Reword the selected commit's message. |
|
||||
| `` R `` | Reword with editor | |
|
||||
| `` d `` | Drop | Drop the selected commit. This will remove the commit from the branch via a rebase. If the commit makes changes that later commits depend on, you may need to resolve merge conflicts. |
|
||||
|
||||
@@ -97,7 +97,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
| `` <c-r> `` | Reset copied (cherry-picked) commits selection | |
|
||||
| `` b `` | View bisect options | |
|
||||
| `` s `` | Squash | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. |
|
||||
| `` f `` | Fixup | Meld the selected commit into the commit below it. Similar to fixup, but the selected commit's message will be discarded. |
|
||||
| `` f `` | Fixup | Meld the selected commit into the commit below it. Similar to squash, but the selected commit's message will be discarded. |
|
||||
| `` r `` | コミットメッセージを変更 | Reword the selected commit's message. |
|
||||
| `` R `` | エディタでコミットメッセージを編集 | |
|
||||
| `` d `` | コミットを削除 | Drop the selected commit. This will remove the commit from the branch via a rebase. If the commit makes changes that later commits depend on, you may need to resolve merge conflicts. |
|
||||
|
||||
@@ -260,7 +260,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
| `` <c-r> `` | Reset cherry-picked (copied) commits selection | |
|
||||
| `` b `` | Bisect 옵션 보기 | |
|
||||
| `` s `` | Squash | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. |
|
||||
| `` f `` | Fixup | Meld the selected commit into the commit below it. Similar to fixup, but the selected commit's message will be discarded. |
|
||||
| `` f `` | Fixup | Meld the selected commit into the commit below it. Similar to squash, but the selected commit's message will be discarded. |
|
||||
| `` r `` | 커밋메시지 변경 | Reword the selected commit's message. |
|
||||
| `` R `` | 에디터에서 커밋메시지 수정 | |
|
||||
| `` d `` | 커밋 삭제 | Drop the selected commit. This will remove the commit from the branch via a rebase. If the commit makes changes that later commits depend on, you may need to resolve merge conflicts. |
|
||||
|
||||
@@ -143,7 +143,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
| `` <c-r> `` | Reset cherry-picked (gekopieerde) commits selectie | |
|
||||
| `` b `` | View bisect options | |
|
||||
| `` s `` | Squash | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. |
|
||||
| `` f `` | Fixup | Meld the selected commit into the commit below it. Similar to fixup, but the selected commit's message will be discarded. |
|
||||
| `` f `` | Fixup | Meld the selected commit into the commit below it. Similar to squash, but the selected commit's message will be discarded. |
|
||||
| `` r `` | Hernoem commit | Reword the selected commit's message. |
|
||||
| `` R `` | Hernoem commit met editor | |
|
||||
| `` d `` | Verwijder commit | Drop the selected commit. This will remove the commit from the branch via a rebase. If the commit makes changes that later commits depend on, you may need to resolve merge conflicts. |
|
||||
|
||||
@@ -144,7 +144,7 @@ _Связки клавиш_
|
||||
| `` <c-r> `` | Сбросить отобранную (скопированную | cherry-picked) выборку коммитов | |
|
||||
| `` b `` | Просмотреть параметры бинарного поиска | |
|
||||
| `` s `` | Объединить коммиты (Squash) | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. |
|
||||
| `` f `` | Объединить несколько коммитов в один отбросив сообщение коммита (Fixup) | Meld the selected commit into the commit below it. Similar to fixup, but the selected commit's message will be discarded. |
|
||||
| `` f `` | Объединить несколько коммитов в один отбросив сообщение коммита (Fixup) | Meld the selected commit into the commit below it. Similar to squash, but the selected commit's message will be discarded. |
|
||||
| `` r `` | Перефразировать коммит | Reword the selected commit's message. |
|
||||
| `` R `` | Переписать коммит с помощью редактора | |
|
||||
| `` d `` | Удалить коммит | Drop the selected commit. This will remove the commit from the branch via a rebase. If the commit makes changes that later commits depend on, you may need to resolve merge conflicts. |
|
||||
|
||||
@@ -141,7 +141,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
| `` <c-r> `` | 重置已拣选(复制)的提交 | |
|
||||
| `` b `` | 查看二分查找选项 | |
|
||||
| `` s `` | 压缩 | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. |
|
||||
| `` f `` | 修正(fixup) | Meld the selected commit into the commit below it. Similar to fixup, but the selected commit's message will be discarded. |
|
||||
| `` f `` | 修正(fixup) | Meld the selected commit into the commit below it. Similar to squash, but the selected commit's message will be discarded. |
|
||||
| `` r `` | 改写提交 | Reword the selected commit's message. |
|
||||
| `` R `` | 使用编辑器重命名提交 | |
|
||||
| `` d `` | 删除提交 | Drop the selected commit. This will remove the commit from the branch via a rebase. If the commit makes changes that later commits depend on, you may need to resolve merge conflicts. |
|
||||
|
||||
@@ -166,7 +166,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B
|
||||
| `` <c-r> `` | 重設選定的揀選 (複製) 提交 | |
|
||||
| `` b `` | 查看二分選項 | |
|
||||
| `` s `` | 壓縮 (Squash) | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. |
|
||||
| `` f `` | 修復 (Fixup) | Meld the selected commit into the commit below it. Similar to fixup, but the selected commit's message will be discarded. |
|
||||
| `` f `` | 修復 (Fixup) | Meld the selected commit into the commit below it. Similar to squash, but the selected commit's message will be discarded. |
|
||||
| `` r `` | 改寫提交 | Reword the selected commit's message. |
|
||||
| `` R `` | 使用編輯器改寫提交 | |
|
||||
| `` d `` | 刪除提交 | Drop the selected commit. This will remove the commit from the branch via a rebase. If the commit makes changes that later commits depend on, you may need to resolve merge conflicts. |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/jesseduffield/lazygit
|
||||
|
||||
go 1.21
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/adrg/xdg v0.4.0
|
||||
@@ -11,6 +11,7 @@ require (
|
||||
github.com/gdamore/tcell/v2 v2.7.4
|
||||
github.com/go-errors/errors v1.5.1
|
||||
github.com/gookit/color v1.4.2
|
||||
github.com/iancoleman/orderedmap v0.3.0
|
||||
github.com/imdario/mergo v0.3.11
|
||||
github.com/integrii/flaggy v1.4.0
|
||||
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68
|
||||
@@ -37,6 +38,7 @@ require (
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
|
||||
golang.org/x/exp v0.0.0-20220318154914-8dddf5d87bd8
|
||||
golang.org/x/sync v0.7.0
|
||||
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@@ -171,6 +171,8 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc=
|
||||
github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
@@ -421,6 +423,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20170407050850-f3918c30c5c2/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -30,6 +32,7 @@ type cliArgs struct {
|
||||
PrintVersionInfo bool
|
||||
Debug bool
|
||||
TailLogs bool
|
||||
Profile bool
|
||||
PrintDefaultConfig bool
|
||||
PrintConfigDir bool
|
||||
UseConfigDir string
|
||||
@@ -145,6 +148,14 @@ func Start(buildInfo *BuildInfo, integrationTest integrationTypes.IntegrationTes
|
||||
return
|
||||
}
|
||||
|
||||
if cliArgs.Profile {
|
||||
go func() {
|
||||
if err := http.ListenAndServe("localhost:6060", nil); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
parsedGitArg := parseGitArg(cliArgs.GitArg)
|
||||
|
||||
Run(appConfig, common, appTypes.NewStartArgs(cliArgs.FilterPath, parsedGitArg, integrationTest))
|
||||
@@ -171,6 +182,9 @@ func parseCliArgsAndEnvVars() *cliArgs {
|
||||
tailLogs := false
|
||||
flaggy.Bool(&tailLogs, "l", "logs", "Tail lazygit logs (intended to be used when `lazygit --debug` is called in a separate terminal tab)")
|
||||
|
||||
profile := false
|
||||
flaggy.Bool(&profile, "", "profile", "Start the profiler and serve it on http port 6060. See CONTRIBUTING.md for more info.")
|
||||
|
||||
printDefaultConfig := false
|
||||
flaggy.Bool(&printDefaultConfig, "c", "config", "Print the default config")
|
||||
|
||||
@@ -202,6 +216,7 @@ func parseCliArgsAndEnvVars() *cliArgs {
|
||||
PrintVersionInfo: printVersionInfo,
|
||||
Debug: debug,
|
||||
TailLogs: tailLogs,
|
||||
Profile: profile,
|
||||
PrintDefaultConfig: printDefaultConfig,
|
||||
PrintConfigDir: printConfigDir,
|
||||
UseConfigDir: useConfigDir,
|
||||
|
||||
+1
-1
@@ -134,7 +134,7 @@ func NewGitCommandAux(
|
||||
worktreeCommands := git_commands.NewWorktreeCommands(gitCommon)
|
||||
blameCommands := git_commands.NewBlameCommands(gitCommon)
|
||||
|
||||
branchLoader := git_commands.NewBranchLoader(cmn, cmd, branchCommands.CurrentBranchInfo, configCommands)
|
||||
branchLoader := git_commands.NewBranchLoader(cmn, gitCommon, cmd, branchCommands.CurrentBranchInfo, configCommands)
|
||||
commitFileLoader := git_commands.NewCommitFileLoader(cmn, cmd)
|
||||
commitLoader := git_commands.NewCommitLoader(cmn, cmd, statusCommands.RebaseMode, gitCommon)
|
||||
reflogCommitLoader := git_commands.NewReflogCommitLoader(cmn, cmd)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/generics/set"
|
||||
"github.com/jesseduffield/go-git/v5/config"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// context:
|
||||
@@ -40,6 +42,7 @@ type BranchInfo struct {
|
||||
// BranchLoader returns a list of Branch objects for the current repo
|
||||
type BranchLoader struct {
|
||||
*common.Common
|
||||
*GitCommon
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
getCurrentBranchInfo func() (BranchInfo, error)
|
||||
config BranchLoaderConfigCommands
|
||||
@@ -47,12 +50,14 @@ type BranchLoader struct {
|
||||
|
||||
func NewBranchLoader(
|
||||
cmn *common.Common,
|
||||
gitCommon *GitCommon,
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
getCurrentBranchInfo func() (BranchInfo, error),
|
||||
config BranchLoaderConfigCommands,
|
||||
) *BranchLoader {
|
||||
return &BranchLoader{
|
||||
Common: cmn,
|
||||
GitCommon: gitCommon,
|
||||
cmd: cmd,
|
||||
getCurrentBranchInfo: getCurrentBranchInfo,
|
||||
config: config,
|
||||
@@ -60,8 +65,14 @@ func NewBranchLoader(
|
||||
}
|
||||
|
||||
// Load the list of branches for the current repo
|
||||
func (self *BranchLoader) Load(reflogCommits []*models.Commit) ([]*models.Branch, error) {
|
||||
branches := self.obtainBranches()
|
||||
func (self *BranchLoader) Load(reflogCommits []*models.Commit,
|
||||
mainBranches *MainBranches,
|
||||
oldBranches []*models.Branch,
|
||||
loadBehindCounts bool,
|
||||
onWorker func(func() error),
|
||||
renderFunc func(),
|
||||
) ([]*models.Branch, error) {
|
||||
branches := self.obtainBranches(self.version.IsAtLeast(2, 22, 0))
|
||||
|
||||
if self.AppState.LocalBranchSortOrder == "recency" {
|
||||
reflogBranches := self.obtainReflogBranches(reflogCommits)
|
||||
@@ -119,12 +130,109 @@ func (self *BranchLoader) Load(reflogCommits []*models.Commit) ([]*models.Branch
|
||||
branch.UpstreamRemote = match.Remote
|
||||
branch.UpstreamBranch = match.Merge.Short()
|
||||
}
|
||||
|
||||
// If the branch already existed, take over its BehindBaseBranch value
|
||||
// to reduce flicker
|
||||
if oldBranch, found := lo.Find(oldBranches, func(b *models.Branch) bool {
|
||||
return b.Name == branch.Name
|
||||
}); found {
|
||||
branch.BehindBaseBranch.Store(oldBranch.BehindBaseBranch.Load())
|
||||
}
|
||||
}
|
||||
|
||||
if loadBehindCounts && self.UserConfig.Gui.ShowDivergenceFromBaseBranch != "none" {
|
||||
onWorker(func() error {
|
||||
return self.GetBehindBaseBranchValuesForAllBranches(branches, mainBranches, renderFunc)
|
||||
})
|
||||
}
|
||||
|
||||
return branches, nil
|
||||
}
|
||||
|
||||
func (self *BranchLoader) obtainBranches() []*models.Branch {
|
||||
func (self *BranchLoader) GetBehindBaseBranchValuesForAllBranches(
|
||||
branches []*models.Branch,
|
||||
mainBranches *MainBranches,
|
||||
renderFunc func(),
|
||||
) error {
|
||||
mainBranchRefs := mainBranches.Get()
|
||||
if len(mainBranchRefs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
t := time.Now()
|
||||
errg := errgroup.Group{}
|
||||
|
||||
for _, branch := range branches {
|
||||
errg.Go(func() error {
|
||||
baseBranch, err := self.GetBaseBranch(branch, mainBranches)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
behind := 0 // prime it in case something below fails
|
||||
if baseBranch != "" {
|
||||
output, err := self.cmd.New(
|
||||
NewGitCmd("rev-list").
|
||||
Arg("--left-right").
|
||||
Arg("--count").
|
||||
Arg(fmt.Sprintf("%s...%s", branch.FullRefName(), baseBranch)).
|
||||
ToArgv(),
|
||||
).DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// The format of the output is "<ahead>\t<behind>"
|
||||
aheadBehindStr := strings.Split(strings.TrimSpace(output), "\t")
|
||||
if len(aheadBehindStr) == 2 {
|
||||
if value, err := strconv.Atoi(aheadBehindStr[1]); err == nil {
|
||||
behind = value
|
||||
}
|
||||
}
|
||||
}
|
||||
branch.BehindBaseBranch.Store(int32(behind))
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
err := errg.Wait()
|
||||
self.Log.Debugf("time to get behind base branch values for all branches: %s", time.Since(t))
|
||||
renderFunc()
|
||||
return err
|
||||
}
|
||||
|
||||
// Find the base branch for the given branch (i.e. the main branch that the
|
||||
// given branch was forked off of)
|
||||
//
|
||||
// Note that this function may return an empty string even if the returned error
|
||||
// is nil, e.g. when none of the configured main branches exist. This is not
|
||||
// considered an error condition, so callers need to check both the returned
|
||||
// error and whether the returned base branch is empty (and possibly react
|
||||
// differently in both cases).
|
||||
func (self *BranchLoader) GetBaseBranch(branch *models.Branch, mainBranches *MainBranches) (string, error) {
|
||||
mergeBase := mainBranches.GetMergeBase(branch.FullRefName())
|
||||
if mergeBase == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
output, err := self.cmd.New(
|
||||
NewGitCmd("for-each-ref").
|
||||
Arg("--contains").
|
||||
Arg(mergeBase).
|
||||
Arg("--format=%(refname)").
|
||||
Arg(mainBranches.Get()...).
|
||||
ToArgv(),
|
||||
).DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
trimmedOutput := strings.TrimSpace(output)
|
||||
split := strings.Split(trimmedOutput, "\n")
|
||||
if len(split) == 0 || split[0] == "" {
|
||||
return "", nil
|
||||
}
|
||||
return split[0], nil
|
||||
}
|
||||
|
||||
func (self *BranchLoader) obtainBranches(canUsePushTrack bool) []*models.Branch {
|
||||
output, err := self.getRawBranches()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -147,7 +255,7 @@ func (self *BranchLoader) obtainBranches() []*models.Branch {
|
||||
}
|
||||
|
||||
storeCommitDateAsRecency := self.AppState.LocalBranchSortOrder != "recency"
|
||||
return obtainBranch(split, storeCommitDateAsRecency), true
|
||||
return obtainBranch(split, storeCommitDateAsRecency, canUsePushTrack), true
|
||||
})
|
||||
}
|
||||
|
||||
@@ -183,23 +291,31 @@ var branchFields = []string{
|
||||
"refname:short",
|
||||
"upstream:short",
|
||||
"upstream:track",
|
||||
"push:track",
|
||||
"subject",
|
||||
"objectname",
|
||||
"committerdate:unix",
|
||||
}
|
||||
|
||||
// Obtain branch information from parsed line output of getRawBranches()
|
||||
func obtainBranch(split []string, storeCommitDateAsRecency bool) *models.Branch {
|
||||
func obtainBranch(split []string, storeCommitDateAsRecency bool, canUsePushTrack bool) *models.Branch {
|
||||
headMarker := split[0]
|
||||
fullName := split[1]
|
||||
upstreamName := split[2]
|
||||
track := split[3]
|
||||
subject := split[4]
|
||||
commitHash := split[5]
|
||||
commitDate := split[6]
|
||||
pushTrack := split[4]
|
||||
subject := split[5]
|
||||
commitHash := split[6]
|
||||
commitDate := split[7]
|
||||
|
||||
name := strings.TrimPrefix(fullName, "heads/")
|
||||
pushables, pullables, gone := parseUpstreamInfo(upstreamName, track)
|
||||
aheadForPull, behindForPull, gone := parseUpstreamInfo(upstreamName, track)
|
||||
var aheadForPush, behindForPush string
|
||||
if canUsePushTrack {
|
||||
aheadForPush, behindForPush, _ = parseUpstreamInfo(upstreamName, pushTrack)
|
||||
} else {
|
||||
aheadForPush, behindForPush = aheadForPull, behindForPull
|
||||
}
|
||||
|
||||
recency := ""
|
||||
if storeCommitDateAsRecency {
|
||||
@@ -209,14 +325,16 @@ func obtainBranch(split []string, storeCommitDateAsRecency bool) *models.Branch
|
||||
}
|
||||
|
||||
return &models.Branch{
|
||||
Name: name,
|
||||
Recency: recency,
|
||||
Pushables: pushables,
|
||||
Pullables: pullables,
|
||||
UpstreamGone: gone,
|
||||
Head: headMarker == "*",
|
||||
Subject: subject,
|
||||
CommitHash: commitHash,
|
||||
Name: name,
|
||||
Recency: recency,
|
||||
AheadForPull: aheadForPull,
|
||||
BehindForPull: behindForPull,
|
||||
AheadForPush: aheadForPush,
|
||||
BehindForPush: behindForPush,
|
||||
UpstreamGone: gone,
|
||||
Head: headMarker == "*",
|
||||
Subject: subject,
|
||||
CommitHash: commitHash,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,10 +350,10 @@ func parseUpstreamInfo(upstreamName string, track string) (string, string, bool)
|
||||
return "?", "?", true
|
||||
}
|
||||
|
||||
pushables := parseDifference(track, `ahead (\d+)`)
|
||||
pullables := parseDifference(track, `behind (\d+)`)
|
||||
ahead := parseDifference(track, `ahead (\d+)`)
|
||||
behind := parseDifference(track, `behind (\d+)`)
|
||||
|
||||
return pushables, pullables, false
|
||||
return ahead, behind, false
|
||||
}
|
||||
|
||||
func parseDifference(track string, regexStr string) string {
|
||||
|
||||
@@ -25,89 +25,101 @@ func TestObtainBranch(t *testing.T) {
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "TrimHeads",
|
||||
input: []string{"", "heads/a_branch", "", "", "subject", "123", timeStamp},
|
||||
input: []string{"", "heads/a_branch", "", "", "", "subject", "123", timeStamp},
|
||||
storeCommitDateAsRecency: false,
|
||||
expectedBranch: &models.Branch{
|
||||
Name: "a_branch",
|
||||
Pushables: "?",
|
||||
Pullables: "?",
|
||||
Head: false,
|
||||
Subject: "subject",
|
||||
CommitHash: "123",
|
||||
Name: "a_branch",
|
||||
AheadForPull: "?",
|
||||
BehindForPull: "?",
|
||||
AheadForPush: "?",
|
||||
BehindForPush: "?",
|
||||
Head: false,
|
||||
Subject: "subject",
|
||||
CommitHash: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "NoUpstream",
|
||||
input: []string{"", "a_branch", "", "", "subject", "123", timeStamp},
|
||||
input: []string{"", "a_branch", "", "", "", "subject", "123", timeStamp},
|
||||
storeCommitDateAsRecency: false,
|
||||
expectedBranch: &models.Branch{
|
||||
Name: "a_branch",
|
||||
Pushables: "?",
|
||||
Pullables: "?",
|
||||
Head: false,
|
||||
Subject: "subject",
|
||||
CommitHash: "123",
|
||||
Name: "a_branch",
|
||||
AheadForPull: "?",
|
||||
BehindForPull: "?",
|
||||
AheadForPush: "?",
|
||||
BehindForPush: "?",
|
||||
Head: false,
|
||||
Subject: "subject",
|
||||
CommitHash: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "IsHead",
|
||||
input: []string{"*", "a_branch", "", "", "subject", "123", timeStamp},
|
||||
input: []string{"*", "a_branch", "", "", "", "subject", "123", timeStamp},
|
||||
storeCommitDateAsRecency: false,
|
||||
expectedBranch: &models.Branch{
|
||||
Name: "a_branch",
|
||||
Pushables: "?",
|
||||
Pullables: "?",
|
||||
Head: true,
|
||||
Subject: "subject",
|
||||
CommitHash: "123",
|
||||
Name: "a_branch",
|
||||
AheadForPull: "?",
|
||||
BehindForPull: "?",
|
||||
AheadForPush: "?",
|
||||
BehindForPush: "?",
|
||||
Head: true,
|
||||
Subject: "subject",
|
||||
CommitHash: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "IsBehindAndAhead",
|
||||
input: []string{"", "a_branch", "a_remote/a_branch", "[behind 2, ahead 3]", "subject", "123", timeStamp},
|
||||
input: []string{"", "a_branch", "a_remote/a_branch", "[behind 2, ahead 3]", "[behind 2, ahead 3]", "subject", "123", timeStamp},
|
||||
storeCommitDateAsRecency: false,
|
||||
expectedBranch: &models.Branch{
|
||||
Name: "a_branch",
|
||||
Pushables: "3",
|
||||
Pullables: "2",
|
||||
Head: false,
|
||||
Subject: "subject",
|
||||
CommitHash: "123",
|
||||
Name: "a_branch",
|
||||
AheadForPull: "3",
|
||||
BehindForPull: "2",
|
||||
AheadForPush: "3",
|
||||
BehindForPush: "2",
|
||||
Head: false,
|
||||
Subject: "subject",
|
||||
CommitHash: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "RemoteBranchIsGone",
|
||||
input: []string{"", "a_branch", "a_remote/a_branch", "[gone]", "subject", "123", timeStamp},
|
||||
input: []string{"", "a_branch", "a_remote/a_branch", "[gone]", "[gone]", "subject", "123", timeStamp},
|
||||
storeCommitDateAsRecency: false,
|
||||
expectedBranch: &models.Branch{
|
||||
Name: "a_branch",
|
||||
UpstreamGone: true,
|
||||
Pushables: "?",
|
||||
Pullables: "?",
|
||||
Head: false,
|
||||
Subject: "subject",
|
||||
CommitHash: "123",
|
||||
Name: "a_branch",
|
||||
UpstreamGone: true,
|
||||
AheadForPull: "?",
|
||||
BehindForPull: "?",
|
||||
AheadForPush: "?",
|
||||
BehindForPush: "?",
|
||||
Head: false,
|
||||
Subject: "subject",
|
||||
CommitHash: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "WithCommitDateAsRecency",
|
||||
input: []string{"", "a_branch", "", "", "subject", "123", timeStamp},
|
||||
input: []string{"", "a_branch", "", "", "", "subject", "123", timeStamp},
|
||||
storeCommitDateAsRecency: true,
|
||||
expectedBranch: &models.Branch{
|
||||
Name: "a_branch",
|
||||
Recency: "2h",
|
||||
Pushables: "?",
|
||||
Pullables: "?",
|
||||
Head: false,
|
||||
Subject: "subject",
|
||||
CommitHash: "123",
|
||||
Name: "a_branch",
|
||||
Recency: "2h",
|
||||
AheadForPull: "?",
|
||||
BehindForPull: "?",
|
||||
AheadForPush: "?",
|
||||
BehindForPush: "?",
|
||||
Head: false,
|
||||
Subject: "subject",
|
||||
CommitHash: "123",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
branch := obtainBranch(s.input, s.storeCommitDateAsRecency)
|
||||
branch := obtainBranch(s.input, s.storeCommitDateAsRecency, true)
|
||||
assert.EqualValues(t, s.expectedBranch, branch)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ func TestBranchGetCommitDifferences(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildBranchCommands(commonDeps{runner: s.runner})
|
||||
pushables, pullables := instance.GetCommitDifferences("HEAD", "@{u}")
|
||||
@@ -89,7 +88,6 @@ func TestBranchDeleteBranch(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildBranchCommands(commonDeps{runner: s.runner})
|
||||
|
||||
@@ -150,7 +148,6 @@ func TestBranchMerge(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
runner := oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs(s.expected, "", nil)
|
||||
@@ -190,7 +187,6 @@ func TestBranchCheckout(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildBranchCommands(commonDeps{runner: s.runner})
|
||||
s.test(instance.Checkout("test", CheckoutOptions{Force: s.force}))
|
||||
@@ -279,7 +275,6 @@ func TestBranchCurrentBranchInfo(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildBranchCommands(commonDeps{runner: s.runner})
|
||||
s.test(instance.CurrentBranchInfo())
|
||||
|
||||
@@ -35,11 +35,6 @@ type CommitLoader struct {
|
||||
readFile func(filename string) ([]byte, error)
|
||||
walkFiles func(root string, fn filepath.WalkFunc) error
|
||||
dotGitDir string
|
||||
// List of main branches that exist in the repo.
|
||||
// We use these to obtain the merge base of the branch.
|
||||
// When nil, we're yet to obtain the list of existing main branches.
|
||||
// When an empty slice, we've obtained the list and it's empty.
|
||||
mainBranches []string
|
||||
*GitCommon
|
||||
}
|
||||
|
||||
@@ -56,7 +51,6 @@ func NewCommitLoader(
|
||||
getRebaseMode: getRebaseMode,
|
||||
readFile: os.ReadFile,
|
||||
walkFiles: filepath.Walk,
|
||||
mainBranches: nil,
|
||||
GitCommon: gitCommon,
|
||||
}
|
||||
}
|
||||
@@ -72,6 +66,7 @@ type GetCommitsOptions struct {
|
||||
All bool
|
||||
// If non-empty, show divergence from this ref (left-right log)
|
||||
RefToShowDivergenceFrom string
|
||||
MainBranches *MainBranches
|
||||
}
|
||||
|
||||
// GetCommits obtains the commits of the current branch
|
||||
@@ -108,9 +103,9 @@ func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit,
|
||||
go utils.Safe(func() {
|
||||
defer wg.Done()
|
||||
|
||||
ancestor = self.getMergeBase(opts.RefName)
|
||||
ancestor = opts.MainBranches.GetMergeBase(opts.RefName)
|
||||
if opts.RefToShowDivergenceFrom != "" {
|
||||
remoteAncestor = self.getMergeBase(opts.RefToShowDivergenceFrom)
|
||||
remoteAncestor = opts.MainBranches.GetMergeBase(opts.RefToShowDivergenceFrom)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -211,11 +206,11 @@ func (self *CommitLoader) extractCommitFromLine(line string, showDivergence bool
|
||||
authorEmail := split[3]
|
||||
extraInfo := strings.TrimSpace(split[4])
|
||||
parentHashes := split[5]
|
||||
message := split[6]
|
||||
divergence := models.DivergenceNone
|
||||
if showDivergence {
|
||||
divergence = lo.Ternary(split[7] == "<", models.DivergenceLeft, models.DivergenceRight)
|
||||
divergence = lo.Ternary(split[6] == "<", models.DivergenceLeft, models.DivergenceRight)
|
||||
}
|
||||
message := split[7]
|
||||
|
||||
tags := []string{}
|
||||
|
||||
@@ -349,6 +344,8 @@ func (self *CommitLoader) getRebasingCommits(rebaseMode enums.RebaseMode) []*mod
|
||||
for _, t := range todos {
|
||||
if t.Command == todo.UpdateRef {
|
||||
t.Msg = t.Ref
|
||||
} else if t.Command == todo.Exec {
|
||||
t.Msg = t.ExecCommand
|
||||
} else if t.Commit == "" {
|
||||
// Command does not have a commit associated, skip
|
||||
continue
|
||||
@@ -471,84 +468,6 @@ func setCommitMergedStatuses(ancestor string, commits []*models.Commit) {
|
||||
}
|
||||
}
|
||||
|
||||
func (self *CommitLoader) getMergeBase(refName string) string {
|
||||
if self.mainBranches == nil {
|
||||
self.mainBranches = self.getExistingMainBranches()
|
||||
}
|
||||
|
||||
if len(self.mainBranches) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// We pass all configured main branches to the merge-base call; git will
|
||||
// return the base commit for the closest one.
|
||||
|
||||
output, err := self.cmd.New(
|
||||
NewGitCmd("merge-base").Arg(refName).Arg(self.mainBranches...).
|
||||
ToArgv(),
|
||||
).DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
// If there's an error, it must be because one of the main branches that
|
||||
// used to exist when we called getExistingMainBranches() was deleted
|
||||
// meanwhile. To fix this for next time, throw away our cache.
|
||||
self.mainBranches = nil
|
||||
}
|
||||
return ignoringWarnings(output)
|
||||
}
|
||||
|
||||
func (self *CommitLoader) getExistingMainBranches() []string {
|
||||
var existingBranches []string
|
||||
var wg sync.WaitGroup
|
||||
|
||||
mainBranches := self.UserConfig.Git.MainBranches
|
||||
existingBranches = make([]string, len(mainBranches))
|
||||
|
||||
for i, branchName := range mainBranches {
|
||||
wg.Add(1)
|
||||
i := i
|
||||
branchName := branchName
|
||||
go utils.Safe(func() {
|
||||
defer wg.Done()
|
||||
|
||||
// Try to determine upstream of local main branch
|
||||
if ref, err := self.cmd.New(
|
||||
NewGitCmd("rev-parse").Arg("--symbolic-full-name", branchName+"@{u}").ToArgv(),
|
||||
).DontLog().RunWithOutput(); err == nil {
|
||||
existingBranches[i] = strings.TrimSpace(ref)
|
||||
return
|
||||
}
|
||||
|
||||
// If this failed, a local branch for this main branch doesn't exist or it
|
||||
// has no upstream configured. Try looking for one in the "origin" remote.
|
||||
ref := "refs/remotes/origin/" + branchName
|
||||
if err := self.cmd.New(
|
||||
NewGitCmd("rev-parse").Arg("--verify", "--quiet", ref).ToArgv(),
|
||||
).DontLog().Run(); err == nil {
|
||||
existingBranches[i] = ref
|
||||
return
|
||||
}
|
||||
|
||||
// If this failed as well, try if we have the main branch as a local
|
||||
// branch. This covers the case where somebody is using git locally
|
||||
// for something, but never pushing anywhere.
|
||||
ref = "refs/heads/" + branchName
|
||||
if err := self.cmd.New(
|
||||
NewGitCmd("rev-parse").Arg("--verify", "--quiet", ref).ToArgv(),
|
||||
).DontLog().Run(); err == nil {
|
||||
existingBranches[i] = ref
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
existingBranches = lo.Filter(existingBranches, func(branch string, _ int) bool {
|
||||
return branch != ""
|
||||
})
|
||||
|
||||
return existingBranches
|
||||
}
|
||||
|
||||
func ignoringWarnings(commandOutput string) string {
|
||||
trimmedOutput := strings.TrimSpace(commandOutput)
|
||||
split := strings.Split(trimmedOutput, "\n")
|
||||
@@ -605,4 +524,4 @@ func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj {
|
||||
return self.cmd.New(cmdArgs).DontLog()
|
||||
}
|
||||
|
||||
const prettyFormat = `--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m`
|
||||
const prettyFormat = `--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s`
|
||||
|
||||
@@ -15,16 +15,16 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var commitsOutput = strings.Replace(`0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|jessedduffield@gmail.com|HEAD -> better-tests|b21997d6b4cbdf84b149|better typing for rebase mode
|
||||
b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164|1640824515|Jesse Duffield|jessedduffield@gmail.com|origin/better-tests|e94e8fc5b6fab4cb755f|fix logging
|
||||
e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c|1640823749|Jesse Duffield|jessedduffield@gmail.com|tag: 123, tag: 456|d8084cd558925eb7c9c3|refactor
|
||||
d8084cd558925eb7c9c38afeed5725c21653ab90|1640821426|Jesse Duffield|jessedduffield@gmail.com||65f910ebd85283b5cce9|WIP
|
||||
65f910ebd85283b5cce9bf67d03d3f1a9ea3813a|1640821275|Jesse Duffield|jessedduffield@gmail.com||26c07b1ab33860a1a759|WIP
|
||||
26c07b1ab33860a1a7591a0638f9925ccf497ffa|1640750752|Jesse Duffield|jessedduffield@gmail.com||3d4470a6c072208722e5|WIP
|
||||
3d4470a6c072208722e5ae9a54bcb9634959a1c5|1640748818|Jesse Duffield|jessedduffield@gmail.com||053a66a7be3da43aacdc|WIP
|
||||
053a66a7be3da43aacdc7aa78e1fe757b82c4dd2|1640739815|Jesse Duffield|jessedduffield@gmail.com||985fe482e806b172aea4|refactoring the config struct`, "|", "\x00", -1)
|
||||
var commitsOutput = strings.Replace(`0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|jessedduffield@gmail.com|HEAD -> better-tests|b21997d6b4cbdf84b149|>|better typing for rebase mode
|
||||
b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164|1640824515|Jesse Duffield|jessedduffield@gmail.com|origin/better-tests|e94e8fc5b6fab4cb755f|>|fix logging
|
||||
e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c|1640823749|Jesse Duffield|jessedduffield@gmail.com|tag: 123, tag: 456|d8084cd558925eb7c9c3|>|refactor
|
||||
d8084cd558925eb7c9c38afeed5725c21653ab90|1640821426|Jesse Duffield|jessedduffield@gmail.com||65f910ebd85283b5cce9|>|WIP
|
||||
65f910ebd85283b5cce9bf67d03d3f1a9ea3813a|1640821275|Jesse Duffield|jessedduffield@gmail.com||26c07b1ab33860a1a759|>|WIP
|
||||
26c07b1ab33860a1a7591a0638f9925ccf497ffa|1640750752|Jesse Duffield|jessedduffield@gmail.com||3d4470a6c072208722e5|>|WIP
|
||||
3d4470a6c072208722e5ae9a54bcb9634959a1c5|1640748818|Jesse Duffield|jessedduffield@gmail.com||053a66a7be3da43aacdc|>|WIP
|
||||
053a66a7be3da43aacdc7aa78e1fe757b82c4dd2|1640739815|Jesse Duffield|jessedduffield@gmail.com||985fe482e806b172aea4|>|refactoring the config struct`, "|", "\x00", -1)
|
||||
|
||||
var singleCommitOutput = strings.Replace(`0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|jessedduffield@gmail.com|HEAD -> better-tests|b21997d6b4cbdf84b149|better typing for rebase mode`, "|", "\x00", -1)
|
||||
var singleCommitOutput = strings.Replace(`0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|jessedduffield@gmail.com|HEAD -> better-tests|b21997d6b4cbdf84b149|>|better typing for rebase mode`, "|", "\x00", -1)
|
||||
|
||||
func TestGetCommits(t *testing.T) {
|
||||
type scenario struct {
|
||||
@@ -46,7 +46,7 @@ func TestGetCommits(t *testing.T) {
|
||||
opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", IncludeRebaseCommits: false},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
|
||||
|
||||
expectedCommits: []*models.Commit{},
|
||||
expectedError: nil,
|
||||
@@ -58,7 +58,7 @@ func TestGetCommits(t *testing.T) {
|
||||
opts: GetCommitsOptions{RefName: "refs/heads/mybranch", RefForPushedStatus: "refs/heads/mybranch", IncludeRebaseCommits: false},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"merge-base", "refs/heads/mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
ExpectGitArgs([]string{"log", "refs/heads/mybranch", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
|
||||
ExpectGitArgs([]string{"log", "refs/heads/mybranch", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
|
||||
|
||||
expectedCommits: []*models.Commit{},
|
||||
expectedError: nil,
|
||||
@@ -73,7 +73,7 @@ func TestGetCommits(t *testing.T) {
|
||||
// here it's seeing which commits are yet to be pushed
|
||||
ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
// here it's actually getting all the commits in a formatted form, one per line
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m", "--abbrev=40", "--no-show-signature", "--"}, commitsOutput, nil).
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, commitsOutput, nil).
|
||||
// here it's testing which of the configured main branches have an upstream
|
||||
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "master@{u}"}, "refs/remotes/origin/master", nil). // this one does
|
||||
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "main@{u}"}, "", errors.New("error")). // this one doesn't, so it checks origin instead
|
||||
@@ -210,7 +210,7 @@ func TestGetCommits(t *testing.T) {
|
||||
// here it's seeing which commits are yet to be pushed
|
||||
ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
// here it's actually getting all the commits in a formatted form, one per line
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil).
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil).
|
||||
// here it's testing which of the configured main branches exist; neither does
|
||||
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "master@{u}"}, "", errors.New("error")).
|
||||
ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/remotes/origin/master"}, "", errors.New("error")).
|
||||
@@ -247,7 +247,7 @@ func TestGetCommits(t *testing.T) {
|
||||
// here it's seeing which commits are yet to be pushed
|
||||
ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
// here it's actually getting all the commits in a formatted form, one per line
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil).
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil).
|
||||
// here it's testing which of the configured main branches exist
|
||||
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "master@{u}"}, "refs/remotes/origin/master", nil).
|
||||
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "main@{u}"}, "", errors.New("error")).
|
||||
@@ -283,7 +283,7 @@ func TestGetCommits(t *testing.T) {
|
||||
opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", IncludeRebaseCommits: false},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
|
||||
|
||||
expectedCommits: []*models.Commit{},
|
||||
expectedError: nil,
|
||||
@@ -295,7 +295,7 @@ func TestGetCommits(t *testing.T) {
|
||||
opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", FilterPath: "src"},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m", "--abbrev=40", "--follow", "--no-show-signature", "--", "src"}, "", nil),
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--follow", "--no-show-signature", "--", "src"}, "", nil),
|
||||
|
||||
expectedCommits: []*models.Commit{},
|
||||
expectedError: nil,
|
||||
@@ -303,15 +303,15 @@ func TestGetCommits(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
scenario := scenario
|
||||
t.Run(scenario.testName, func(t *testing.T) {
|
||||
common := utils.NewDummyCommon()
|
||||
common.AppState = &config.AppState{}
|
||||
common.AppState.GitLogOrder = scenario.logOrder
|
||||
cmd := oscommands.NewDummyCmdObjBuilder(scenario.runner)
|
||||
|
||||
builder := &CommitLoader{
|
||||
Common: common,
|
||||
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
|
||||
cmd: cmd,
|
||||
getRebaseMode: func() (enums.RebaseMode, error) { return scenario.rebaseMode, nil },
|
||||
dotGitDir: ".git",
|
||||
readFile: func(filename string) ([]byte, error) {
|
||||
@@ -323,7 +323,9 @@ func TestGetCommits(t *testing.T) {
|
||||
}
|
||||
|
||||
common.UserConfig.Git.MainBranches = scenario.mainBranches
|
||||
commits, err := builder.GetCommits(scenario.opts)
|
||||
opts := scenario.opts
|
||||
opts.MainBranches = NewMainBranches(scenario.mainBranches, cmd)
|
||||
commits, err := builder.GetCommits(opts)
|
||||
|
||||
assert.Equal(t, scenario.expectedCommits, commits)
|
||||
assert.Equal(t, scenario.expectedError, err)
|
||||
|
||||
@@ -30,7 +30,6 @@ func TestCommitRewordCommit(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildCommitCommands(commonDeps{runner: s.runner})
|
||||
|
||||
@@ -100,7 +99,6 @@ func TestCommitCommitCmdObj(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
userConfig := config.GetDefaultConfig()
|
||||
userConfig.Git.Commit.SignOff = s.configSignoff
|
||||
@@ -136,7 +134,6 @@ func TestCommitCommitEditorCmdObj(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
userConfig := config.GetDefaultConfig()
|
||||
userConfig.Git.Commit.SignOff = s.configSignoff
|
||||
@@ -171,7 +168,6 @@ func TestCommitCreateFixupCommit(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildCommitCommands(commonDeps{runner: s.runner})
|
||||
s.test(instance.CreateFixupCommit(s.hash))
|
||||
@@ -221,7 +217,6 @@ func TestCommitCreateAmendCommit(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildCommitCommands(commonDeps{runner: s.runner})
|
||||
err := instance.CreateAmendCommit(s.originalSubject, s.newSubject, s.newDescription, s.includeFileChanges)
|
||||
@@ -285,7 +280,6 @@ func TestCommitShowCmdObj(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
userConfig := config.GetDefaultConfig()
|
||||
userConfig.Git.Paging.ExternalDiffCommand = s.extDiffCmd
|
||||
@@ -334,7 +328,6 @@ func TestGetCommitMsg(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildCommitCommands(commonDeps{
|
||||
runner: oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"-c", "log.showsignature=false", "log", "--format=%B", "--max-count=1", "deadbeef"}, s.input, nil),
|
||||
@@ -374,7 +367,6 @@ func TestGetCommitMessageFromHistory(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildCommitCommands(commonDeps{runner: s.runner})
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package git_commands
|
||||
|
||||
import "github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
)
|
||||
|
||||
type DiffCommands struct {
|
||||
*GitCommon
|
||||
@@ -13,10 +17,16 @@ func NewDiffCommands(gitCommon *GitCommon) *DiffCommands {
|
||||
}
|
||||
|
||||
func (self *DiffCommands) DiffCmdObj(diffArgs []string) oscommands.ICmdObj {
|
||||
extDiffCmd := self.UserConfig.Git.Paging.ExternalDiffCommand
|
||||
useExtDiff := extDiffCmd != ""
|
||||
|
||||
return self.cmd.New(
|
||||
NewGitCmd("diff").
|
||||
Config("diff.noprefix=false").
|
||||
Arg("--submodule", "--no-ext-diff", "--color").
|
||||
ConfigIf(useExtDiff, "diff.external="+extDiffCmd).
|
||||
ArgIfElse(useExtDiff, "--ext-diff", "--no-ext-diff").
|
||||
Arg("--submodule").
|
||||
Arg(fmt.Sprintf("--color=%s", self.UserConfig.Git.Paging.ColorArg)).
|
||||
Arg(diffArgs...).
|
||||
Dir(self.repoPaths.worktreePath).
|
||||
ToArgv(),
|
||||
|
||||
@@ -172,7 +172,6 @@ func TestFileGetStatusFiles(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
cmd := oscommands.NewDummyCmdObjBuilder(s.runner)
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ func TestStartCmdObj(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildFlowCommands(commonDeps{})
|
||||
|
||||
@@ -69,7 +68,6 @@ func TestFinishCmdObj(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildFlowCommands(commonDeps{
|
||||
gitConfig: git_config.NewFakeGitConfig(s.gitConfigMockResponses),
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
)
|
||||
|
||||
type MainBranches struct {
|
||||
// List of main branches configured by the user. Just the bare names.
|
||||
configuredMainBranches []string
|
||||
// Which of these actually exist in the repository. Full ref names, and it
|
||||
// could be either "refs/heads/..." or "refs/remotes/origin/..." depending
|
||||
// on which one exists for a given bare name.
|
||||
existingMainBranches []string
|
||||
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
mutex *deadlock.Mutex
|
||||
}
|
||||
|
||||
func NewMainBranches(
|
||||
configuredMainBranches []string,
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
) *MainBranches {
|
||||
return &MainBranches{
|
||||
configuredMainBranches: configuredMainBranches,
|
||||
existingMainBranches: nil,
|
||||
cmd: cmd,
|
||||
mutex: &deadlock.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
// Get the list of main branches that exist in the repository. This is a list of
|
||||
// full ref names.
|
||||
func (self *MainBranches) Get() []string {
|
||||
self.mutex.Lock()
|
||||
defer self.mutex.Unlock()
|
||||
|
||||
if self.existingMainBranches == nil {
|
||||
self.existingMainBranches = self.determineMainBranches()
|
||||
}
|
||||
|
||||
return self.existingMainBranches
|
||||
}
|
||||
|
||||
// Return the merge base of the given refName with the closest main branch.
|
||||
func (self *MainBranches) GetMergeBase(refName string) string {
|
||||
mainBranches := self.Get()
|
||||
if len(mainBranches) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// We pass all existing main branches to the merge-base call; git will
|
||||
// return the base commit for the closest one.
|
||||
|
||||
// We ignore errors from this call, since we can't distinguish whether the
|
||||
// error is because one of the main branches has been deleted since the last
|
||||
// call to determineMainBranches, or because the refName has no common
|
||||
// history with any of the main branches. Since the former should happen
|
||||
// very rarely, users must quit and restart lazygit to fix it; the latter is
|
||||
// also not very common, but can totally happen and is not an error.
|
||||
|
||||
output, _ := self.cmd.New(
|
||||
NewGitCmd("merge-base").Arg(refName).Arg(mainBranches...).
|
||||
ToArgv(),
|
||||
).DontLog().RunWithOutput()
|
||||
return ignoringWarnings(output)
|
||||
}
|
||||
|
||||
func (self *MainBranches) determineMainBranches() []string {
|
||||
var existingBranches []string
|
||||
var wg sync.WaitGroup
|
||||
|
||||
existingBranches = make([]string, len(self.configuredMainBranches))
|
||||
|
||||
for i, branchName := range self.configuredMainBranches {
|
||||
wg.Add(1)
|
||||
go utils.Safe(func() {
|
||||
defer wg.Done()
|
||||
|
||||
// Try to determine upstream of local main branch
|
||||
if ref, err := self.cmd.New(
|
||||
NewGitCmd("rev-parse").Arg("--symbolic-full-name", branchName+"@{u}").ToArgv(),
|
||||
).DontLog().RunWithOutput(); err == nil {
|
||||
existingBranches[i] = strings.TrimSpace(ref)
|
||||
return
|
||||
}
|
||||
|
||||
// If this failed, a local branch for this main branch doesn't exist or it
|
||||
// has no upstream configured. Try looking for one in the "origin" remote.
|
||||
ref := "refs/remotes/origin/" + branchName
|
||||
if err := self.cmd.New(
|
||||
NewGitCmd("rev-parse").Arg("--verify", "--quiet", ref).ToArgv(),
|
||||
).DontLog().Run(); err == nil {
|
||||
existingBranches[i] = ref
|
||||
return
|
||||
}
|
||||
|
||||
// If this failed as well, try if we have the main branch as a local
|
||||
// branch. This covers the case where somebody is using git locally
|
||||
// for something, but never pushing anywhere.
|
||||
ref = "refs/heads/" + branchName
|
||||
if err := self.cmd.New(
|
||||
NewGitCmd("rev-parse").Arg("--verify", "--quiet", ref).ToArgv(),
|
||||
).DontLog().Run(); err == nil {
|
||||
existingBranches[i] = ref
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
existingBranches = lo.Filter(existingBranches, func(branch string, _ int) bool {
|
||||
return branch != ""
|
||||
})
|
||||
|
||||
return existingBranches
|
||||
}
|
||||
@@ -67,42 +67,47 @@ func (self *RebaseCommands) RewordCommitInEditor(commits []*models.Commit, index
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) ResetCommitAuthor(commits []*models.Commit, index int) error {
|
||||
return self.GenericAmend(commits, index, func() error {
|
||||
func (self *RebaseCommands) ResetCommitAuthor(commits []*models.Commit, start, end int) error {
|
||||
return self.GenericAmend(commits, start, end, func(_ *models.Commit) error {
|
||||
return self.commit.ResetAuthor()
|
||||
})
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) SetCommitAuthor(commits []*models.Commit, index int, value string) error {
|
||||
return self.GenericAmend(commits, index, func() error {
|
||||
func (self *RebaseCommands) SetCommitAuthor(commits []*models.Commit, start, end int, value string) error {
|
||||
return self.GenericAmend(commits, start, end, func(_ *models.Commit) error {
|
||||
return self.commit.SetAuthor(value)
|
||||
})
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) AddCommitCoAuthor(commits []*models.Commit, index int, value string) error {
|
||||
return self.GenericAmend(commits, index, func() error {
|
||||
return self.commit.AddCoAuthor(commits[index].Hash, value)
|
||||
func (self *RebaseCommands) AddCommitCoAuthor(commits []*models.Commit, start, end int, value string) error {
|
||||
return self.GenericAmend(commits, start, end, func(commit *models.Commit) error {
|
||||
return self.commit.AddCoAuthor(commit.Hash, value)
|
||||
})
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) GenericAmend(commits []*models.Commit, index int, f func() error) error {
|
||||
if models.IsHeadCommit(commits, index) {
|
||||
func (self *RebaseCommands) GenericAmend(commits []*models.Commit, start, end int, f func(commit *models.Commit) error) error {
|
||||
if start == end && models.IsHeadCommit(commits, start) {
|
||||
// we've selected the top commit so no rebase is required
|
||||
return f()
|
||||
return f(commits[start])
|
||||
}
|
||||
|
||||
err := self.BeginInteractiveRebaseForCommit(commits, index, false)
|
||||
err := self.BeginInteractiveRebaseForCommitRange(commits, start, end, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// now the selected commit should be our head so we'll amend it
|
||||
err = f()
|
||||
if err != nil {
|
||||
return err
|
||||
for commitIndex := end; commitIndex >= start; commitIndex-- {
|
||||
err = f(commits[commitIndex])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := self.ContinueRebase(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return self.ContinueRebase()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) MoveCommitsDown(commits []*models.Commit, startIdx int, endIdx int) error {
|
||||
@@ -381,7 +386,13 @@ func (self *RebaseCommands) SquashAllAboveFixupCommits(commit *models.Commit) er
|
||||
func (self *RebaseCommands) BeginInteractiveRebaseForCommit(
|
||||
commits []*models.Commit, commitIndex int, keepCommitsThatBecomeEmpty bool,
|
||||
) error {
|
||||
if len(commits)-1 < commitIndex {
|
||||
return self.BeginInteractiveRebaseForCommitRange(commits, commitIndex, commitIndex, keepCommitsThatBecomeEmpty)
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) BeginInteractiveRebaseForCommitRange(
|
||||
commits []*models.Commit, start, end int, keepCommitsThatBecomeEmpty bool,
|
||||
) error {
|
||||
if len(commits)-1 < end {
|
||||
return errors.New("index outside of range of commits")
|
||||
}
|
||||
|
||||
@@ -392,14 +403,17 @@ func (self *RebaseCommands) BeginInteractiveRebaseForCommit(
|
||||
return errors.New(self.Tr.DisabledForGPG)
|
||||
}
|
||||
|
||||
changes := []daemon.ChangeTodoAction{{
|
||||
Hash: commits[commitIndex].Hash,
|
||||
NewAction: todo.Edit,
|
||||
}}
|
||||
changes := make([]daemon.ChangeTodoAction, 0, end-start)
|
||||
for commitIndex := end; commitIndex >= start; commitIndex-- {
|
||||
changes = append(changes, daemon.ChangeTodoAction{
|
||||
Hash: commits[commitIndex].Hash,
|
||||
NewAction: todo.Edit,
|
||||
})
|
||||
}
|
||||
self.os.LogCommand(logTodoChanges(changes), false)
|
||||
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseHashOrRoot: getBaseHashOrRoot(commits, commitIndex+1),
|
||||
baseHashOrRoot: getBaseHashOrRoot(commits, end+1),
|
||||
overrideEditor: true,
|
||||
keepCommitsThatBecomeEmpty: keepCommitsThatBecomeEmpty,
|
||||
instruction: daemon.NewChangeTodoActionsInstruction(changes),
|
||||
|
||||
@@ -67,7 +67,6 @@ func TestRebaseRebaseBranch(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildRebaseCommands(commonDeps{runner: s.runner, gitVersion: s.gitVersion})
|
||||
s.test(instance.RebaseBranch(s.arg))
|
||||
@@ -89,7 +88,6 @@ func TestRebaseSkipEditorCommand(t *testing.T) {
|
||||
`^GIT_SEQUENCE_EDITOR=.*$`,
|
||||
"^" + daemon.DaemonKindEnvKey + "=" + strconv.Itoa(int(daemon.DaemonKindExitImmediately)) + "$",
|
||||
} {
|
||||
regexStr := regexStr
|
||||
foundMatch := lo.ContainsBy(envVars, func(envVar string) bool {
|
||||
return regexp.MustCompile(regexStr).MatchString(envVar)
|
||||
})
|
||||
@@ -163,7 +161,6 @@ func TestRebaseDiscardOldFileChanges(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildRebaseCommands(commonDeps{
|
||||
runner: s.runner,
|
||||
|
||||
@@ -176,7 +176,6 @@ func TestGetReflogCommits(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
scenario := scenario
|
||||
t.Run(scenario.testName, func(t *testing.T) {
|
||||
builder := &ReflogCommitLoader{
|
||||
Common: utils.NewDummyCommon(),
|
||||
|
||||
@@ -101,7 +101,6 @@ func TestGetRepoPaths(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.Name, func(t *testing.T) {
|
||||
runner := oscommands.NewFakeRunner(t)
|
||||
cmd := oscommands.NewDummyCmdObjBuilder(runner)
|
||||
|
||||
@@ -121,9 +121,22 @@ func (self *StashCommands) StashUnstagedChanges(message string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveStagedChanges stashes only the currently staged changes. This takes a few steps
|
||||
// shoutouts to Joe on https://stackoverflow.com/questions/14759748/stashing-only-staged-changes-in-git-is-it-possible
|
||||
// SaveStagedChanges stashes only the currently staged changes.
|
||||
func (self *StashCommands) SaveStagedChanges(message string) error {
|
||||
if self.version.IsAtLeast(2, 35, 0) {
|
||||
return self.cmd.New(NewGitCmd("stash").Arg("push").Arg("--staged").Arg("-m", message).ToArgv()).Run()
|
||||
}
|
||||
|
||||
// Git versions older than 2.35.0 don't support the --staged flag, so we
|
||||
// need to fall back to a more complex solution.
|
||||
// Shoutouts to Joe on https://stackoverflow.com/questions/14759748/stashing-only-staged-changes-in-git-is-it-possible
|
||||
//
|
||||
// Note that this method has a few bugs:
|
||||
// - it fails when there are *only* staged changes
|
||||
// - it fails when staged and unstaged changes within a single file are too close together
|
||||
// We don't bother fixing these, because users can simply update git when
|
||||
// they are affected by these issues.
|
||||
|
||||
// wrap in 'writing', which uses a mutex
|
||||
if err := self.cmd.New(
|
||||
NewGitCmd("stash").Arg("--keep-index").ToArgv(),
|
||||
|
||||
@@ -47,7 +47,6 @@ func TestGetStashEntries(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
cmd := oscommands.NewDummyCmdObjBuilder(s.runner)
|
||||
|
||||
|
||||
@@ -74,7 +74,6 @@ func TestStashStore(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
runner := oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs(s.expected, "", nil)
|
||||
@@ -131,7 +130,6 @@ func TestStashStashEntryCmdObj(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
userConfig := config.GetDefaultConfig()
|
||||
appState := &config.AppState{}
|
||||
@@ -181,7 +179,6 @@ func TestStashRename(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
runner := oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs(s.expectedHashCmd, s.hashResult, nil).
|
||||
|
||||
@@ -19,6 +19,7 @@ func NewSyncCommands(gitCommon *GitCommon) *SyncCommands {
|
||||
// Push pushes to a branch
|
||||
type PushOpts struct {
|
||||
Force bool
|
||||
ForceWithLease bool
|
||||
UpstreamRemote string
|
||||
UpstreamBranch string
|
||||
SetUpstream bool
|
||||
@@ -30,10 +31,11 @@ func (self *SyncCommands) PushCmdObj(task gocui.Task, opts PushOpts) (oscommands
|
||||
}
|
||||
|
||||
cmdArgs := NewGitCmd("push").
|
||||
ArgIf(opts.Force, "--force-with-lease").
|
||||
ArgIf(opts.Force, "--force").
|
||||
ArgIf(opts.ForceWithLease, "--force-with-lease").
|
||||
ArgIf(opts.SetUpstream, "--set-upstream").
|
||||
ArgIf(opts.UpstreamRemote != "", opts.UpstreamRemote).
|
||||
ArgIf(opts.UpstreamBranch != "", opts.UpstreamBranch).
|
||||
ArgIf(opts.UpstreamBranch != "", "HEAD:"+opts.UpstreamBranch).
|
||||
ToArgv()
|
||||
|
||||
cmdObj := self.cmd.New(cmdArgs).PromptOnCredentialRequest(task)
|
||||
|
||||
@@ -18,62 +18,70 @@ func TestSyncPush(t *testing.T) {
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Push with force disabled",
|
||||
opts: PushOpts{Force: false},
|
||||
opts: PushOpts{ForceWithLease: false},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "push"})
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Push with force-with-lease enabled",
|
||||
opts: PushOpts{ForceWithLease: true},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "--force-with-lease"})
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Push with force enabled",
|
||||
opts: PushOpts{Force: true},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "--force-with-lease"})
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "--force"})
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Push with force disabled, upstream supplied",
|
||||
opts: PushOpts{
|
||||
Force: false,
|
||||
ForceWithLease: false,
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "master",
|
||||
},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "origin", "master"})
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "origin", "HEAD:master"})
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Push with force disabled, setting upstream",
|
||||
opts: PushOpts{
|
||||
Force: false,
|
||||
ForceWithLease: false,
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "master",
|
||||
SetUpstream: true,
|
||||
},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "--set-upstream", "origin", "master"})
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "--set-upstream", "origin", "HEAD:master"})
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Push with force enabled, setting upstream",
|
||||
testName: "Push with force-with-lease enabled, setting upstream",
|
||||
opts: PushOpts{
|
||||
Force: true,
|
||||
ForceWithLease: true,
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "master",
|
||||
SetUpstream: true,
|
||||
},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "--force-with-lease", "--set-upstream", "origin", "master"})
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "--force-with-lease", "--set-upstream", "origin", "HEAD:master"})
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Push with remote branch but no origin",
|
||||
opts: PushOpts{
|
||||
Force: true,
|
||||
ForceWithLease: true,
|
||||
UpstreamRemote: "",
|
||||
UpstreamBranch: "master",
|
||||
SetUpstream: true,
|
||||
@@ -86,7 +94,6 @@ func TestSyncPush(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildSyncCommands(commonDeps{})
|
||||
task := gocui.NewFakeTask()
|
||||
@@ -124,7 +131,6 @@ func TestSyncFetch(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildSyncCommands(commonDeps{})
|
||||
instance.UserConfig.Git.FetchAll = s.fetchAllConfig
|
||||
@@ -163,7 +169,6 @@ func TestSyncFetchBackground(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildSyncCommands(commonDeps{})
|
||||
instance.UserConfig.Git.FetchAll = s.fetchAllConfig
|
||||
|
||||
@@ -44,7 +44,6 @@ func TestGetTags(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
scenario := scenario
|
||||
t.Run(scenario.testName, func(t *testing.T) {
|
||||
loader := &TagLoader{
|
||||
Common: utils.NewDummyCommon(),
|
||||
|
||||
@@ -363,7 +363,7 @@ func (self *WorkingTreeCommands) ResetAndClean() error {
|
||||
return self.RemoveUntrackedFiles()
|
||||
}
|
||||
|
||||
// ResetHardHead runs `git reset --hard`
|
||||
// ResetHard runs `git reset --hard`
|
||||
func (self *WorkingTreeCommands) ResetHard(ref string) error {
|
||||
cmdArgs := NewGitCmd("reset").Arg("--hard", ref).
|
||||
ToArgv()
|
||||
|
||||
@@ -61,7 +61,6 @@ func TestWorkingTreeUnstageFile(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner})
|
||||
s.test(instance.UnStageFile([]string{"test.txt"}, s.reset))
|
||||
@@ -190,7 +189,6 @@ func TestWorkingTreeDiscardAllFileChanges(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner, removeFile: s.removeFile})
|
||||
err := instance.DiscardAllFileChanges(s.file)
|
||||
@@ -306,7 +304,6 @@ func TestWorkingTreeDiff(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
userConfig := config.GetDefaultConfig()
|
||||
appState := &config.AppState{}
|
||||
@@ -375,7 +372,6 @@ func TestWorkingTreeShowFileDiff(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
userConfig := config.GetDefaultConfig()
|
||||
appState := &config.AppState{}
|
||||
@@ -428,7 +424,6 @@ func TestWorkingTreeCheckoutFile(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner})
|
||||
|
||||
@@ -459,7 +454,6 @@ func TestWorkingTreeDiscardUnstagedFileChanges(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner})
|
||||
s.test(instance.DiscardUnstagedFileChanges(s.file))
|
||||
@@ -487,7 +481,6 @@ func TestWorkingTreeDiscardAnyUnstagedFileChanges(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner})
|
||||
s.test(instance.DiscardAnyUnstagedFileChanges())
|
||||
@@ -515,7 +508,6 @@ func TestWorkingTreeRemoveUntrackedFiles(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner})
|
||||
s.test(instance.RemoveUntrackedFiles())
|
||||
@@ -545,7 +537,6 @@ func TestWorkingTreeResetHard(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner})
|
||||
s.test(instance.ResetHard(s.ref))
|
||||
|
||||
@@ -76,8 +76,6 @@ func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) {
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(worktrees))
|
||||
for _, worktree := range worktrees {
|
||||
worktree := worktree
|
||||
|
||||
go utils.Safe(func() {
|
||||
defer wg.Done()
|
||||
|
||||
|
||||
@@ -181,7 +181,6 @@ branch refs/heads/mybranch-worktree
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
runner := oscommands.NewFakeRunner(t)
|
||||
fs := afero.NewMemMapFs()
|
||||
|
||||
@@ -50,7 +50,6 @@ func TestGetBool(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
fake := NewFakeGitConfig(s.mockResponses)
|
||||
real := NewCachedGitConfig(
|
||||
@@ -87,7 +86,6 @@ func TestGet(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
fake := NewFakeGitConfig(s.mockResponses)
|
||||
real := NewCachedGitConfig(
|
||||
|
||||
@@ -413,7 +413,6 @@ func TestGetPullRequestURL(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
tr := i18n.EnglishTranslationSet()
|
||||
log := &fakes.FakeFieldLogger{}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package models
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// Branch : A git branch
|
||||
// duplicating this for now
|
||||
@@ -10,10 +13,14 @@ type Branch struct {
|
||||
DisplayName string
|
||||
// indicator of when the branch was last checked out e.g. '2d', '3m'
|
||||
Recency string
|
||||
// how many commits ahead we are from the remote branch (how many commits we can push)
|
||||
Pushables string
|
||||
// how many commits ahead we are from the remote branch (how many commits we can push, assuming we push to our tracked remote branch)
|
||||
AheadForPull string
|
||||
// how many commits behind we are from the remote branch (how many commits we can pull)
|
||||
Pullables string
|
||||
BehindForPull string
|
||||
// how many commits ahead we are from the branch we're pushing to (which might not be the same as our upstream branch in a triangular workflow)
|
||||
AheadForPush string
|
||||
// how many commits behind we are from the branch we're pushing to (which might not be the same as our upstream branch in a triangular workflow)
|
||||
BehindForPush string
|
||||
// whether the remote branch is 'gone' i.e. we're tracking a remote branch that has been deleted
|
||||
UpstreamGone bool
|
||||
// whether this is the current branch. Exactly one branch should have this be true
|
||||
@@ -28,6 +35,11 @@ type Branch struct {
|
||||
Subject string
|
||||
// commit hash
|
||||
CommitHash string
|
||||
|
||||
// How far we have fallen behind our base branch. 0 means either not
|
||||
// determined yet, or up to date with base branch. (We don't need to
|
||||
// distinguish the two, as we don't draw anything in both cases.)
|
||||
BehindBaseBranch atomic.Int32
|
||||
}
|
||||
|
||||
func (b *Branch) FullRefName() string {
|
||||
@@ -80,26 +92,30 @@ func (b *Branch) IsTrackingRemote() bool {
|
||||
// we know that the remote branch is not stored locally based on our pushable/pullable
|
||||
// count being question marks.
|
||||
func (b *Branch) RemoteBranchStoredLocally() bool {
|
||||
return b.IsTrackingRemote() && b.Pushables != "?" && b.Pullables != "?"
|
||||
return b.IsTrackingRemote() && b.AheadForPull != "?" && b.BehindForPull != "?"
|
||||
}
|
||||
|
||||
func (b *Branch) RemoteBranchNotStoredLocally() bool {
|
||||
return b.IsTrackingRemote() && b.Pushables == "?" && b.Pullables == "?"
|
||||
return b.IsTrackingRemote() && b.AheadForPull == "?" && b.BehindForPull == "?"
|
||||
}
|
||||
|
||||
func (b *Branch) MatchesUpstream() bool {
|
||||
return b.RemoteBranchStoredLocally() && b.Pushables == "0" && b.Pullables == "0"
|
||||
return b.RemoteBranchStoredLocally() && b.AheadForPull == "0" && b.BehindForPull == "0"
|
||||
}
|
||||
|
||||
func (b *Branch) HasCommitsToPush() bool {
|
||||
return b.RemoteBranchStoredLocally() && b.Pushables != "0"
|
||||
func (b *Branch) IsAheadForPull() bool {
|
||||
return b.RemoteBranchStoredLocally() && b.AheadForPull != "0"
|
||||
}
|
||||
|
||||
func (b *Branch) HasCommitsToPull() bool {
|
||||
return b.RemoteBranchStoredLocally() && b.Pullables != "0"
|
||||
func (b *Branch) IsBehindForPull() bool {
|
||||
return b.RemoteBranchStoredLocally() && b.BehindForPull != "0"
|
||||
}
|
||||
|
||||
func (b *Branch) IsBehindForPush() bool {
|
||||
return b.RemoteBranchStoredLocally() && b.BehindForPush != "0"
|
||||
}
|
||||
|
||||
// for when we're in a detached head state
|
||||
func (b *Branch) IsRealBranch() bool {
|
||||
return b.Pushables != "" && b.Pullables != ""
|
||||
return b.AheadForPull != "" && b.BehindForPull != ""
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ func (self *cmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line st
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(stdoutPipe)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
scanner.Split(utils.ScanLinesAndTruncateWhenLongerThanBuffer(bufio.MaxScanTokenSize))
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -178,6 +178,11 @@ func (self *cmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line st
|
||||
}
|
||||
}
|
||||
|
||||
if scanner.Err() != nil {
|
||||
_ = Kill(cmd)
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
_ = cmd.Wait()
|
||||
|
||||
self.log.Infof("%s (%s)", cmdObj.ToString(), time.Since(t))
|
||||
|
||||
@@ -239,14 +239,13 @@ func (c *OSCommand) PipeCommands(cmdObjs ...ICmdObj) error {
|
||||
wg.Add(len(cmds))
|
||||
|
||||
for _, cmd := range cmds {
|
||||
currentCmd := cmd
|
||||
go utils.Safe(func() {
|
||||
stderr, err := currentCmd.StderrPipe()
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
c.Log.Error(err)
|
||||
}
|
||||
|
||||
if err := currentCmd.Start(); err != nil {
|
||||
if err := cmd.Start(); err != nil {
|
||||
c.Log.Error(err)
|
||||
}
|
||||
|
||||
@@ -256,7 +255,7 @@ func (c *OSCommand) PipeCommands(cmdObjs ...ICmdObj) error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := currentCmd.Wait(); err != nil {
|
||||
if err := cmd.Wait(); err != nil {
|
||||
c.Log.Error(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -509,7 +509,6 @@ func TestTransform(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
lineIndices := ExpandRange(s.firstLineIndex, s.lastLineIndex)
|
||||
|
||||
@@ -566,7 +565,6 @@ func TestParseAndFormatPlain(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
// here we parse the patch, then format it, and ensure the result
|
||||
// matches the original patch. Note that unified diffs allow omitting
|
||||
@@ -604,7 +602,6 @@ func TestLineNumberOfLine(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
for i, idx := range s.indexes {
|
||||
patch := Parse(s.patchStr)
|
||||
@@ -633,7 +630,6 @@ func TestGetNextStageableLineIndex(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
for i, idx := range s.indexes {
|
||||
patch := Parse(s.patchStr)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package config
|
||||
|
||||
import "os"
|
||||
|
||||
func GetEditTemplate(osConfig *OSConfig, guessDefaultEditor func() string) (string, bool) {
|
||||
preset := getPreset(osConfig, guessDefaultEditor)
|
||||
template := osConfig.Edit
|
||||
@@ -42,9 +44,11 @@ type editPreset struct {
|
||||
editAtLineTemplate string
|
||||
editAtLineAndWaitTemplate string
|
||||
openDirInEditorTemplate string
|
||||
suspend bool
|
||||
suspend func() bool
|
||||
}
|
||||
|
||||
func returnBool(a bool) func() bool { return (func() bool { return a }) }
|
||||
|
||||
// IF YOU ADD A PRESET TO THIS FUNCTION YOU MUST UPDATE THE `Supported presets` SECTION OF docs/Config.md
|
||||
func getPreset(osConfig *OSConfig, guessDefaultEditor func() string) *editPreset {
|
||||
presets := map[string]*editPreset{
|
||||
@@ -52,12 +56,15 @@ func getPreset(osConfig *OSConfig, guessDefaultEditor func() string) *editPreset
|
||||
"vim": standardTerminalEditorPreset("vim"),
|
||||
"nvim": standardTerminalEditorPreset("nvim"),
|
||||
"nvim-remote": {
|
||||
editTemplate: `nvim --server "$NVIM" --remote-tab {{filename}}`,
|
||||
editAtLineTemplate: `nvim --server "$NVIM" --remote-tab {{filename}}; [ -z "$NVIM" ] || nvim --server "$NVIM" --remote-send ":{{line}}<CR>"`,
|
||||
editTemplate: `[ -z "$NVIM" ] && (nvim -- {{filename}}) || (nvim --server "$NVIM" --remote-send "q" && nvim --server "$NVIM" --remote-tab {{filename}})`,
|
||||
editAtLineTemplate: `[ -z "$NVIM" ] && (nvim +{{line}} -- {{filename}}) || (nvim --server "$NVIM" --remote-send "q" && nvim --server "$NVIM" --remote-tab {{filename}} && nvim --server "$NVIM" --remote-send ":{{line}}<CR>")`,
|
||||
// No remote-wait support yet. See https://github.com/neovim/neovim/pull/17856
|
||||
editAtLineAndWaitTemplate: `nvim +{{line}} {{filename}}`,
|
||||
openDirInEditorTemplate: `nvim --server "$NVIM" --remote-tab {{dir}}`,
|
||||
suspend: false,
|
||||
openDirInEditorTemplate: `[ -z "$NVIM" ] && (nvim -- {{dir}}) || (nvim --server "$NVIM" --remote-send "q" && nvim --server "$NVIM" --remote-tab {{dir}})`,
|
||||
suspend: func() bool {
|
||||
_, ok := os.LookupEnv("NVIM")
|
||||
return !ok
|
||||
},
|
||||
},
|
||||
"lvim": standardTerminalEditorPreset("lvim"),
|
||||
"emacs": standardTerminalEditorPreset("emacs"),
|
||||
@@ -69,42 +76,42 @@ func getPreset(osConfig *OSConfig, guessDefaultEditor func() string) *editPreset
|
||||
editAtLineTemplate: "helix -- {{filename}}:{{line}}",
|
||||
editAtLineAndWaitTemplate: "helix -- {{filename}}:{{line}}",
|
||||
openDirInEditorTemplate: "helix -- {{dir}}",
|
||||
suspend: true,
|
||||
suspend: returnBool(true),
|
||||
},
|
||||
"helix (hx)": {
|
||||
editTemplate: "hx -- {{filename}}",
|
||||
editAtLineTemplate: "hx -- {{filename}}:{{line}}",
|
||||
editAtLineAndWaitTemplate: "hx -- {{filename}}:{{line}}",
|
||||
openDirInEditorTemplate: "hx -- {{dir}}",
|
||||
suspend: true,
|
||||
suspend: returnBool(true),
|
||||
},
|
||||
"vscode": {
|
||||
editTemplate: "code --reuse-window -- {{filename}}",
|
||||
editAtLineTemplate: "code --reuse-window --goto -- {{filename}}:{{line}}",
|
||||
editAtLineAndWaitTemplate: "code --reuse-window --goto --wait -- {{filename}}:{{line}}",
|
||||
openDirInEditorTemplate: "code -- {{dir}}",
|
||||
suspend: false,
|
||||
suspend: returnBool(false),
|
||||
},
|
||||
"sublime": {
|
||||
editTemplate: "subl -- {{filename}}",
|
||||
editAtLineTemplate: "subl -- {{filename}}:{{line}}",
|
||||
editAtLineAndWaitTemplate: "subl --wait -- {{filename}}:{{line}}",
|
||||
openDirInEditorTemplate: "subl -- {{dir}}",
|
||||
suspend: false,
|
||||
suspend: returnBool(false),
|
||||
},
|
||||
"bbedit": {
|
||||
editTemplate: "bbedit -- {{filename}}",
|
||||
editAtLineTemplate: "bbedit +{{line}} -- {{filename}}",
|
||||
editAtLineAndWaitTemplate: "bbedit +{{line}} --wait -- {{filename}}",
|
||||
openDirInEditorTemplate: "bbedit -- {{dir}}",
|
||||
suspend: false,
|
||||
suspend: returnBool(false),
|
||||
},
|
||||
"xcode": {
|
||||
editTemplate: "xed -- {{filename}}",
|
||||
editAtLineTemplate: "xed --line {{line}} -- {{filename}}",
|
||||
editAtLineAndWaitTemplate: "xed --line {{line}} --wait -- {{filename}}",
|
||||
openDirInEditorTemplate: "xed -- {{dir}}",
|
||||
suspend: false,
|
||||
suspend: returnBool(false),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -141,7 +148,7 @@ func standardTerminalEditorPreset(editor string) *editPreset {
|
||||
editAtLineTemplate: editor + " +{{line}} -- {{filename}}",
|
||||
editAtLineAndWaitTemplate: editor + " +{{line}} -- {{filename}}",
|
||||
openDirInEditorTemplate: editor + " -- {{dir}}",
|
||||
suspend: true,
|
||||
suspend: returnBool(true),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,5 +156,5 @@ func getEditInTerminal(osConfig *OSConfig, preset *editPreset) bool {
|
||||
if osConfig.SuspendOnEdit != nil {
|
||||
return *osConfig.SuspendOnEdit
|
||||
}
|
||||
return preset.suspend
|
||||
return preset.suspend()
|
||||
}
|
||||
|
||||
+52
-34
@@ -19,12 +19,9 @@ type UserConfig struct {
|
||||
ConfirmOnQuit bool `yaml:"confirmOnQuit"`
|
||||
// If true, exit Lazygit when the user presses escape in a context where there is nothing to cancel/close
|
||||
QuitOnTopLevelReturn bool `yaml:"quitOnTopLevelReturn"`
|
||||
// Keybindings
|
||||
Keybinding KeybindingConfig `yaml:"keybinding"`
|
||||
// Config relating to things outside of Lazygit like how files are opened, copying to clipboard, etc
|
||||
OS OSConfig `yaml:"os,omitempty"`
|
||||
// If true, don't display introductory popups upon opening Lazygit.
|
||||
// Lazygit sets this to true upon first runninng the program so that you don't see introductory popups every time you open the program.
|
||||
DisableStartupPopups bool `yaml:"disableStartupPopups"`
|
||||
// User-configured commands that can be invoked from within Lazygit
|
||||
CustomCommands []CustomCommand `yaml:"customCommands" jsonschema:"uniqueItems=true"`
|
||||
@@ -38,6 +35,8 @@ type UserConfig struct {
|
||||
NotARepository string `yaml:"notARepository" jsonschema:"enum=prompt,enum=create,enum=skip,enum=quit"`
|
||||
// If true, display a confirmation when subprocess terminates. This allows you to view the output of the subprocess before returning to Lazygit.
|
||||
PromptToReturnFromSubprocess bool `yaml:"promptToReturnFromSubprocess"`
|
||||
// Keybindings
|
||||
Keybinding KeybindingConfig `yaml:"keybinding"`
|
||||
}
|
||||
|
||||
type RefresherConfig struct {
|
||||
@@ -78,6 +77,9 @@ type GuiConfig struct {
|
||||
SidePanelWidth float64 `yaml:"sidePanelWidth" jsonschema:"maximum=1,minimum=0"`
|
||||
// If true, increase the height of the focused side window; creating an accordion effect.
|
||||
ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
|
||||
// The weight of the expanded side panel, relative to the other panels. 2 means
|
||||
// twice as tall as the other panels. Only relevant if `expandFocusedSidePanel` is true.
|
||||
ExpandedSidePanelWeight int `yaml:"expandedSidePanelWeight"`
|
||||
// Sometimes the main window is split in two (e.g. when the selected file has both staged and unstaged changes). This setting controls how the two sections are split.
|
||||
// Options are:
|
||||
// - 'horizontal': split the window horizontally
|
||||
@@ -123,8 +125,13 @@ type GuiConfig struct {
|
||||
NerdFontsVersion string `yaml:"nerdFontsVersion" jsonschema:"enum=2,enum=3,enum="`
|
||||
// If true (default), file icons are shown in the file views. Only relevant if NerdFontsVersion is not empty.
|
||||
ShowFileIcons bool `yaml:"showFileIcons"`
|
||||
// Length of commit hash in commits view. 0 shows '*' if NF icons aren't on.
|
||||
CommitHashLength int `yaml:"commitHashLength" jsonschema:"minimum=0"`
|
||||
// If true, show commit hashes alongside branch names in the branches view.
|
||||
ShowBranchCommitHash bool `yaml:"showBranchCommitHash"`
|
||||
// Whether to show the divergence from the base branch in the branches view.
|
||||
// One of: 'none' | 'onlyArrow' | 'arrowAndNumber'
|
||||
ShowDivergenceFromBaseBranch string `yaml:"showDivergenceFromBaseBranch" jsonschema:"enum=none,enum=onlyArrow,enum=arrowAndNumber"`
|
||||
// Height of the command log view
|
||||
CommandLogSize int `yaml:"commandLogSize" jsonschema:"minimum=0"`
|
||||
// Whether to split the main window when viewing file changes.
|
||||
@@ -150,6 +157,8 @@ type GuiConfig struct {
|
||||
// Status panel view.
|
||||
// One of 'dashboard' (default) | 'allBranchesLog'
|
||||
StatusPanelView string `yaml:"statusPanelView" jsonschema:"enum=dashboard,enum=allBranchesLog"`
|
||||
// Horizontal padding between views that are next to each other.
|
||||
HorizontalPadding int `yaml:"horizontalPadding"`
|
||||
}
|
||||
|
||||
func (c *GuiConfig) UseFuzzySearch() bool {
|
||||
@@ -220,6 +229,8 @@ type GitConfig struct {
|
||||
// If true, do not allow force pushes
|
||||
DisableForcePushing bool `yaml:"disableForcePushing"`
|
||||
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#predefined-commit-message-prefix
|
||||
CommitPrefix *CommitPrefixConfig `yaml:"commitPrefix"`
|
||||
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#predefined-commit-message-prefix
|
||||
CommitPrefixes map[string]CommitPrefixConfig `yaml:"commitPrefixes"`
|
||||
// If true, parse emoji strings in commit messages e.g. render :rocket: as 🚀
|
||||
// (This should really be under 'gui', not 'git')
|
||||
@@ -248,7 +259,7 @@ type PagingConfig struct {
|
||||
// diff-so-fancy
|
||||
// delta --dark --paging=never
|
||||
// ydiff -p cat -s --wrap --width={{columnWidth}}
|
||||
Pager PagerType `yaml:"pager" jsonschema:"minLength=1"`
|
||||
Pager PagerType `yaml:"pager"`
|
||||
// If true, Lazygit will use whatever pager is specified in `$GIT_PAGER`, `$PAGER`, or your *git config*. If the pager ends with something like ` | less` we will strip that part out, because less doesn't play nice with our rendering approach. If the custom pager uses less under the hood, that will also break rendering (hence the `--paging=never` flag for the `delta` pager).
|
||||
UseConfig bool `yaml:"useConfig"`
|
||||
// e.g. 'difft --color=always'
|
||||
@@ -290,9 +301,9 @@ type LogConfig struct {
|
||||
|
||||
type CommitPrefixConfig struct {
|
||||
// pattern to match on. E.g. for 'feature/AB-123' to match on the AB-123 use "^\\w+\\/(\\w+-\\w+).*"
|
||||
Pattern string `yaml:"pattern" jsonschema:"example=^\\w+\\/(\\w+-\\w+).*,minLength=1"`
|
||||
Pattern string `yaml:"pattern" jsonschema:"example=^\\w+\\/(\\w+-\\w+).*"`
|
||||
// Replace directive. E.g. for 'feature/AB-123' to start the commit message with 'AB-123 ' use "[$1] "
|
||||
Replace string `yaml:"replace" jsonschema:"example=[$1] ,minLength=1"`
|
||||
Replace string `yaml:"replace" jsonschema:"example=[$1]"`
|
||||
}
|
||||
|
||||
type UpdateConfig struct {
|
||||
@@ -577,6 +588,8 @@ type CustomCommand struct {
|
||||
Stream bool `yaml:"stream"`
|
||||
// If true, show the command's output in a popup within Lazygit
|
||||
ShowOutput bool `yaml:"showOutput"`
|
||||
// The title to display in the popup panel if showOutput is true. If left unset, the command will be used as the title.
|
||||
OutputTitle string `yaml:"outputTitle"`
|
||||
// Actions to take after the command has completed
|
||||
After CustomCommandAfterHook `yaml:"after"`
|
||||
}
|
||||
@@ -646,6 +659,7 @@ func GetDefaultConfig() *UserConfig {
|
||||
SkipStashWarning: false,
|
||||
SidePanelWidth: 0.3333,
|
||||
ExpandFocusedSidePanel: false,
|
||||
ExpandedSidePanelWeight: 2,
|
||||
MainPanelSplitMode: "flexible",
|
||||
EnlargedSideViewLocation: "left",
|
||||
Language: "auto",
|
||||
@@ -664,30 +678,34 @@ func GetDefaultConfig() *UserConfig {
|
||||
UnstagedChangesColor: []string{"red"},
|
||||
DefaultFgColor: []string{"default"},
|
||||
},
|
||||
CommitLength: CommitLengthConfig{Show: true},
|
||||
SkipNoStagedFilesWarning: false,
|
||||
ShowListFooter: true,
|
||||
ShowCommandLog: true,
|
||||
ShowBottomLine: true,
|
||||
ShowPanelJumps: true,
|
||||
ShowFileTree: true,
|
||||
ShowRandomTip: true,
|
||||
ShowIcons: false,
|
||||
NerdFontsVersion: "",
|
||||
ShowFileIcons: true,
|
||||
ShowBranchCommitHash: false,
|
||||
CommandLogSize: 8,
|
||||
SplitDiff: "auto",
|
||||
SkipRewordInEditorWarning: false,
|
||||
Border: "rounded",
|
||||
AnimateExplosion: true,
|
||||
PortraitMode: "auto",
|
||||
FilterMode: "substring",
|
||||
CommitLength: CommitLengthConfig{Show: true},
|
||||
SkipNoStagedFilesWarning: false,
|
||||
ShowListFooter: true,
|
||||
ShowCommandLog: true,
|
||||
ShowBottomLine: true,
|
||||
ShowPanelJumps: true,
|
||||
ShowFileTree: true,
|
||||
ShowRandomTip: true,
|
||||
ShowIcons: false,
|
||||
NerdFontsVersion: "",
|
||||
ShowFileIcons: true,
|
||||
CommitHashLength: 8,
|
||||
ShowBranchCommitHash: false,
|
||||
ShowDivergenceFromBaseBranch: "none",
|
||||
CommandLogSize: 8,
|
||||
SplitDiff: "auto",
|
||||
SkipRewordInEditorWarning: false,
|
||||
WindowSize: "normal",
|
||||
Border: "rounded",
|
||||
AnimateExplosion: true,
|
||||
PortraitMode: "auto",
|
||||
FilterMode: "substring",
|
||||
Spinner: SpinnerConfig{
|
||||
Frames: []string{"|", "/", "-", "\\"},
|
||||
Rate: 50,
|
||||
},
|
||||
StatusPanelView: "dashboard",
|
||||
StatusPanelView: "dashboard",
|
||||
HorizontalPadding: 0,
|
||||
},
|
||||
Git: GitConfig{
|
||||
Paging: PagingConfig{
|
||||
@@ -730,8 +748,14 @@ func GetDefaultConfig() *UserConfig {
|
||||
Method: "prompt",
|
||||
Days: 14,
|
||||
},
|
||||
ConfirmOnQuit: false,
|
||||
QuitOnTopLevelReturn: false,
|
||||
ConfirmOnQuit: false,
|
||||
QuitOnTopLevelReturn: false,
|
||||
OS: OSConfig{},
|
||||
DisableStartupPopups: false,
|
||||
CustomCommands: []CustomCommand(nil),
|
||||
Services: map[string]string(nil),
|
||||
NotARepository: "prompt",
|
||||
PromptToReturnFromSubprocess: true,
|
||||
Keybinding: KeybindingConfig{
|
||||
Universal: KeybindingUniversalConfig{
|
||||
Quit: "q",
|
||||
@@ -898,11 +922,5 @@ func GetDefaultConfig() *UserConfig {
|
||||
CommitMenu: "<c-o>",
|
||||
},
|
||||
},
|
||||
OS: OSConfig{},
|
||||
DisableStartupPopups: false,
|
||||
CustomCommands: []CustomCommand(nil),
|
||||
Services: map[string]string(nil),
|
||||
NotARepository: "prompt",
|
||||
PromptToReturnFromSubprocess: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,12 @@ import (
|
||||
)
|
||||
|
||||
func (config *UserConfig) Validate() error {
|
||||
if err := validateEnum("gui.statusPanelView", config.Gui.StatusPanelView, []string{"dashboard", "allBranchesLog"}); err != nil {
|
||||
if err := validateEnum("gui.statusPanelView", config.Gui.StatusPanelView,
|
||||
[]string{"dashboard", "allBranchesLog"}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateEnum("gui.showDivergenceFromBaseBranch", config.Gui.ShowDivergenceFromBaseBranch,
|
||||
[]string{"none", "onlyArrow", "arrowAndNumber"}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -105,7 +105,6 @@ func (self *MenuViewModel) GetNonModelItems() []*NonModelItem {
|
||||
menuItems := self.FilteredListViewModel.GetItems()
|
||||
var prevSection *types.MenuSection = nil
|
||||
for i, menuItem := range menuItems {
|
||||
menuItem := menuItem
|
||||
if menuItem.Section != nil && menuItem.Section != prevSection {
|
||||
if prevSection != nil {
|
||||
result = append(result, &NonModelItem{
|
||||
|
||||
@@ -75,7 +75,7 @@ func NewSubCommitsContext(
|
||||
endIdx,
|
||||
// Don't show the graph in the left/right view; we'd like to, but
|
||||
// it's too complicated:
|
||||
shouldShowGraph(c) && viewModel.GetRefToShowDivergenceFrom() == "",
|
||||
shouldShowGraph(c),
|
||||
git_commands.NewNullBisectInfo(),
|
||||
false,
|
||||
)
|
||||
|
||||
@@ -14,10 +14,13 @@ type SuggestionsContext struct {
|
||||
}
|
||||
|
||||
type SuggestionsContextState struct {
|
||||
Suggestions []*types.Suggestion
|
||||
OnConfirm func() error
|
||||
OnClose func() error
|
||||
AsyncHandler *tasks.AsyncHandler
|
||||
Suggestions []*types.Suggestion
|
||||
OnConfirm func() error
|
||||
OnClose func() error
|
||||
OnDeleteSuggestion func() error
|
||||
AsyncHandler *tasks.AsyncHandler
|
||||
|
||||
AllowEditSuggestion bool
|
||||
|
||||
// FindSuggestions will take a string that the user has typed into a prompt
|
||||
// and return a slice of suggestions which match that string.
|
||||
|
||||
@@ -100,14 +100,13 @@ func (self *BranchesController) GetKeybindings(opts types.KeybindingsOpts) []*ty
|
||||
DisplayOnScreen: true,
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Branches.RebaseBranch),
|
||||
Handler: opts.Guards.OutsideFilterMode(self.rebase),
|
||||
GetDisabledReason: self.require(
|
||||
self.singleItemSelected(self.notRebasingOntoSelf),
|
||||
),
|
||||
Description: self.c.Tr.RebaseBranch,
|
||||
Tooltip: self.c.Tr.RebaseBranchTooltip,
|
||||
DisplayOnScreen: true,
|
||||
Key: opts.GetKey(opts.Config.Branches.RebaseBranch),
|
||||
Handler: opts.Guards.OutsideFilterMode(self.withItem(self.rebase)),
|
||||
GetDisabledReason: self.require(self.singleItemSelected()),
|
||||
Description: self.c.Tr.RebaseBranch,
|
||||
Tooltip: self.c.Tr.RebaseBranchTooltip,
|
||||
OpensMenu: true,
|
||||
DisplayOnScreen: true,
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Branches.MergeIntoCurrentBranch),
|
||||
@@ -205,6 +204,40 @@ func (self *BranchesController) viewUpstreamOptions(selectedBranch *models.Branc
|
||||
},
|
||||
}
|
||||
|
||||
var disabledReason *types.DisabledReason
|
||||
baseBranch, err := self.c.Git().Loaders.BranchLoader.GetBaseBranch(selectedBranch, self.c.Model().MainBranches)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if baseBranch == "" {
|
||||
baseBranch = self.c.Tr.CouldNotDetermineBaseBranch
|
||||
disabledReason = &types.DisabledReason{Text: self.c.Tr.CouldNotDetermineBaseBranch}
|
||||
}
|
||||
shortBaseBranchName := helpers.ShortBranchName(baseBranch)
|
||||
label := utils.ResolvePlaceholderString(
|
||||
self.c.Tr.ViewDivergenceFromBaseBranch,
|
||||
map[string]string{"baseBranch": shortBaseBranchName},
|
||||
)
|
||||
viewDivergenceFromBaseBranchItem := &types.MenuItem{
|
||||
LabelColumns: []string{label},
|
||||
Key: 'b',
|
||||
OnPress: func() error {
|
||||
branch := self.context().GetSelected()
|
||||
if branch == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return self.c.Helpers().SubCommits.ViewSubCommits(helpers.ViewSubCommitsOpts{
|
||||
Ref: branch,
|
||||
TitleRef: fmt.Sprintf("%s <-> %s", branch.RefName(), shortBaseBranchName),
|
||||
RefToShowDivergenceFrom: baseBranch,
|
||||
Context: self.context(),
|
||||
ShowBranchHeads: false,
|
||||
})
|
||||
},
|
||||
DisabledReason: disabledReason,
|
||||
}
|
||||
|
||||
unsetUpstreamItem := &types.MenuItem{
|
||||
LabelColumns: []string{self.c.Tr.UnsetUpstream},
|
||||
OnPress: func() error {
|
||||
@@ -312,6 +345,7 @@ func (self *BranchesController) viewUpstreamOptions(selectedBranch *models.Branc
|
||||
|
||||
options := []*types.MenuItem{
|
||||
viewDivergenceItem,
|
||||
viewDivergenceFromBaseBranchItem,
|
||||
unsetUpstreamItem,
|
||||
setUpstreamItem,
|
||||
upstreamResetItem,
|
||||
@@ -598,19 +632,8 @@ func (self *BranchesController) merge() error {
|
||||
return self.c.Helpers().MergeAndRebase.MergeRefIntoCheckedOutBranch(selectedBranchName)
|
||||
}
|
||||
|
||||
func (self *BranchesController) rebase() error {
|
||||
selectedBranchName := self.context().GetSelected().Name
|
||||
return self.c.Helpers().MergeAndRebase.RebaseOntoRef(selectedBranchName)
|
||||
}
|
||||
|
||||
func (self *BranchesController) notRebasingOntoSelf(branch *models.Branch) *types.DisabledReason {
|
||||
selectedBranchName := branch.Name
|
||||
checkedOutBranch := self.c.Helpers().Refs.GetCheckedOutRef().Name
|
||||
if selectedBranchName == checkedOutBranch {
|
||||
return &types.DisabledReason{Text: self.c.Tr.CantRebaseOntoSelf}
|
||||
}
|
||||
|
||||
return nil
|
||||
func (self *BranchesController) rebase(branch *models.Branch) error {
|
||||
return self.c.Helpers().MergeAndRebase.RebaseOntoRef(branch.Name)
|
||||
}
|
||||
|
||||
func (self *BranchesController) fastForward(branch *models.Branch) error {
|
||||
@@ -620,7 +643,7 @@ func (self *BranchesController) fastForward(branch *models.Branch) error {
|
||||
if !branch.RemoteBranchStoredLocally() {
|
||||
return errors.New(self.c.Tr.FwdNoLocalUpstream)
|
||||
}
|
||||
if branch.HasCommitsToPush() {
|
||||
if branch.IsAheadForPull() {
|
||||
return errors.New(self.c.Tr.FwdCommitsToPush)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
@@ -39,6 +41,14 @@ func (self *ConfirmationController) GetKeybindings(opts types.KeybindingsOpts) [
|
||||
Key: opts.GetKey(opts.Config.Universal.TogglePanel),
|
||||
Handler: func() error {
|
||||
if len(self.c.Contexts().Suggestions.State.Suggestions) > 0 {
|
||||
subtitle := ""
|
||||
if self.c.State().GetRepoState().GetCurrentPopupOpts().HandleDeleteSuggestion != nil {
|
||||
// We assume that whenever things are deletable, they
|
||||
// are also editable, so we show both keybindings
|
||||
subtitle = fmt.Sprintf(self.c.Tr.SuggestionsSubtitle,
|
||||
self.c.UserConfig.Keybinding.Universal.Remove, self.c.UserConfig.Keybinding.Universal.Edit)
|
||||
}
|
||||
self.c.Views().Suggestions.Subtitle = subtitle
|
||||
return self.c.ReplaceContext(self.c.Contexts().Suggestions)
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
|
||||
@@ -17,6 +18,7 @@ func (self *CustomCommandAction) Call() error {
|
||||
return self.c.Prompt(types.PromptOpts{
|
||||
Title: self.c.Tr.CustomCommand,
|
||||
FindSuggestionsFunc: self.GetCustomCommandsHistorySuggestionsFunc(),
|
||||
AllowEditSuggestion: true,
|
||||
HandleConfirm: func(command string) error {
|
||||
if self.shouldSaveCommand(command) {
|
||||
self.c.GetAppState().CustomCommandsHistory = utils.Limit(
|
||||
@@ -32,13 +34,34 @@ func (self *CustomCommandAction) Call() error {
|
||||
self.c.OS().Cmd.NewShell(command),
|
||||
)
|
||||
},
|
||||
HandleDeleteSuggestion: func(index int) error {
|
||||
// index is the index in the _filtered_ list of suggestions, so we
|
||||
// need to map it back to the full list. There's no really good way
|
||||
// to do this, but fortunately we keep the items in the
|
||||
// CustomCommandsHistory unique, which allows us to simply search
|
||||
// for it by string.
|
||||
item := self.c.Contexts().Suggestions.GetItems()[index].Value
|
||||
fullIndex := lo.IndexOf(self.c.GetAppState().CustomCommandsHistory, item)
|
||||
if fullIndex == -1 {
|
||||
// Should never happen, but better be safe
|
||||
return nil
|
||||
}
|
||||
|
||||
self.c.GetAppState().CustomCommandsHistory = slices.Delete(
|
||||
self.c.GetAppState().CustomCommandsHistory, fullIndex, fullIndex+1)
|
||||
self.c.SaveAppStateAndLogError()
|
||||
self.c.Contexts().Suggestions.RefreshSuggestions()
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *CustomCommandAction) GetCustomCommandsHistorySuggestionsFunc() func(string) []*types.Suggestion {
|
||||
history := self.c.GetAppState().CustomCommandsHistory
|
||||
return func(input string) []*types.Suggestion {
|
||||
history := self.c.GetAppState().CustomCommandsHistory
|
||||
|
||||
return helpers.FilterFunc(history, self.c.UserConfig.Gui.UseFuzzySearch())
|
||||
return helpers.FilterFunc(history, self.c.UserConfig.Gui.UseFuzzySearch())(input)
|
||||
}
|
||||
}
|
||||
|
||||
// this mimics the shell functionality `ignorespace`
|
||||
|
||||
@@ -217,7 +217,10 @@ func (self *CustomPatchOptionsMenuAction) handlePullPatchIntoNewCommit() error {
|
||||
_ = self.c.Helpers().Commits.PopCommitMessageContexts()
|
||||
self.c.LogAction(self.c.Tr.Actions.MovePatchIntoNewCommit)
|
||||
err := self.c.Git().Patch.PullPatchIntoNewCommit(self.c.Model().Commits, commitIndex, summary, description)
|
||||
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err)
|
||||
if err := self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err); err != nil {
|
||||
return err
|
||||
}
|
||||
return self.c.PushContext(self.c.Contexts().LocalCommits)
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
@@ -17,7 +17,6 @@ func (self *DiffingMenuAction) Call() error {
|
||||
|
||||
menuItems := []*types.MenuItem{}
|
||||
for _, name := range names {
|
||||
name := name
|
||||
menuItems = append(menuItems, []*types.MenuItem{
|
||||
{
|
||||
Label: fmt.Sprintf("%s %s", self.c.Tr.Diff, name),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
@@ -44,3 +46,7 @@ func (self *BranchesHelper) ConfirmDeleteRemote(remoteName string, branchName st
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func ShortBranchName(fullBranchName string) string {
|
||||
return strings.TrimPrefix(strings.TrimPrefix(fullBranchName, "refs/heads/"), "refs/remotes/")
|
||||
}
|
||||
|
||||
@@ -159,6 +159,7 @@ func (self *ConfirmationHelper) prepareConfirmationPanel(
|
||||
suggestionsContext.SetSuggestions(opts.FindSuggestionsFunc(""))
|
||||
suggestionsView.Visible = true
|
||||
suggestionsView.Title = fmt.Sprintf(self.c.Tr.SuggestionsTitle, self.c.UserConfig.Keybinding.Universal.TogglePanel)
|
||||
suggestionsView.Subtitle = ""
|
||||
}
|
||||
|
||||
self.ResizeConfirmationPanel()
|
||||
@@ -223,6 +224,8 @@ func (self *ConfirmationHelper) CreatePopupPanel(ctx goContext.Context, opts typ
|
||||
return err
|
||||
}
|
||||
|
||||
self.c.Contexts().Suggestions.State.AllowEditSuggestion = opts.AllowEditSuggestion
|
||||
|
||||
self.c.State().GetRepoState().SetCurrentPopupOpts(&opts)
|
||||
|
||||
return self.c.PushContext(self.c.Contexts().Confirmation)
|
||||
@@ -270,10 +273,20 @@ func (self *ConfirmationHelper) setKeyBindings(cancel goContext.CancelFunc, opts
|
||||
|
||||
onClose := self.wrappedConfirmationFunction(cancel, opts.HandleClose)
|
||||
|
||||
onDeleteSuggestion := func() error {
|
||||
if opts.HandleDeleteSuggestion == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
idx := self.c.Contexts().Suggestions.GetSelectedLineIdx()
|
||||
return opts.HandleDeleteSuggestion(idx)
|
||||
}
|
||||
|
||||
self.c.Contexts().Confirmation.State.OnConfirm = onConfirm
|
||||
self.c.Contexts().Confirmation.State.OnClose = onClose
|
||||
self.c.Contexts().Suggestions.State.OnConfirm = onSuggestionConfirm
|
||||
self.c.Contexts().Suggestions.State.OnClose = onClose
|
||||
self.c.Contexts().Suggestions.State.OnDeleteSuggestion = onDeleteSuggestion
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -284,6 +297,7 @@ func (self *ConfirmationHelper) clearConfirmationViewKeyBindings() {
|
||||
self.c.Contexts().Confirmation.State.OnClose = noop
|
||||
self.c.Contexts().Suggestions.State.OnConfirm = noop
|
||||
self.c.Contexts().Suggestions.State.OnClose = noop
|
||||
self.c.Contexts().Suggestions.State.OnDeleteSuggestion = noop
|
||||
}
|
||||
|
||||
func (self *ConfirmationHelper) getSelectedSuggestionValue() string {
|
||||
@@ -354,7 +368,8 @@ func (self *ConfirmationHelper) resizeMenu() {
|
||||
if selectedItem != nil {
|
||||
tooltip = self.TooltipForMenuItem(selectedItem)
|
||||
}
|
||||
tooltipHeight := getMessageHeight(true, tooltip, panelWidth) + 2 // plus 2 for the frame
|
||||
contentWidth := panelWidth - 2 // minus 2 for the frame
|
||||
tooltipHeight := getMessageHeight(true, tooltip, contentWidth) + 2 // plus 2 for the frame
|
||||
_, _ = self.c.GocuiGui().SetView(self.c.Views().Tooltip.Name(), x0, tooltipTop, x1, tooltipTop+tooltipHeight-1, 0)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,13 +5,13 @@ import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/jesseduffield/generics/set"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type FixupHelper struct {
|
||||
@@ -26,7 +26,17 @@ func NewFixupHelper(
|
||||
}
|
||||
}
|
||||
|
||||
type deletedLineInfo struct {
|
||||
// hunk describes the lines in a diff hunk. Used for two distinct cases:
|
||||
//
|
||||
// - when the hunk contains some deleted lines. Because we're diffing with a
|
||||
// context of 0, all deleted lines always come first, and then the added lines
|
||||
// (if any). In this case, numLines is only the number of deleted lines, we
|
||||
// ignore whether there are also some added lines in the hunk, as this is not
|
||||
// relevant for our algorithm.
|
||||
//
|
||||
// - when the hunk contains only added lines, in which case (obviously) numLines
|
||||
// is the number of added lines.
|
||||
type hunk struct {
|
||||
filename string
|
||||
startLineIdx int
|
||||
numLines int
|
||||
@@ -37,23 +47,74 @@ func (self *FixupHelper) HandleFindBaseCommitForFixupPress() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if diff == "" {
|
||||
|
||||
deletedLineHunks, addedLineHunks := parseDiff(diff)
|
||||
|
||||
commits := self.c.Model().Commits
|
||||
|
||||
var hashes []string
|
||||
warnAboutAddedLines := false
|
||||
|
||||
if len(deletedLineHunks) > 0 {
|
||||
hashes, err = self.blameDeletedLines(deletedLineHunks)
|
||||
warnAboutAddedLines = len(addedLineHunks) > 0
|
||||
} else if len(addedLineHunks) > 0 {
|
||||
hashes, err = self.blameAddedLines(commits, addedLineHunks)
|
||||
} else {
|
||||
return errors.New(self.c.Tr.NoChangedFiles)
|
||||
}
|
||||
|
||||
deletedLineInfos, hasHunksWithOnlyAddedLines := self.parseDiff(diff)
|
||||
if len(deletedLineInfos) == 0 {
|
||||
return errors.New(self.c.Tr.NoDeletedLinesInDiff)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hashes := self.blameDeletedLines(deletedLineInfos)
|
||||
|
||||
if len(hashes) == 0 {
|
||||
// This should never happen
|
||||
return errors.New(self.c.Tr.NoBaseCommitsFound)
|
||||
}
|
||||
if len(hashes) > 1 {
|
||||
subjects, err := self.c.Git().Commit.GetHashesAndCommitMessagesFirstLine(hashes)
|
||||
|
||||
// If a commit can't be found, and the last known commit is already merged,
|
||||
// we know that the commit we're looking for is also merged. Otherwise we
|
||||
// can't tell.
|
||||
notFoundMeansMerged := len(commits) > 0 && commits[len(commits)-1].Status == models.StatusMerged
|
||||
|
||||
const (
|
||||
MERGED int = iota
|
||||
NOT_MERGED
|
||||
CANNOT_TELL
|
||||
)
|
||||
|
||||
// Group the hashes into buckets by merged status
|
||||
hashGroups := lo.GroupBy(hashes, func(hash string) int {
|
||||
commit, _, ok := self.findCommit(commits, hash)
|
||||
if ok {
|
||||
return lo.Ternary(commit.Status == models.StatusMerged, MERGED, NOT_MERGED)
|
||||
}
|
||||
return lo.Ternary(notFoundMeansMerged, MERGED, CANNOT_TELL)
|
||||
})
|
||||
|
||||
if len(hashGroups[CANNOT_TELL]) > 0 {
|
||||
// If we have any commits that we can't tell if they're merged, just
|
||||
// show the generic "not in current view" error. This can only happen if
|
||||
// a feature branch has more than 300 commits, or there is no main
|
||||
// branch. Both are so unlikely that we don't bother returning a more
|
||||
// detailed error message (e.g. we could say something about the commits
|
||||
// that *are* in the current branch, but it's not worth it).
|
||||
return errors.New(self.c.Tr.BaseCommitIsNotInCurrentView)
|
||||
}
|
||||
|
||||
if len(hashGroups[NOT_MERGED]) == 0 {
|
||||
// If all the commits are merged, show the "already on main branch"
|
||||
// error. It isn't worth doing a detailed report of which commits we
|
||||
// found.
|
||||
return errors.New(self.c.Tr.BaseCommitIsAlreadyOnMainBranch)
|
||||
}
|
||||
|
||||
if len(hashGroups[NOT_MERGED]) > 1 {
|
||||
// If there are multiple commits that could be the base commit, list
|
||||
// them in the error message. But only the candidates from the current
|
||||
// branch, not including any that are already merged.
|
||||
subjects, err := self.c.Git().Commit.GetHashesAndCommitMessagesFirstLine(hashGroups[NOT_MERGED])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -63,23 +124,9 @@ func (self *FixupHelper) HandleFindBaseCommitForFixupPress() error {
|
||||
return fmt.Errorf("%s\n\n%s", message, subjects)
|
||||
}
|
||||
|
||||
commit, index, ok := lo.FindIndexOf(self.c.Model().Commits, func(commit *models.Commit) bool {
|
||||
return commit.Hash == hashes[0]
|
||||
})
|
||||
if !ok {
|
||||
commits := self.c.Model().Commits
|
||||
if commits[len(commits)-1].Status == models.StatusMerged {
|
||||
// If the commit is not found, it's most likely because it's already
|
||||
// merged, and more than 300 commits away. Check if the last known
|
||||
// commit is already merged; if so, show the "already merged" error.
|
||||
return errors.New(self.c.Tr.BaseCommitIsAlreadyOnMainBranch)
|
||||
}
|
||||
// If we get here, the current branch must have more then 300 commits. Unlikely...
|
||||
return errors.New(self.c.Tr.BaseCommitIsNotInCurrentView)
|
||||
}
|
||||
if commit.Status == models.StatusMerged {
|
||||
return errors.New(self.c.Tr.BaseCommitIsAlreadyOnMainBranch)
|
||||
}
|
||||
// At this point we know that the NOT_MERGED bucket has exactly one commit,
|
||||
// and that's the one we want to select.
|
||||
_, index, _ := self.findCommit(commits, hashGroups[NOT_MERGED][0])
|
||||
|
||||
doIt := func() error {
|
||||
if !hasStagedChanges {
|
||||
@@ -93,7 +140,7 @@ func (self *FixupHelper) HandleFindBaseCommitForFixupPress() error {
|
||||
return self.c.PushContext(self.c.Contexts().LocalCommits)
|
||||
}
|
||||
|
||||
if hasHunksWithOnlyAddedLines {
|
||||
if warnAboutAddedLines {
|
||||
return self.c.Confirm(types.ConfirmOpts{
|
||||
Title: self.c.Tr.FindBaseCommitForFixup,
|
||||
Prompt: self.c.Tr.HunksWithOnlyAddedLinesWarning,
|
||||
@@ -122,29 +169,38 @@ func (self *FixupHelper) getDiff() (string, bool, error) {
|
||||
return diff, hasStagedChanges, err
|
||||
}
|
||||
|
||||
func (self *FixupHelper) parseDiff(diff string) ([]*deletedLineInfo, bool) {
|
||||
// Parse the diff output into hunks, and return two lists of hunks: the first
|
||||
// are ones that contain deleted lines, the second are ones that contain only
|
||||
// added lines.
|
||||
func parseDiff(diff string) ([]*hunk, []*hunk) {
|
||||
lines := strings.Split(strings.TrimSuffix(diff, "\n"), "\n")
|
||||
|
||||
deletedLineInfos := []*deletedLineInfo{}
|
||||
hasHunksWithOnlyAddedLines := false
|
||||
deletedLineHunks := []*hunk{}
|
||||
addedLineHunks := []*hunk{}
|
||||
|
||||
hunkHeaderRegexp := regexp.MustCompile(`@@ -(\d+)(?:,\d+)? \+\d+(?:,\d+)? @@`)
|
||||
|
||||
var filename string
|
||||
var currentLineInfo *deletedLineInfo
|
||||
var currentHunk *hunk
|
||||
numDeletedLines := 0
|
||||
numAddedLines := 0
|
||||
finishHunk := func() {
|
||||
if currentLineInfo != nil {
|
||||
if currentLineInfo.numLines > 0 {
|
||||
deletedLineInfos = append(deletedLineInfos, currentLineInfo)
|
||||
} else {
|
||||
hasHunksWithOnlyAddedLines = true
|
||||
if currentHunk != nil {
|
||||
if numDeletedLines > 0 {
|
||||
currentHunk.numLines = numDeletedLines
|
||||
deletedLineHunks = append(deletedLineHunks, currentHunk)
|
||||
} else if numAddedLines > 0 {
|
||||
currentHunk.numLines = numAddedLines
|
||||
addedLineHunks = append(addedLineHunks, currentHunk)
|
||||
}
|
||||
}
|
||||
numDeletedLines = 0
|
||||
numAddedLines = 0
|
||||
}
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "diff --git") {
|
||||
finishHunk()
|
||||
currentLineInfo = nil
|
||||
currentHunk = nil
|
||||
} else if strings.HasPrefix(line, "--- ") {
|
||||
// For some reason, the line ends with a tab character if the file
|
||||
// name contains spaces
|
||||
@@ -153,40 +209,42 @@ func (self *FixupHelper) parseDiff(diff string) ([]*deletedLineInfo, bool) {
|
||||
finishHunk()
|
||||
match := hunkHeaderRegexp.FindStringSubmatch(line)
|
||||
startIdx := utils.MustConvertToInt(match[1])
|
||||
currentLineInfo = &deletedLineInfo{filename, startIdx, 0}
|
||||
} else if currentLineInfo != nil && line[0] == '-' {
|
||||
currentLineInfo.numLines++
|
||||
currentHunk = &hunk{filename, startIdx, 0}
|
||||
} else if currentHunk != nil && line[0] == '-' {
|
||||
numDeletedLines++
|
||||
} else if currentHunk != nil && line[0] == '+' {
|
||||
numAddedLines++
|
||||
}
|
||||
}
|
||||
finishHunk()
|
||||
|
||||
return deletedLineInfos, hasHunksWithOnlyAddedLines
|
||||
return deletedLineHunks, addedLineHunks
|
||||
}
|
||||
|
||||
// returns the list of commit hashes that introduced the lines which have now been deleted
|
||||
func (self *FixupHelper) blameDeletedLines(deletedLineInfos []*deletedLineInfo) []string {
|
||||
var wg sync.WaitGroup
|
||||
func (self *FixupHelper) blameDeletedLines(deletedLineHunks []*hunk) ([]string, error) {
|
||||
errg := errgroup.Group{}
|
||||
hashChan := make(chan string)
|
||||
|
||||
for _, info := range deletedLineInfos {
|
||||
wg.Add(1)
|
||||
go func(info *deletedLineInfo) {
|
||||
defer wg.Done()
|
||||
|
||||
blameOutput, err := self.c.Git().Blame.BlameLineRange(info.filename, "HEAD", info.startLineIdx, info.numLines)
|
||||
for _, h := range deletedLineHunks {
|
||||
errg.Go(func() error {
|
||||
blameOutput, err := self.c.Git().Blame.BlameLineRange(h.filename, "HEAD", h.startLineIdx, h.numLines)
|
||||
if err != nil {
|
||||
self.c.Log.Errorf("Error blaming file '%s': %v", info.filename, err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
blameLines := strings.Split(strings.TrimSuffix(blameOutput, "\n"), "\n")
|
||||
for _, line := range blameLines {
|
||||
hashChan <- strings.Split(line, " ")[0]
|
||||
}
|
||||
}(info)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
// We don't care about the error here, we'll check it later (in the
|
||||
// return statement below). Here we only wait for all the goroutines to
|
||||
// finish so that we can close the channel.
|
||||
_ = errg.Wait()
|
||||
close(hashChan)
|
||||
}()
|
||||
|
||||
@@ -195,5 +253,92 @@ func (self *FixupHelper) blameDeletedLines(deletedLineInfos []*deletedLineInfo)
|
||||
result.Add(hash)
|
||||
}
|
||||
|
||||
return result.ToSlice()
|
||||
return result.ToSlice(), errg.Wait()
|
||||
}
|
||||
|
||||
func (self *FixupHelper) blameAddedLines(commits []*models.Commit, addedLineHunks []*hunk) ([]string, error) {
|
||||
errg := errgroup.Group{}
|
||||
hashesChan := make(chan []string)
|
||||
|
||||
for _, h := range addedLineHunks {
|
||||
errg.Go(func() error {
|
||||
result := make([]string, 0, 2)
|
||||
|
||||
appendBlamedLine := func(blameOutput string) {
|
||||
blameLines := strings.Split(strings.TrimSuffix(blameOutput, "\n"), "\n")
|
||||
if len(blameLines) == 1 {
|
||||
result = append(result, strings.Split(blameLines[0], " ")[0])
|
||||
}
|
||||
}
|
||||
|
||||
// Blame the line before this hunk, if there is one
|
||||
if h.startLineIdx > 0 {
|
||||
blameOutput, err := self.c.Git().Blame.BlameLineRange(h.filename, "HEAD", h.startLineIdx, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
appendBlamedLine(blameOutput)
|
||||
}
|
||||
|
||||
// Blame the line after this hunk. We don't know how many lines the
|
||||
// file has, so we can't check if there is a line after the hunk;
|
||||
// let the error tell us.
|
||||
blameOutput, err := self.c.Git().Blame.BlameLineRange(h.filename, "HEAD", h.startLineIdx+1, 1)
|
||||
if err != nil {
|
||||
// If this fails, we're probably at the end of the file (we
|
||||
// could have checked this beforehand, but it's expensive). If
|
||||
// there was a line before this hunk, this is fine, we'll just
|
||||
// return that one; if not, the hunk encompasses the entire
|
||||
// file, and we can't blame the lines before and after the hunk.
|
||||
// This is an error.
|
||||
if h.startLineIdx == 0 {
|
||||
return errors.New("Entire file") // TODO i18n
|
||||
}
|
||||
} else {
|
||||
appendBlamedLine(blameOutput)
|
||||
}
|
||||
|
||||
hashesChan <- result
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
go func() {
|
||||
// We don't care about the error here, we'll check it later (in the
|
||||
// return statement below). Here we only wait for all the goroutines to
|
||||
// finish so that we can close the channel.
|
||||
_ = errg.Wait()
|
||||
close(hashesChan)
|
||||
}()
|
||||
|
||||
result := set.New[string]()
|
||||
for hashes := range hashesChan {
|
||||
if len(hashes) == 1 {
|
||||
result.Add(hashes[0])
|
||||
} else if len(hashes) > 1 {
|
||||
if hashes[0] == hashes[1] {
|
||||
result.Add(hashes[0])
|
||||
} else {
|
||||
_, index1, ok1 := self.findCommit(commits, hashes[0])
|
||||
_, index2, ok2 := self.findCommit(commits, hashes[1])
|
||||
if ok1 && ok2 {
|
||||
result.Add(lo.Ternary(index1 < index2, hashes[0], hashes[1]))
|
||||
} else if ok1 {
|
||||
result.Add(hashes[0])
|
||||
} else if ok2 {
|
||||
result.Add(hashes[1])
|
||||
} else {
|
||||
return nil, errors.New(self.c.Tr.NoBaseCommitsFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.ToSlice(), errg.Wait()
|
||||
}
|
||||
|
||||
func (self *FixupHelper) findCommit(commits []*models.Commit, hash string) (*models.Commit, int, bool) {
|
||||
return lo.FindIndexOf(commits, func(commit *models.Commit) bool {
|
||||
return commit.Hash == hash
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFixupHelper_parseDiff(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
diff string
|
||||
expectedDeletedLineHunks []*hunk
|
||||
expectedAddedLineHunks []*hunk
|
||||
}{
|
||||
{
|
||||
name: "no diff",
|
||||
diff: "",
|
||||
expectedDeletedLineHunks: []*hunk{},
|
||||
expectedAddedLineHunks: []*hunk{},
|
||||
},
|
||||
{
|
||||
name: "hunk with only deleted lines",
|
||||
diff: `
|
||||
diff --git a/file1.txt b/file1.txt
|
||||
index 9ce8efb33..aaf2a4666 100644
|
||||
--- a/file1.txt
|
||||
+++ b/file1.txt
|
||||
@@ -3 +2,0 @@ bbb
|
||||
-xxx
|
||||
`,
|
||||
expectedDeletedLineHunks: []*hunk{
|
||||
{
|
||||
filename: "file1.txt",
|
||||
startLineIdx: 3,
|
||||
numLines: 1,
|
||||
},
|
||||
},
|
||||
expectedAddedLineHunks: []*hunk{},
|
||||
},
|
||||
{
|
||||
name: "hunk with deleted and added lines",
|
||||
diff: `
|
||||
diff --git a/file1.txt b/file1.txt
|
||||
index 9ce8efb33..eb246cf98 100644
|
||||
--- a/file1.txt
|
||||
+++ b/file1.txt
|
||||
@@ -3 +3 @@ bbb
|
||||
-xxx
|
||||
+yyy
|
||||
`,
|
||||
expectedDeletedLineHunks: []*hunk{
|
||||
{
|
||||
filename: "file1.txt",
|
||||
startLineIdx: 3,
|
||||
numLines: 1,
|
||||
},
|
||||
},
|
||||
expectedAddedLineHunks: []*hunk{},
|
||||
},
|
||||
{
|
||||
name: "hunk with only added lines",
|
||||
diff: `
|
||||
diff --git a/file1.txt b/file1.txt
|
||||
index 9ce8efb33..fb5e469e7 100644
|
||||
--- a/file1.txt
|
||||
+++ b/file1.txt
|
||||
@@ -4,0 +5,2 @@ ddd
|
||||
+xxx
|
||||
+yyy
|
||||
`,
|
||||
expectedDeletedLineHunks: []*hunk{},
|
||||
expectedAddedLineHunks: []*hunk{
|
||||
{
|
||||
filename: "file1.txt",
|
||||
startLineIdx: 4,
|
||||
numLines: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "several hunks in different files",
|
||||
diff: `
|
||||
diff --git a/file1.txt b/file1.txt
|
||||
index 9ce8efb33..0632e41b0 100644
|
||||
--- a/file1.txt
|
||||
+++ b/file1.txt
|
||||
@@ -2 +1,0 @@ aaa
|
||||
-bbb
|
||||
@@ -4 +3 @@ ccc
|
||||
-ddd
|
||||
+xxx
|
||||
@@ -6,0 +6 @@ fff
|
||||
+zzz
|
||||
diff --git a/file2.txt b/file2.txt
|
||||
index 9ce8efb33..0632e41b0 100644
|
||||
--- a/file2.txt
|
||||
+++ b/file2.txt
|
||||
@@ -0,3 +1,0 @@ aaa
|
||||
-aaa
|
||||
-bbb
|
||||
-ccc
|
||||
`,
|
||||
expectedDeletedLineHunks: []*hunk{
|
||||
{
|
||||
filename: "file1.txt",
|
||||
startLineIdx: 2,
|
||||
numLines: 1,
|
||||
},
|
||||
{
|
||||
filename: "file1.txt",
|
||||
startLineIdx: 4,
|
||||
numLines: 1,
|
||||
},
|
||||
{
|
||||
filename: "file2.txt",
|
||||
startLineIdx: 0,
|
||||
numLines: 3,
|
||||
},
|
||||
},
|
||||
expectedAddedLineHunks: []*hunk{
|
||||
{
|
||||
filename: "file1.txt",
|
||||
startLineIdx: 6,
|
||||
numLines: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
deletedLineHunks, addedLineHunks := parseDiff(s.diff)
|
||||
assert.Equal(t, s.expectedDeletedLineHunks, deletedLineHunks)
|
||||
assert.Equal(t, s.expectedAddedLineHunks, addedLineHunks)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,12 @@ import (
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
"github.com/stefanhaller/git-todo-parser/todo"
|
||||
)
|
||||
|
||||
type MergeAndRebaseHelper struct {
|
||||
@@ -110,7 +112,12 @@ func (self *MergeAndRebaseHelper) genericMergeCommand(command string) error {
|
||||
// we should end up with a command like 'git merge --continue'
|
||||
|
||||
// it's impossible for a rebase to require a commit so we'll use a subprocess only if it's a merge
|
||||
if status == enums.REBASE_MODE_MERGING && command != REBASE_OPTION_ABORT && self.c.UserConfig.Git.Merging.ManualCommit {
|
||||
needsSubprocess := (status == enums.REBASE_MODE_MERGING && command != REBASE_OPTION_ABORT && self.c.UserConfig.Git.Merging.ManualCommit) ||
|
||||
// but we'll also use a subprocess if we have exec todos; those are likely to be lengthy build
|
||||
// tasks whose output the user will want to see in the terminal
|
||||
(status == enums.REBASE_MODE_REBASING && command != REBASE_OPTION_ABORT && self.hasExecTodos())
|
||||
|
||||
if needsSubprocess {
|
||||
// TODO: see if we should be calling more of the code from self.Git.Rebase.GenericMergeOrRebaseAction
|
||||
return self.c.RunSubprocessAndRefresh(
|
||||
self.c.Git().Rebase.GenericMergeOrRebaseActionCmdObj(commandType, command),
|
||||
@@ -123,6 +130,18 @@ func (self *MergeAndRebaseHelper) genericMergeCommand(command string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *MergeAndRebaseHelper) hasExecTodos() bool {
|
||||
for _, commit := range self.c.Model().Commits {
|
||||
if commit.Status != models.StatusRebasing {
|
||||
break
|
||||
}
|
||||
if commit.Action == todo.Exec {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var conflictStrings = []string{
|
||||
"Failed to merge in the changes",
|
||||
"When you have resolved this problem",
|
||||
@@ -234,11 +253,29 @@ func (self *MergeAndRebaseHelper) PromptToContinueRebase() error {
|
||||
}
|
||||
|
||||
func (self *MergeAndRebaseHelper) RebaseOntoRef(ref string) error {
|
||||
checkedOutBranch := self.refsHelper.GetCheckedOutRef().Name
|
||||
checkedOutBranch := self.refsHelper.GetCheckedOutRef()
|
||||
checkedOutBranchName := self.refsHelper.GetCheckedOutRef().Name
|
||||
var disabledReason, baseBranchDisabledReason *types.DisabledReason
|
||||
if checkedOutBranchName == ref {
|
||||
disabledReason = &types.DisabledReason{Text: self.c.Tr.CantRebaseOntoSelf}
|
||||
}
|
||||
|
||||
baseBranch, err := self.c.Git().Loaders.BranchLoader.GetBaseBranch(checkedOutBranch, self.refsHelper.c.Model().MainBranches)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if baseBranch == "" {
|
||||
baseBranch = self.c.Tr.CouldNotDetermineBaseBranch
|
||||
baseBranchDisabledReason = &types.DisabledReason{Text: self.c.Tr.CouldNotDetermineBaseBranch}
|
||||
}
|
||||
|
||||
menuItems := []*types.MenuItem{
|
||||
{
|
||||
Label: self.c.Tr.SimpleRebase,
|
||||
Key: 's',
|
||||
Label: utils.ResolvePlaceholderString(self.c.Tr.SimpleRebase,
|
||||
map[string]string{"ref": ref},
|
||||
),
|
||||
Key: 's',
|
||||
DisabledReason: disabledReason,
|
||||
OnPress: func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.RebaseBranch)
|
||||
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(task gocui.Task) error {
|
||||
@@ -258,9 +295,12 @@ func (self *MergeAndRebaseHelper) RebaseOntoRef(ref string) error {
|
||||
},
|
||||
},
|
||||
{
|
||||
Label: self.c.Tr.InteractiveRebase,
|
||||
Key: 'i',
|
||||
Tooltip: self.c.Tr.InteractiveRebaseTooltip,
|
||||
Label: utils.ResolvePlaceholderString(self.c.Tr.InteractiveRebase,
|
||||
map[string]string{"ref": ref},
|
||||
),
|
||||
Key: 'i',
|
||||
DisabledReason: disabledReason,
|
||||
Tooltip: self.c.Tr.InteractiveRebaseTooltip,
|
||||
OnPress: func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.RebaseBranch)
|
||||
baseCommit := self.c.Modes().MarkedBaseCommit.GetHash()
|
||||
@@ -279,6 +319,31 @@ func (self *MergeAndRebaseHelper) RebaseOntoRef(ref string) error {
|
||||
return self.c.PushContext(self.c.Contexts().LocalCommits)
|
||||
},
|
||||
},
|
||||
{
|
||||
Label: utils.ResolvePlaceholderString(self.c.Tr.RebaseOntoBaseBranch,
|
||||
map[string]string{"baseBranch": ShortBranchName(baseBranch)},
|
||||
),
|
||||
Key: 'b',
|
||||
DisabledReason: baseBranchDisabledReason,
|
||||
Tooltip: self.c.Tr.RebaseOntoBaseBranchTooltip,
|
||||
OnPress: func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.RebaseBranch)
|
||||
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(task gocui.Task) error {
|
||||
baseCommit := self.c.Modes().MarkedBaseCommit.GetHash()
|
||||
var err error
|
||||
if baseCommit != "" {
|
||||
err = self.c.Git().Rebase.RebaseBranchFromBaseCommit(baseBranch, baseCommit)
|
||||
} else {
|
||||
err = self.c.Git().Rebase.RebaseBranch(baseBranch)
|
||||
}
|
||||
err = self.CheckMergeOrRebase(err)
|
||||
if err == nil {
|
||||
return self.ResetMarkedBaseCommit()
|
||||
}
|
||||
return err
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
title := utils.ResolvePlaceholderString(
|
||||
@@ -286,8 +351,7 @@ func (self *MergeAndRebaseHelper) RebaseOntoRef(ref string) error {
|
||||
self.c.Tr.RebasingFromBaseCommitTitle,
|
||||
self.c.Tr.RebasingTitle),
|
||||
map[string]string{
|
||||
"checkedOutBranch": checkedOutBranch,
|
||||
"ref": ref,
|
||||
"checkedOutBranch": checkedOutBranchName,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -127,7 +127,12 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) error {
|
||||
refresh("commits and commit files", self.refreshCommitsAndCommitFiles)
|
||||
|
||||
includeWorktreesWithBranches = scopeSet.Includes(types.WORKTREES)
|
||||
refresh("reflog and branches", func() { self.refreshReflogAndBranches(includeWorktreesWithBranches, options.KeepBranchSelectionIndex) })
|
||||
if self.c.AppState.LocalBranchSortOrder == "recency" {
|
||||
refresh("reflog and branches", func() { self.refreshReflogAndBranches(includeWorktreesWithBranches, options.KeepBranchSelectionIndex) })
|
||||
} else {
|
||||
refresh("branches", func() { self.refreshBranches(includeWorktreesWithBranches, options.KeepBranchSelectionIndex, true) })
|
||||
refresh("reflog", func() { _ = self.refreshReflogCommits() })
|
||||
}
|
||||
} else if scopeSet.Includes(types.REBASE_COMMITS) {
|
||||
// the above block handles rebase commits so we only need to call this one
|
||||
// if we've asked specifically for rebase commits and not those other things
|
||||
@@ -251,7 +256,7 @@ func (self *RefreshHelper) refreshReflogCommitsConsideringStartup() {
|
||||
case types.INITIAL:
|
||||
self.c.OnWorker(func(_ gocui.Task) error {
|
||||
_ = self.refreshReflogCommits()
|
||||
self.refreshBranches(false, true)
|
||||
self.refreshBranches(false, true, true)
|
||||
self.c.State().GetRepoState().SetStartupStage(types.COMPLETE)
|
||||
return nil
|
||||
})
|
||||
@@ -262,9 +267,11 @@ func (self *RefreshHelper) refreshReflogCommitsConsideringStartup() {
|
||||
}
|
||||
|
||||
func (self *RefreshHelper) refreshReflogAndBranches(refreshWorktrees bool, keepBranchSelectionIndex bool) {
|
||||
loadBehindCounts := self.c.State().GetRepoState().GetStartupStage() == types.COMPLETE
|
||||
|
||||
self.refreshReflogCommitsConsideringStartup()
|
||||
|
||||
self.refreshBranches(refreshWorktrees, keepBranchSelectionIndex)
|
||||
self.refreshBranches(refreshWorktrees, keepBranchSelectionIndex, loadBehindCounts)
|
||||
}
|
||||
|
||||
func (self *RefreshHelper) refreshCommitsAndCommitFiles() {
|
||||
@@ -326,6 +333,7 @@ func (self *RefreshHelper) refreshCommitsWithLimit() error {
|
||||
RefName: self.refForLog(),
|
||||
RefForPushedStatus: checkedOutBranchName,
|
||||
All: self.c.Contexts().LocalCommits.GetShowWholeGitGraph(),
|
||||
MainBranches: self.c.Model().MainBranches,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@@ -352,6 +360,7 @@ func (self *RefreshHelper) refreshSubCommitsWithLimit() error {
|
||||
RefName: self.c.Contexts().SubCommits.GetRef().FullRefName(),
|
||||
RefToShowDivergenceFrom: self.c.Contexts().SubCommits.GetRefToShowDivergenceFrom(),
|
||||
RefForPushedStatus: self.c.Contexts().SubCommits.GetRef().FullRefName(),
|
||||
MainBranches: self.c.Model().MainBranches,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@@ -431,7 +440,7 @@ func (self *RefreshHelper) refreshStateSubmoduleConfigs() error {
|
||||
|
||||
// self.refreshStatus is called at the end of this because that's when we can
|
||||
// be sure there is a State.Model.Branches array to pick the current branch from
|
||||
func (self *RefreshHelper) refreshBranches(refreshWorktrees bool, keepBranchSelectionIndex bool) {
|
||||
func (self *RefreshHelper) refreshBranches(refreshWorktrees bool, keepBranchSelectionIndex bool, loadBehindCounts bool) {
|
||||
self.c.Mutexes().RefreshingBranchesMutex.Lock()
|
||||
defer self.c.Mutexes().RefreshingBranchesMutex.Unlock()
|
||||
|
||||
@@ -450,7 +459,25 @@ func (self *RefreshHelper) refreshBranches(refreshWorktrees bool, keepBranchSele
|
||||
}
|
||||
}
|
||||
|
||||
branches, err := self.c.Git().Loaders.BranchLoader.Load(reflogCommits)
|
||||
branches, err := self.c.Git().Loaders.BranchLoader.Load(
|
||||
reflogCommits,
|
||||
self.c.Model().MainBranches,
|
||||
self.c.Model().Branches,
|
||||
loadBehindCounts,
|
||||
func(f func() error) {
|
||||
self.c.OnWorker(func(_ gocui.Task) error {
|
||||
return f()
|
||||
})
|
||||
},
|
||||
func() {
|
||||
self.c.OnUIThread(func() error {
|
||||
if err := self.c.Contexts().Branches.HandleRender(); err != nil {
|
||||
self.c.Log.Error(err)
|
||||
}
|
||||
self.refreshStatus()
|
||||
return nil
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
self.c.Log.Error(err)
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ func (self *SubCommitsHelper) ViewSubCommits(opts ViewSubCommitsOpts) error {
|
||||
RefName: opts.Ref.FullRefName(),
|
||||
RefForPushedStatus: opts.Ref.FullRefName(),
|
||||
RefToShowDivergenceFrom: opts.RefToShowDivergenceFrom,
|
||||
MainBranches: self.c.Model().MainBranches,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -125,8 +125,10 @@ func GetWindowDimensions(args WindowArrangementArgs) map[string]boxlayout.Dimens
|
||||
sideSectionWeight, mainSectionWeight := getMidSectionWeights(args)
|
||||
|
||||
sidePanelsDirection := boxlayout.COLUMN
|
||||
sidePanelsPadding := args.UserConfig.Gui.HorizontalPadding
|
||||
if shouldUsePortraitMode(args) {
|
||||
sidePanelsDirection = boxlayout.ROW
|
||||
sidePanelsPadding = 0
|
||||
}
|
||||
|
||||
showInfoSection := args.UserConfig.Gui.ShowBottomLine ||
|
||||
@@ -143,6 +145,7 @@ func GetWindowDimensions(args WindowArrangementArgs) map[string]boxlayout.Dimens
|
||||
Children: []*boxlayout.Box{
|
||||
{
|
||||
Direction: sidePanelsDirection,
|
||||
Padding: sidePanelsPadding,
|
||||
Weight: 1,
|
||||
Children: []*boxlayout.Box{
|
||||
{
|
||||
@@ -173,13 +176,16 @@ func GetWindowDimensions(args WindowArrangementArgs) map[string]boxlayout.Dimens
|
||||
|
||||
func mainPanelChildren(args WindowArrangementArgs) []*boxlayout.Box {
|
||||
mainPanelsDirection := boxlayout.ROW
|
||||
mainPanelsPadding := 0
|
||||
if splitMainPanelSideBySide(args) {
|
||||
mainPanelsDirection = boxlayout.COLUMN
|
||||
mainPanelsPadding = args.UserConfig.Gui.HorizontalPadding
|
||||
}
|
||||
|
||||
result := []*boxlayout.Box{
|
||||
{
|
||||
Direction: mainPanelsDirection,
|
||||
Padding: mainPanelsPadding,
|
||||
Children: mainSectionChildren(args),
|
||||
Weight: 1,
|
||||
},
|
||||
@@ -216,6 +222,15 @@ func mainSectionChildren(args WindowArrangementArgs) []*boxlayout.Box {
|
||||
}
|
||||
}
|
||||
|
||||
if args.CurrentWindow == "secondary" && args.ScreenMode == types.SCREEN_FULL {
|
||||
return []*boxlayout.Box{
|
||||
{
|
||||
Window: "secondary",
|
||||
Weight: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return []*boxlayout.Box{
|
||||
{
|
||||
Window: "main",
|
||||
@@ -239,7 +254,7 @@ func getMidSectionWeights(args WindowArrangementArgs) (int, int) {
|
||||
mainSectionWeight = 5 // need to shrink side panel to make way for main panels if side-by-side
|
||||
}
|
||||
|
||||
if args.CurrentWindow == "main" {
|
||||
if args.CurrentWindow == "main" || args.CurrentWindow == "secondary" {
|
||||
if args.ScreenMode == types.SCREEN_HALF || args.ScreenMode == types.SCREEN_FULL {
|
||||
sideSectionWeight = 0
|
||||
}
|
||||
@@ -443,7 +458,7 @@ func sidePanelChildren(args WindowArrangementArgs) func(width int, height int) [
|
||||
if accordionMode && defaultBox.Window == args.CurrentSideWindow {
|
||||
return &boxlayout.Box{
|
||||
Window: defaultBox.Window,
|
||||
Weight: 2,
|
||||
Weight: args.UserConfig.Gui.ExpandedSidePanelWeight,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -121,6 +121,87 @@ func TestGetWindowDimensions(t *testing.T) {
|
||||
B: information
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "expandFocusedSidePanel",
|
||||
mutateArgs: func(args *WindowArrangementArgs) {
|
||||
args.UserConfig.Gui.ExpandFocusedSidePanel = true
|
||||
},
|
||||
expected: `
|
||||
╭status─────────────────╮╭main────────────────────────────────────────────╮
|
||||
│ ││ │
|
||||
╰───────────────────────╯│ │
|
||||
╭files──────────────────╮│ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
╰───────────────────────╯│ │
|
||||
╭branches───────────────╮│ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
╰───────────────────────╯│ │
|
||||
╭commits────────────────╮│ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
╰───────────────────────╯│ │
|
||||
╭stash──────────────────╮│ │
|
||||
│ ││ │
|
||||
╰───────────────────────╯╰────────────────────────────────────────────────╯
|
||||
<options──────────────────────────────────────────────────────>A<B────────>
|
||||
A: statusSpacer1
|
||||
B: information
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "expandSidePanelWeight",
|
||||
mutateArgs: func(args *WindowArrangementArgs) {
|
||||
args.UserConfig.Gui.ExpandFocusedSidePanel = true
|
||||
args.UserConfig.Gui.ExpandedSidePanelWeight = 4
|
||||
},
|
||||
expected: `
|
||||
╭status─────────────────╮╭main────────────────────────────────────────────╮
|
||||
│ ││ │
|
||||
╰───────────────────────╯│ │
|
||||
╭files──────────────────╮│ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
╰───────────────────────╯│ │
|
||||
╭branches───────────────╮│ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
╰───────────────────────╯│ │
|
||||
╭commits────────────────╮│ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
╰───────────────────────╯│ │
|
||||
╭stash──────────────────╮│ │
|
||||
│ ││ │
|
||||
╰───────────────────────╯╰────────────────────────────────────────────────╯
|
||||
<options──────────────────────────────────────────────────────>A<B────────>
|
||||
A: statusSpacer1
|
||||
B: information
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "half screen mode, enlargedSideViewLocation left",
|
||||
mutateArgs: func(args *WindowArrangementArgs) {
|
||||
|
||||
@@ -220,9 +220,9 @@ func (self *WorkingTreeHelper) prepareFilesForCommit() error {
|
||||
|
||||
func (self *WorkingTreeHelper) commitPrefixConfigForRepo() *config.CommitPrefixConfig {
|
||||
cfg, ok := self.c.UserConfig.Git.CommitPrefixes[self.c.Git().RepoPaths.RepoName()]
|
||||
if !ok {
|
||||
return nil
|
||||
if ok {
|
||||
return &cfg
|
||||
}
|
||||
|
||||
return &cfg
|
||||
return self.c.UserConfig.Git.CommitPrefix
|
||||
}
|
||||
|
||||
@@ -238,8 +238,8 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Commits.ResetCommitAuthor),
|
||||
Handler: self.withItem(self.amendAttribute),
|
||||
GetDisabledReason: self.require(self.singleItemSelected(self.canAmend)),
|
||||
Handler: self.withItemsRange(self.amendAttribute),
|
||||
GetDisabledReason: self.require(self.itemRangeSelected(self.canAmendRange)),
|
||||
Description: self.c.Tr.AmendCommitAttribute,
|
||||
Tooltip: self.c.Tr.AmendCommitAttributeTooltip,
|
||||
OpensMenu: true,
|
||||
@@ -284,6 +284,9 @@ func (self *LocalCommitsController) GetOnRenderToMain() func() error {
|
||||
map[string]string{
|
||||
"ref": strings.TrimPrefix(commit.Name, "refs/heads/"),
|
||||
}))
|
||||
} else if commit.Action == todo.Exec {
|
||||
task = types.NewRenderStringTask(
|
||||
self.c.Tr.ExecCommandHere + "\n\n" + commit.Name)
|
||||
} else {
|
||||
cmdObj := self.c.Git().Commit.ShowCmdObj(commit.Hash, self.c.Modes().Filtering.GetPath())
|
||||
task = types.NewRunPtyTask(cmdObj.GetCmd())
|
||||
@@ -371,7 +374,7 @@ func (self *LocalCommitsController) reword(commit *models.Commit) error {
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) switchFromCommitMessagePanelToEditor(filepath string) error {
|
||||
if self.isHeadCommit() {
|
||||
if self.isSelectedHeadCommit() {
|
||||
return self.c.RunSubprocessAndRefresh(
|
||||
self.c.Git().Commit.RewordLastCommitInEditorWithMessageFileCmdObj(filepath))
|
||||
}
|
||||
@@ -408,7 +411,7 @@ func (self *LocalCommitsController) handleReword(summary string, description str
|
||||
func (self *LocalCommitsController) doRewordEditor() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.RewordCommit)
|
||||
|
||||
if self.isHeadCommit() {
|
||||
if self.isSelectedHeadCommit() {
|
||||
return self.c.RunSubprocessAndRefresh(self.c.Git().Commit.RewordLastCommitInEditorCmdObj())
|
||||
}
|
||||
|
||||
@@ -607,7 +610,7 @@ func (self *LocalCommitsController) rewordEnabled(commit *models.Commit) *types.
|
||||
|
||||
// If we are in a rebase, the only action that is allowed for
|
||||
// non-todo commits is rewording the current head commit
|
||||
if self.isRebasing() && !self.isHeadCommit() {
|
||||
if self.isRebasing() && !self.isSelectedHeadCommit() {
|
||||
return &types.DisabledReason{Text: self.c.Tr.AlreadyRebasing}
|
||||
}
|
||||
|
||||
@@ -665,7 +668,7 @@ func (self *LocalCommitsController) moveUp(selectedCommits []*models.Commit, sta
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) amendTo(commit *models.Commit) error {
|
||||
if self.isHeadCommit() {
|
||||
if self.isSelectedHeadCommit() {
|
||||
return self.c.Confirm(types.ConfirmOpts{
|
||||
Title: self.c.Tr.AmendCommitTitle,
|
||||
Prompt: self.c.Tr.AmendCommitPrompt,
|
||||
@@ -695,34 +698,39 @@ func (self *LocalCommitsController) amendTo(commit *models.Commit) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) canAmend(commit *models.Commit) *types.DisabledReason {
|
||||
if !self.isHeadCommit() && self.isRebasing() {
|
||||
func (self *LocalCommitsController) canAmendRange(commits []*models.Commit, start, end int) *types.DisabledReason {
|
||||
if (start != end || !self.isHeadCommit(start)) && self.isRebasing() {
|
||||
return &types.DisabledReason{Text: self.c.Tr.AlreadyRebasing}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) amendAttribute(commit *models.Commit) error {
|
||||
func (self *LocalCommitsController) canAmend(_ *models.Commit) *types.DisabledReason {
|
||||
idx := self.context().GetSelectedLineIdx()
|
||||
return self.canAmendRange(self.c.Model().Commits, idx, idx)
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) amendAttribute(commits []*models.Commit, start, end int) error {
|
||||
opts := self.c.KeybindingsOpts()
|
||||
return self.c.Menu(types.CreateMenuOptions{
|
||||
Title: "Amend commit attribute",
|
||||
Items: []*types.MenuItem{
|
||||
{
|
||||
Label: self.c.Tr.ResetAuthor,
|
||||
OnPress: self.resetAuthor,
|
||||
OnPress: func() error { return self.resetAuthor(start, end) },
|
||||
Key: opts.GetKey(opts.Config.AmendAttribute.ResetAuthor),
|
||||
Tooltip: self.c.Tr.ResetAuthorTooltip,
|
||||
},
|
||||
{
|
||||
Label: self.c.Tr.SetAuthor,
|
||||
OnPress: self.setAuthor,
|
||||
OnPress: func() error { return self.setAuthor(start, end) },
|
||||
Key: opts.GetKey(opts.Config.AmendAttribute.SetAuthor),
|
||||
Tooltip: self.c.Tr.SetAuthorTooltip,
|
||||
},
|
||||
{
|
||||
Label: self.c.Tr.AddCoAuthor,
|
||||
OnPress: self.addCoAuthor,
|
||||
OnPress: func() error { return self.addCoAuthor(start, end) },
|
||||
Key: opts.GetKey(opts.Config.AmendAttribute.AddCoAuthor),
|
||||
Tooltip: self.c.Tr.AddCoAuthorTooltip,
|
||||
},
|
||||
@@ -730,10 +738,10 @@ func (self *LocalCommitsController) amendAttribute(commit *models.Commit) error
|
||||
})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) resetAuthor() error {
|
||||
func (self *LocalCommitsController) resetAuthor(start, end int) error {
|
||||
return self.c.WithWaitingStatus(self.c.Tr.AmendingStatus, func(gocui.Task) error {
|
||||
self.c.LogAction(self.c.Tr.Actions.ResetCommitAuthor)
|
||||
if err := self.c.Git().Rebase.ResetCommitAuthor(self.c.Model().Commits, self.context().GetSelectedLineIdx()); err != nil {
|
||||
if err := self.c.Git().Rebase.ResetCommitAuthor(self.c.Model().Commits, start, end); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -741,14 +749,14 @@ func (self *LocalCommitsController) resetAuthor() error {
|
||||
})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) setAuthor() error {
|
||||
func (self *LocalCommitsController) setAuthor(start, end int) error {
|
||||
return self.c.Prompt(types.PromptOpts{
|
||||
Title: self.c.Tr.SetAuthorPromptTitle,
|
||||
FindSuggestionsFunc: self.c.Helpers().Suggestions.GetAuthorsSuggestionsFunc(),
|
||||
HandleConfirm: func(value string) error {
|
||||
return self.c.WithWaitingStatus(self.c.Tr.AmendingStatus, func(gocui.Task) error {
|
||||
self.c.LogAction(self.c.Tr.Actions.SetCommitAuthor)
|
||||
if err := self.c.Git().Rebase.SetCommitAuthor(self.c.Model().Commits, self.context().GetSelectedLineIdx(), value); err != nil {
|
||||
if err := self.c.Git().Rebase.SetCommitAuthor(self.c.Model().Commits, start, end, value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -758,14 +766,14 @@ func (self *LocalCommitsController) setAuthor() error {
|
||||
})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) addCoAuthor() error {
|
||||
func (self *LocalCommitsController) addCoAuthor(start, end int) error {
|
||||
return self.c.Prompt(types.PromptOpts{
|
||||
Title: self.c.Tr.AddCoAuthorPromptTitle,
|
||||
FindSuggestionsFunc: self.c.Helpers().Suggestions.GetAuthorsSuggestionsFunc(),
|
||||
HandleConfirm: func(value string) error {
|
||||
return self.c.WithWaitingStatus(self.c.Tr.AmendingStatus, func(gocui.Task) error {
|
||||
self.c.LogAction(self.c.Tr.Actions.AddCommitCoAuthor)
|
||||
if err := self.c.Git().Rebase.AddCommitCoAuthor(self.c.Model().Commits, self.context().GetSelectedLineIdx(), value); err != nil {
|
||||
if err := self.c.Git().Rebase.AddCommitCoAuthor(self.c.Model().Commits, start, end, value); err != nil {
|
||||
return err
|
||||
}
|
||||
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
|
||||
@@ -801,7 +809,6 @@ func (self *LocalCommitsController) revert(commit *models.Commit) error {
|
||||
func (self *LocalCommitsController) createRevertMergeCommitMenu(commit *models.Commit) error {
|
||||
menuItems := make([]*types.MenuItem, len(commit.Parents))
|
||||
for i, parentHash := range commit.Parents {
|
||||
i := i
|
||||
message, err := self.c.Git().Commit.GetCommitMessageFirstLine(parentHash)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1189,8 +1196,12 @@ func (self *LocalCommitsController) markAsBaseCommit(commit *models.Commit) erro
|
||||
return self.c.PostRefreshUpdate(self.c.Contexts().LocalCommits)
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) isHeadCommit() bool {
|
||||
return models.IsHeadCommit(self.c.Model().Commits, self.context().GetSelectedLineIdx())
|
||||
func (self *LocalCommitsController) isHeadCommit(idx int) bool {
|
||||
return models.IsHeadCommit(self.c.Model().Commits, idx)
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) isSelectedHeadCommit() bool {
|
||||
return self.isHeadCommit(self.context().GetSelectedLineIdx())
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) notMidRebase(message string) func() *types.DisabledReason {
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
@@ -75,6 +76,11 @@ func (self *StatusController) GetMouseKeybindings(opts types.KeybindingsOpts) []
|
||||
Key: gocui.MouseLeft,
|
||||
Handler: self.onClickMain,
|
||||
},
|
||||
{
|
||||
ViewName: self.Context().GetViewName(),
|
||||
Key: gocui.MouseLeft,
|
||||
Handler: self.onClick,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,15 +101,11 @@ func (self *StatusController) GetOnRenderToMain() func() error {
|
||||
}
|
||||
}
|
||||
|
||||
func (self *StatusController) GetOnClick() func() error {
|
||||
return self.onClick
|
||||
}
|
||||
|
||||
func (self *StatusController) Context() types.Context {
|
||||
return self.c.Contexts().Status
|
||||
}
|
||||
|
||||
func (self *StatusController) onClick() error {
|
||||
func (self *StatusController) onClick(opts gocui.ViewMouseBindingOpts) error {
|
||||
// TODO: move into some abstraction (status is currently not a listViewContext where a lot of this code lives)
|
||||
currentBranch := self.c.Helpers().Refs.GetCheckedOutRef()
|
||||
if currentBranch == nil {
|
||||
@@ -115,21 +117,20 @@ func (self *StatusController) onClick() error {
|
||||
return err
|
||||
}
|
||||
|
||||
cx, _ := self.c.Views().Status.Cursor()
|
||||
upstreamStatus := presentation.BranchStatus(currentBranch, types.ItemOperationNone, self.c.Tr, time.Now(), self.c.UserConfig)
|
||||
upstreamStatus := utils.Decolorise(presentation.BranchStatus(currentBranch, types.ItemOperationNone, self.c.Tr, time.Now(), self.c.UserConfig))
|
||||
repoName := self.c.Git().RepoPaths.RepoName()
|
||||
workingTreeState := self.c.Git().Status.WorkingTreeState()
|
||||
switch workingTreeState {
|
||||
case enums.REBASE_MODE_REBASING, enums.REBASE_MODE_MERGING:
|
||||
workingTreeStatus := fmt.Sprintf("(%s)", presentation.FormatWorkingTreeStateLower(self.c.Tr, workingTreeState))
|
||||
if cursorInSubstring(cx, upstreamStatus+" ", workingTreeStatus) {
|
||||
if cursorInSubstring(opts.X, upstreamStatus+" ", workingTreeStatus) {
|
||||
return self.c.Helpers().MergeAndRebase.CreateRebaseOptionsMenu()
|
||||
}
|
||||
if cursorInSubstring(cx, upstreamStatus+" "+workingTreeStatus+" ", repoName) {
|
||||
if cursorInSubstring(opts.X, upstreamStatus+" "+workingTreeStatus+" ", repoName) {
|
||||
return self.c.Helpers().Repos.CreateRecentReposMenu()
|
||||
}
|
||||
default:
|
||||
if cursorInSubstring(cx, upstreamStatus+" ", repoName) {
|
||||
if cursorInSubstring(opts.X, upstreamStatus+" ", repoName) {
|
||||
return self.c.Helpers().Repos.CreateRecentReposMenu()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,8 +40,32 @@ func (self *SuggestionsController) GetKeybindings(opts types.KeybindingsOpts) []
|
||||
Handler: func() error { return self.context().State.OnClose() },
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Universal.TogglePanel),
|
||||
Handler: func() error { return self.c.ReplaceContext(self.c.Contexts().Confirmation) },
|
||||
Key: opts.GetKey(opts.Config.Universal.TogglePanel),
|
||||
Handler: func() error {
|
||||
self.c.Views().Suggestions.Subtitle = ""
|
||||
return self.c.ReplaceContext(self.c.Contexts().Confirmation)
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Universal.Remove),
|
||||
Handler: func() error {
|
||||
return self.context().State.OnDeleteSuggestion()
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Universal.Edit),
|
||||
Handler: func() error {
|
||||
if self.context().State.AllowEditSuggestion {
|
||||
if selectedItem := self.c.Contexts().Suggestions.GetSelected(); selectedItem != nil {
|
||||
self.c.Contexts().Confirmation.GetView().TextArea.Clear()
|
||||
self.c.Contexts().Confirmation.GetView().TextArea.TypeString(selectedItem.Value)
|
||||
self.c.Contexts().Confirmation.GetView().RenderTextArea()
|
||||
self.c.Contexts().Suggestions.RefreshSuggestions()
|
||||
return self.c.ReplaceContext(self.c.Contexts().Confirmation)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -87,10 +87,10 @@ func (self *SyncController) branchCheckedOut(f func(*models.Branch) error) func(
|
||||
}
|
||||
|
||||
func (self *SyncController) push(currentBranch *models.Branch) error {
|
||||
// if we have pullables we'll ask if the user wants to force push
|
||||
// if we are behind our upstream branch we'll ask if the user wants to force push
|
||||
if currentBranch.IsTrackingRemote() {
|
||||
opts := pushOpts{}
|
||||
if currentBranch.HasCommitsToPull() {
|
||||
opts := pushOpts{remoteBranchStoredLocally: currentBranch.RemoteBranchStoredLocally()}
|
||||
if currentBranch.IsBehindForPush() {
|
||||
return self.requestToForcePush(currentBranch, opts)
|
||||
} else {
|
||||
return self.pushAux(currentBranch, opts)
|
||||
@@ -180,9 +180,16 @@ func (self *SyncController) pullWithLock(task gocui.Task, opts PullFilesOptions)
|
||||
|
||||
type pushOpts struct {
|
||||
force bool
|
||||
forceWithLease bool
|
||||
upstreamRemote string
|
||||
upstreamBranch string
|
||||
setUpstream bool
|
||||
|
||||
// If this is false, we can't tell ahead of time whether a force-push will
|
||||
// be necessary, so we start with a normal push and offer to force-push if
|
||||
// the server rejected. If this is true, we don't offer to force-push if the
|
||||
// server rejected, but rather ask the user to fetch.
|
||||
remoteBranchStoredLocally bool
|
||||
}
|
||||
|
||||
func (self *SyncController) pushAux(currentBranch *models.Branch, opts pushOpts) error {
|
||||
@@ -192,13 +199,32 @@ func (self *SyncController) pushAux(currentBranch *models.Branch, opts pushOpts)
|
||||
task,
|
||||
git_commands.PushOpts{
|
||||
Force: opts.force,
|
||||
ForceWithLease: opts.forceWithLease,
|
||||
UpstreamRemote: opts.upstreamRemote,
|
||||
UpstreamBranch: opts.upstreamBranch,
|
||||
SetUpstream: opts.setUpstream,
|
||||
})
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "Updates were rejected") {
|
||||
return errors.New(self.c.Tr.UpdatesRejected)
|
||||
if !opts.force && !opts.forceWithLease && strings.Contains(err.Error(), "Updates were rejected") {
|
||||
if opts.remoteBranchStoredLocally {
|
||||
return errors.New(self.c.Tr.UpdatesRejected)
|
||||
}
|
||||
|
||||
forcePushDisabled := self.c.UserConfig.Git.DisableForcePushing
|
||||
if forcePushDisabled {
|
||||
return errors.New(self.c.Tr.UpdatesRejectedAndForcePushDisabled)
|
||||
}
|
||||
_ = self.c.Confirm(types.ConfirmOpts{
|
||||
Title: self.c.Tr.ForcePush,
|
||||
Prompt: self.forcePushPrompt(),
|
||||
HandleConfirm: func() error {
|
||||
newOpts := opts
|
||||
newOpts.force = true
|
||||
|
||||
return self.pushAux(currentBranch, newOpts)
|
||||
},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -216,7 +242,7 @@ func (self *SyncController) requestToForcePush(currentBranch *models.Branch, opt
|
||||
Title: self.c.Tr.ForcePush,
|
||||
Prompt: self.forcePushPrompt(),
|
||||
HandleConfirm: func() error {
|
||||
opts.force = true
|
||||
opts.forceWithLease = true
|
||||
return self.pushAux(currentBranch, opts)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
// we then do the reverse of what that reflog describes.
|
||||
// When we do this, we create a new reflog entry, and tag it as either an undo or redo
|
||||
// Then, next time we want to undo, we'll use those entries to know which user-initiated
|
||||
// actions we can skip. E.g. if I do do three things, A, B, and C, and hit undo twice,
|
||||
// actions we can skip. E.g. if I do three things, A, B, and C, and hit undo twice,
|
||||
// the reflog will read UUCBA, and when I read the first two undos, I know to skip the following
|
||||
// two user actions, meaning we end up undoing reflog entry C. Redoing works in a similar way.
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ func TestBuildTreeFromFiles(t *testing.T) {
|
||||
expected: &Node[models.File]{
|
||||
Path: "",
|
||||
// it is a little strange that we're not bubbling up our merge conflict
|
||||
// here but we are technically still in in tree mode and that's the rule
|
||||
// here but we are technically still in tree mode and that's the rule
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Name: "a"},
|
||||
@@ -147,7 +147,6 @@ func TestBuildTreeFromFiles(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
result := BuildTreeFromFiles(s.files)
|
||||
assert.EqualValues(t, s.expected, result)
|
||||
@@ -306,7 +305,6 @@ func TestBuildFlatTreeFromFiles(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
result := BuildFlatTreeFromFiles(s.files)
|
||||
assert.EqualValues(t, s.expected, result)
|
||||
@@ -420,7 +418,6 @@ func TestBuildTreeFromCommitFiles(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
result := BuildTreeFromCommitFiles(s.files)
|
||||
assert.EqualValues(t, s.expected, result)
|
||||
@@ -521,7 +518,6 @@ func TestBuildFlatTreeFromCommitFiles(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
result := BuildFlatTreeFromCommitFiles(s.files)
|
||||
assert.EqualValues(t, s.expected, result)
|
||||
|
||||
@@ -125,7 +125,6 @@ func TestCompress(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
s.root.Compress()
|
||||
assert.EqualValues(t, s.expected, s.root)
|
||||
@@ -155,7 +154,6 @@ func TestGetFile(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
assert.EqualValues(t, s.expected, s.viewModel.GetFile(s.path))
|
||||
})
|
||||
|
||||
@@ -71,7 +71,6 @@ func TestFilterAction(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
mngr := &FileTree{getFiles: func() []*models.File { return s.files }, filter: s.filter}
|
||||
result := mngr.getFilesForDisplay()
|
||||
|
||||
+4
-10
@@ -342,15 +342,8 @@ func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, contextKey types.Context
|
||||
return nil
|
||||
}
|
||||
|
||||
// resetState determines if we pull the repo state from our repo state map or
|
||||
// just re-initialize it. For now we're only re-using state when we're going
|
||||
// in and out of submodules, for the sake of having the cursor back on the submodule
|
||||
// when we return.
|
||||
//
|
||||
// I tried out always reverting to the repo's original state but found that in fact
|
||||
// it gets a bit confusing to land back in the status panel when visiting a repo
|
||||
// you've already switched from. There's no doubt some easy way to make the UX
|
||||
// optimal for all cases but I'm too lazy to think about what that is right now
|
||||
// resetState reuses the repo state from our repo state map, if the repo was
|
||||
// open before; otherwise it creates a new one.
|
||||
func (gui *Gui) resetState(startArgs appTypes.StartArgs) types.Context {
|
||||
worktreePath := gui.git.RepoPaths.WorktreePath()
|
||||
|
||||
@@ -386,6 +379,7 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs) types.Context {
|
||||
BisectInfo: git_commands.NewNullBisectInfo(),
|
||||
FilesTrie: patricia.NewTrie(),
|
||||
Authors: map[string]*models.Author{},
|
||||
MainBranches: git_commands.NewMainBranches(gui.UserConfig.Git.MainBranches, gui.os.Cmd),
|
||||
},
|
||||
Modes: &types.Modes{
|
||||
Filtering: filtering.New(startArgs.FilterPath, ""),
|
||||
@@ -662,7 +656,7 @@ func (gui *Gui) Run(startArgs appTypes.StartArgs) error {
|
||||
// disable deadlock reporting if we're not running in debug mode, or if
|
||||
// we're debugging an integration test. In this latter case, stopping at
|
||||
// breakpoints and stepping through code can easily take more than 30s.
|
||||
deadlock.Opts.Disable = !gui.Debug || os.Getenv(components.WAIT_FOR_DEBUGGER_ENV_VAR) == ""
|
||||
deadlock.Opts.Disable = !gui.Debug || os.Getenv(components.WAIT_FOR_DEBUGGER_ENV_VAR) != ""
|
||||
|
||||
if err := gui.Config.ReloadUserConfig(); err != nil {
|
||||
return nil
|
||||
|
||||
@@ -21,6 +21,7 @@ type GuiDriver struct {
|
||||
gui *Gui
|
||||
isIdleChan chan struct{}
|
||||
toastChan chan string
|
||||
headless bool
|
||||
}
|
||||
|
||||
var _ integrationTypes.GuiDriver = &GuiDriver{}
|
||||
@@ -161,3 +162,7 @@ func (self *GuiDriver) NextToast() *string {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (self *GuiDriver) Headless() bool {
|
||||
return self.headless
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ func FileHasConflictMarkers(path string) (bool, error) {
|
||||
// Efficiently scans through a file looking for merge conflict markers. Returns true if it does
|
||||
func fileHasConflictMarkersAux(file io.Reader) bool {
|
||||
scanner := bufio.NewScanner(file)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
scanner.Split(utils.ScanLinesAndTruncateWhenLongerThanBuffer(bufio.MaxScanTokenSize))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Bytes()
|
||||
|
||||
|
||||
@@ -115,7 +115,6 @@ baz
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
assert.EqualValues(t, s.expected, findConflicts(s.content))
|
||||
})
|
||||
|
||||
@@ -122,7 +122,6 @@ func TestNewOrigin(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
assert.EqualValues(t, s.expected, calculateOrigin(s.origin, s.bufferHeight, s.numLines, s.firstLineIdx, s.lastLineIdx, s.selectedLineIdx, s.selectMode))
|
||||
})
|
||||
|
||||
@@ -104,13 +104,15 @@ func (self *PopupHandler) Confirm(opts types.ConfirmOpts) error {
|
||||
|
||||
func (self *PopupHandler) Prompt(opts types.PromptOpts) error {
|
||||
return self.createPopupPanelFn(context.Background(), types.CreatePopupPanelOpts{
|
||||
Title: opts.Title,
|
||||
Prompt: opts.InitialContent,
|
||||
Editable: true,
|
||||
HandleConfirmPrompt: opts.HandleConfirm,
|
||||
HandleClose: opts.HandleClose,
|
||||
FindSuggestionsFunc: opts.FindSuggestionsFunc,
|
||||
Mask: opts.Mask,
|
||||
Title: opts.Title,
|
||||
Prompt: opts.InitialContent,
|
||||
Editable: true,
|
||||
HandleConfirmPrompt: opts.HandleConfirm,
|
||||
HandleClose: opts.HandleClose,
|
||||
HandleDeleteSuggestion: opts.HandleDeleteSuggestion,
|
||||
FindSuggestionsFunc: opts.FindSuggestionsFunc,
|
||||
AllowEditSuggestion: opts.AllowEditSuggestion,
|
||||
Mask: opts.Mask,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ func getBranchDisplayStrings(
|
||||
// Recency is always three characters, plus one for the space
|
||||
availableWidth := viewWidth - 4
|
||||
if len(branchStatus) > 0 {
|
||||
availableWidth -= runewidth.StringWidth(branchStatus) + 1
|
||||
availableWidth -= runewidth.StringWidth(utils.Decolorise(branchStatus)) + 1
|
||||
}
|
||||
if icons.IsIconEnabled() {
|
||||
availableWidth -= 2 // one for the icon, one for the space
|
||||
@@ -89,8 +89,7 @@ func getBranchDisplayStrings(
|
||||
coloredName = fmt.Sprintf("%s %s", coloredName, style.FgDefault.Sprint(worktreeIcon))
|
||||
}
|
||||
if len(branchStatus) > 0 {
|
||||
coloredStatus := branchStatusColor(b, itemOperation).Sprint(branchStatus)
|
||||
coloredName = fmt.Sprintf("%s %s", coloredName, coloredStatus)
|
||||
coloredName = fmt.Sprintf("%s %s", coloredName, branchStatus)
|
||||
}
|
||||
|
||||
recencyColor := style.FgCyan
|
||||
@@ -144,30 +143,6 @@ func GetBranchTextStyle(name string) style.TextStyle {
|
||||
}
|
||||
}
|
||||
|
||||
func branchStatusColor(branch *models.Branch, itemOperation types.ItemOperation) style.TextStyle {
|
||||
colour := style.FgYellow
|
||||
if itemOperation != types.ItemOperationNone {
|
||||
colour = style.FgCyan
|
||||
} else if branch.UpstreamGone {
|
||||
colour = style.FgRed
|
||||
} else if branch.MatchesUpstream() {
|
||||
colour = style.FgGreen
|
||||
} else if branch.RemoteBranchNotStoredLocally() {
|
||||
colour = style.FgMagenta
|
||||
}
|
||||
|
||||
return colour
|
||||
}
|
||||
|
||||
func ColoredBranchStatus(
|
||||
branch *models.Branch,
|
||||
itemOperation types.ItemOperation,
|
||||
tr *i18n.TranslationSet,
|
||||
userConfig *config.UserConfig,
|
||||
) string {
|
||||
return branchStatusColor(branch, itemOperation).Sprint(BranchStatus(branch, itemOperation, tr, time.Now(), userConfig))
|
||||
}
|
||||
|
||||
func BranchStatus(
|
||||
branch *models.Branch,
|
||||
itemOperation types.ItemOperation,
|
||||
@@ -177,30 +152,38 @@ func BranchStatus(
|
||||
) string {
|
||||
itemOperationStr := ItemOperationToString(itemOperation, tr)
|
||||
if itemOperationStr != "" {
|
||||
return itemOperationStr + " " + utils.Loader(now, userConfig.Gui.Spinner)
|
||||
}
|
||||
|
||||
if !branch.IsTrackingRemote() {
|
||||
return ""
|
||||
}
|
||||
|
||||
if branch.UpstreamGone {
|
||||
return tr.UpstreamGone
|
||||
}
|
||||
|
||||
if branch.MatchesUpstream() {
|
||||
return "✓"
|
||||
}
|
||||
if branch.RemoteBranchNotStoredLocally() {
|
||||
return "?"
|
||||
return style.FgCyan.Sprintf("%s %s", itemOperationStr, utils.Loader(now, userConfig.Gui.Spinner))
|
||||
}
|
||||
|
||||
result := ""
|
||||
if branch.HasCommitsToPush() {
|
||||
result = fmt.Sprintf("↑%s", branch.Pushables)
|
||||
if branch.IsTrackingRemote() {
|
||||
if branch.UpstreamGone {
|
||||
result = style.FgRed.Sprint(tr.UpstreamGone)
|
||||
} else if branch.MatchesUpstream() {
|
||||
result = style.FgGreen.Sprint("✓")
|
||||
} else if branch.RemoteBranchNotStoredLocally() {
|
||||
result = style.FgMagenta.Sprint("?")
|
||||
} else if branch.IsBehindForPull() && branch.IsAheadForPull() {
|
||||
result = style.FgYellow.Sprintf("↓%s↑%s", branch.BehindForPull, branch.AheadForPull)
|
||||
} else if branch.IsBehindForPull() {
|
||||
result = style.FgYellow.Sprintf("↓%s", branch.BehindForPull)
|
||||
} else if branch.IsAheadForPull() {
|
||||
result = style.FgYellow.Sprintf("↑%s", branch.AheadForPull)
|
||||
}
|
||||
}
|
||||
if branch.HasCommitsToPull() {
|
||||
result = fmt.Sprintf("%s↓%s", result, branch.Pullables)
|
||||
|
||||
if userConfig.Gui.ShowDivergenceFromBaseBranch != "none" {
|
||||
behind := branch.BehindBaseBranch.Load()
|
||||
if behind != 0 {
|
||||
if result != "" {
|
||||
result += " "
|
||||
}
|
||||
if userConfig.Gui.ShowDivergenceFromBaseBranch == "arrowAndNumber" {
|
||||
result += style.FgCyan.Sprintf("↓%d", behind)
|
||||
} else {
|
||||
result += style.FgCyan.Sprintf("↓")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
@@ -2,6 +2,7 @@ package presentation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -15,6 +16,11 @@ import (
|
||||
"github.com/xo/terminfo"
|
||||
)
|
||||
|
||||
func makeAtomic(v int32) (result atomic.Int32) {
|
||||
result.Store(v)
|
||||
return //nolint: nakedret
|
||||
}
|
||||
|
||||
func Test_getBranchDisplayStrings(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
branch *models.Branch
|
||||
@@ -23,6 +29,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
||||
viewWidth int
|
||||
useIcons bool
|
||||
checkedOutByWorktree bool
|
||||
showDivergenceCfg string
|
||||
expected []string
|
||||
}{
|
||||
// First some tests for when the view is wide enough so that everything fits:
|
||||
@@ -33,6 +40,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
||||
viewWidth: 100,
|
||||
useIcons: false,
|
||||
checkedOutByWorktree: false,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "branch_name"},
|
||||
},
|
||||
{
|
||||
@@ -42,6 +50,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
||||
viewWidth: 100,
|
||||
useIcons: false,
|
||||
checkedOutByWorktree: true,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "branch_name (worktree)"},
|
||||
},
|
||||
{
|
||||
@@ -51,6 +60,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
||||
viewWidth: 100,
|
||||
useIcons: true,
|
||||
checkedOutByWorktree: true,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "", "branch_name "},
|
||||
},
|
||||
{
|
||||
@@ -58,14 +68,15 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
||||
Name: "branch_name",
|
||||
Recency: "1m",
|
||||
UpstreamRemote: "origin",
|
||||
Pushables: "0",
|
||||
Pullables: "0",
|
||||
AheadForPull: "0",
|
||||
BehindForPull: "0",
|
||||
},
|
||||
itemOperation: types.ItemOperationNone,
|
||||
fullDescription: false,
|
||||
viewWidth: 100,
|
||||
useIcons: false,
|
||||
checkedOutByWorktree: false,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "branch_name ✓"},
|
||||
},
|
||||
{
|
||||
@@ -73,15 +84,64 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
||||
Name: "branch_name",
|
||||
Recency: "1m",
|
||||
UpstreamRemote: "origin",
|
||||
Pushables: "3",
|
||||
Pullables: "5",
|
||||
AheadForPull: "3",
|
||||
BehindForPull: "5",
|
||||
},
|
||||
itemOperation: types.ItemOperationNone,
|
||||
fullDescription: false,
|
||||
viewWidth: 100,
|
||||
useIcons: false,
|
||||
checkedOutByWorktree: true,
|
||||
expected: []string{"1m", "branch_name (worktree) ↑3↓5"},
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "branch_name (worktree) ↓5↑3"},
|
||||
},
|
||||
{
|
||||
branch: &models.Branch{
|
||||
Name: "branch_name",
|
||||
Recency: "1m",
|
||||
BehindBaseBranch: makeAtomic(2),
|
||||
},
|
||||
itemOperation: types.ItemOperationNone,
|
||||
fullDescription: false,
|
||||
viewWidth: 100,
|
||||
useIcons: false,
|
||||
checkedOutByWorktree: false,
|
||||
showDivergenceCfg: "onlyArrow",
|
||||
expected: []string{"1m", "branch_name ↓"},
|
||||
},
|
||||
{
|
||||
branch: &models.Branch{
|
||||
Name: "branch_name",
|
||||
Recency: "1m",
|
||||
UpstreamRemote: "origin",
|
||||
AheadForPull: "0",
|
||||
BehindForPull: "0",
|
||||
BehindBaseBranch: makeAtomic(2),
|
||||
},
|
||||
itemOperation: types.ItemOperationNone,
|
||||
fullDescription: false,
|
||||
viewWidth: 100,
|
||||
useIcons: false,
|
||||
checkedOutByWorktree: false,
|
||||
showDivergenceCfg: "arrowAndNumber",
|
||||
expected: []string{"1m", "branch_name ✓ ↓2"},
|
||||
},
|
||||
{
|
||||
branch: &models.Branch{
|
||||
Name: "branch_name",
|
||||
Recency: "1m",
|
||||
UpstreamRemote: "origin",
|
||||
AheadForPull: "3",
|
||||
BehindForPull: "5",
|
||||
BehindBaseBranch: makeAtomic(2),
|
||||
},
|
||||
itemOperation: types.ItemOperationNone,
|
||||
fullDescription: false,
|
||||
viewWidth: 100,
|
||||
useIcons: false,
|
||||
checkedOutByWorktree: false,
|
||||
showDivergenceCfg: "arrowAndNumber",
|
||||
expected: []string{"1m", "branch_name ↓5↑3 ↓2"},
|
||||
},
|
||||
{
|
||||
branch: &models.Branch{Name: "branch_name", Recency: "1m"},
|
||||
@@ -90,6 +150,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
||||
viewWidth: 100,
|
||||
useIcons: false,
|
||||
checkedOutByWorktree: false,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "branch_name Pushing |"},
|
||||
},
|
||||
{
|
||||
@@ -99,8 +160,8 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
||||
CommitHash: "1234567890",
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "branch_name",
|
||||
Pushables: "0",
|
||||
Pullables: "0",
|
||||
AheadForPull: "0",
|
||||
BehindForPull: "0",
|
||||
Subject: "commit title",
|
||||
},
|
||||
itemOperation: types.ItemOperationNone,
|
||||
@@ -108,6 +169,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
||||
viewWidth: 100,
|
||||
useIcons: false,
|
||||
checkedOutByWorktree: false,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "12345678", "branch_name ✓", "origin branch_name", "commit title"},
|
||||
},
|
||||
|
||||
@@ -119,6 +181,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
||||
viewWidth: 14,
|
||||
useIcons: false,
|
||||
checkedOutByWorktree: false,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "branch_na…"},
|
||||
},
|
||||
{
|
||||
@@ -128,6 +191,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
||||
viewWidth: 14,
|
||||
useIcons: false,
|
||||
checkedOutByWorktree: true,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "bra… (worktree)"},
|
||||
},
|
||||
{
|
||||
@@ -137,6 +201,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
||||
viewWidth: 14,
|
||||
useIcons: true,
|
||||
checkedOutByWorktree: true,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "", "branc… "},
|
||||
},
|
||||
{
|
||||
@@ -144,14 +209,15 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
||||
Name: "branch_name",
|
||||
Recency: "1m",
|
||||
UpstreamRemote: "origin",
|
||||
Pushables: "0",
|
||||
Pullables: "0",
|
||||
AheadForPull: "0",
|
||||
BehindForPull: "0",
|
||||
},
|
||||
itemOperation: types.ItemOperationNone,
|
||||
fullDescription: false,
|
||||
viewWidth: 14,
|
||||
useIcons: false,
|
||||
checkedOutByWorktree: false,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "branch_… ✓"},
|
||||
},
|
||||
{
|
||||
@@ -159,15 +225,16 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
||||
Name: "branch_name",
|
||||
Recency: "1m",
|
||||
UpstreamRemote: "origin",
|
||||
Pushables: "3",
|
||||
Pullables: "5",
|
||||
AheadForPull: "3",
|
||||
BehindForPull: "5",
|
||||
},
|
||||
itemOperation: types.ItemOperationNone,
|
||||
fullDescription: false,
|
||||
viewWidth: 30,
|
||||
useIcons: false,
|
||||
checkedOutByWorktree: true,
|
||||
expected: []string{"1m", "branch_na… (worktree) ↑3↓5"},
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "branch_na… (worktree) ↓5↑3"},
|
||||
},
|
||||
{
|
||||
branch: &models.Branch{Name: "branch_name", Recency: "1m"},
|
||||
@@ -176,6 +243,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
||||
viewWidth: 20,
|
||||
useIcons: false,
|
||||
checkedOutByWorktree: false,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "branc… Pushing |"},
|
||||
},
|
||||
{
|
||||
@@ -185,6 +253,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
||||
viewWidth: -1,
|
||||
useIcons: false,
|
||||
checkedOutByWorktree: false,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "abc Pushing |"},
|
||||
},
|
||||
{
|
||||
@@ -194,6 +263,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
||||
viewWidth: -1,
|
||||
useIcons: false,
|
||||
checkedOutByWorktree: false,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "ab Pushing |"},
|
||||
},
|
||||
{
|
||||
@@ -203,6 +273,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
||||
viewWidth: -1,
|
||||
useIcons: false,
|
||||
checkedOutByWorktree: false,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "a Pushing |"},
|
||||
},
|
||||
{
|
||||
@@ -212,8 +283,8 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
||||
CommitHash: "1234567890",
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "branch_name",
|
||||
Pushables: "0",
|
||||
Pullables: "0",
|
||||
AheadForPull: "0",
|
||||
BehindForPull: "0",
|
||||
Subject: "commit title",
|
||||
},
|
||||
itemOperation: types.ItemOperationNone,
|
||||
@@ -221,6 +292,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
||||
viewWidth: 20,
|
||||
useIcons: false,
|
||||
checkedOutByWorktree: false,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "12345678", "bran… ✓", "origin branch_name", "commit title"},
|
||||
},
|
||||
}
|
||||
@@ -232,6 +304,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
||||
|
||||
for i, s := range scenarios {
|
||||
icons.SetNerdFontsVersion(lo.Ternary(s.useIcons, "3", ""))
|
||||
c.UserConfig.Gui.ShowDivergenceFromBaseBranch = s.showDivergenceCfg
|
||||
|
||||
worktrees := []*models.Worktree{}
|
||||
if s.checkedOutByWorktree {
|
||||
|
||||
+100
-30
@@ -24,6 +24,7 @@ import (
|
||||
type pipeSetCacheKey struct {
|
||||
commitHash string
|
||||
commitCount int
|
||||
divergence models.Divergence
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -78,24 +79,76 @@ func GetCommitListDisplayStrings(
|
||||
// function expects to be passed the index of the commit in terms of the `commits` slice
|
||||
var getGraphLine func(int) string
|
||||
if showGraph {
|
||||
// this is where the graph begins (may be beyond the TODO commits depending on startIdx,
|
||||
// but we'll never include TODO commits as part of the graph because it'll be messy)
|
||||
graphOffset := max(startIdx, rebaseOffset)
|
||||
if len(commits) > 0 && commits[0].Divergence != models.DivergenceNone {
|
||||
// Showing a divergence log; we know we don't have any rebasing
|
||||
// commits in this case. But we need to render separate graphs for
|
||||
// the Local and Remote sections.
|
||||
allGraphLines := []string{}
|
||||
|
||||
pipeSets := loadPipesets(commits[rebaseOffset:])
|
||||
pipeSetOffset := max(startIdx-rebaseOffset, 0)
|
||||
graphPipeSets := pipeSets[pipeSetOffset:max(endIdx-rebaseOffset, 0)]
|
||||
graphCommits := commits[graphOffset:endIdx]
|
||||
graphLines := graph.RenderAux(
|
||||
graphPipeSets,
|
||||
graphCommits,
|
||||
selectedCommitHash,
|
||||
)
|
||||
getGraphLine = func(idx int) string {
|
||||
if idx >= graphOffset {
|
||||
return graphLines[idx-graphOffset]
|
||||
} else {
|
||||
return ""
|
||||
_, localSectionStart, found := lo.FindIndexOf(
|
||||
commits, func(c *models.Commit) bool { return c.Divergence == models.DivergenceLeft })
|
||||
if !found {
|
||||
localSectionStart = len(commits)
|
||||
}
|
||||
|
||||
if localSectionStart > 0 {
|
||||
// we have some remote commits
|
||||
pipeSets := loadPipesets(commits[:localSectionStart])
|
||||
if startIdx < localSectionStart {
|
||||
// some of the remote commits are visible
|
||||
start := startIdx
|
||||
end := min(endIdx, localSectionStart)
|
||||
graphPipeSets := pipeSets[start:end]
|
||||
graphCommits := commits[start:end]
|
||||
graphLines := graph.RenderAux(
|
||||
graphPipeSets,
|
||||
graphCommits,
|
||||
selectedCommitHash,
|
||||
)
|
||||
allGraphLines = append(allGraphLines, graphLines...)
|
||||
}
|
||||
}
|
||||
if localSectionStart < len(commits) {
|
||||
// we have some local commits
|
||||
pipeSets := loadPipesets(commits[localSectionStart:])
|
||||
if localSectionStart < endIdx {
|
||||
// some of the local commits are visible
|
||||
graphOffset := max(startIdx, localSectionStart)
|
||||
pipeSetOffset := max(startIdx-localSectionStart, 0)
|
||||
graphPipeSets := pipeSets[pipeSetOffset : endIdx-localSectionStart]
|
||||
graphCommits := commits[graphOffset:endIdx]
|
||||
graphLines := graph.RenderAux(
|
||||
graphPipeSets,
|
||||
graphCommits,
|
||||
selectedCommitHash,
|
||||
)
|
||||
allGraphLines = append(allGraphLines, graphLines...)
|
||||
}
|
||||
}
|
||||
|
||||
getGraphLine = func(idx int) string {
|
||||
return allGraphLines[idx-startIdx]
|
||||
}
|
||||
} else {
|
||||
// this is where the graph begins (may be beyond the TODO commits depending on startIdx,
|
||||
// but we'll never include TODO commits as part of the graph because it'll be messy)
|
||||
graphOffset := max(startIdx, rebaseOffset)
|
||||
|
||||
pipeSets := loadPipesets(commits[rebaseOffset:])
|
||||
pipeSetOffset := max(startIdx-rebaseOffset, 0)
|
||||
graphPipeSets := pipeSets[pipeSetOffset:max(endIdx-rebaseOffset, 0)]
|
||||
graphCommits := commits[graphOffset:endIdx]
|
||||
graphLines := graph.RenderAux(
|
||||
graphPipeSets,
|
||||
graphCommits,
|
||||
selectedCommitHash,
|
||||
)
|
||||
getGraphLine = func(idx int) string {
|
||||
if idx >= graphOffset {
|
||||
return graphLines[idx-graphOffset]
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -205,6 +258,7 @@ func loadPipesets(commits []*models.Commit) [][]*graph.Pipe {
|
||||
cacheKey := pipeSetCacheKey{
|
||||
commitHash: commits[0].Hash,
|
||||
commitCount: len(commits),
|
||||
divergence: commits[0].Divergence,
|
||||
}
|
||||
|
||||
pipeSets, ok := pipeSetCache[cacheKey]
|
||||
@@ -312,9 +366,33 @@ func displayCommit(
|
||||
bisectInfo *git_commands.BisectInfo,
|
||||
isYouAreHereCommit bool,
|
||||
) []string {
|
||||
hashColor := getHashColor(commit, diffName, cherryPickedCommitHashSet, bisectStatus, bisectInfo)
|
||||
bisectString := getBisectStatusText(bisectStatus, bisectInfo)
|
||||
|
||||
hashString := ""
|
||||
hashColor := getHashColor(commit, diffName, cherryPickedCommitHashSet, bisectStatus, bisectInfo)
|
||||
hashLength := common.UserConfig.Gui.CommitHashLength
|
||||
if hashLength >= len(commit.Hash) {
|
||||
hashString = hashColor.Sprint(commit.Hash)
|
||||
} else if hashLength > 0 {
|
||||
hashString = hashColor.Sprint(commit.Hash[:hashLength])
|
||||
} else if !icons.IsIconEnabled() { // hashLength <= 0
|
||||
hashString = hashColor.Sprint("*")
|
||||
}
|
||||
|
||||
divergenceString := ""
|
||||
if commit.Divergence != models.DivergenceNone {
|
||||
divergenceString = hashColor.Sprint(lo.Ternary(commit.Divergence == models.DivergenceLeft, "↑", "↓"))
|
||||
} else if icons.IsIconEnabled() {
|
||||
divergenceString = hashColor.Sprint(icons.IconForCommit(commit))
|
||||
}
|
||||
|
||||
descriptionString := ""
|
||||
if fullDescription {
|
||||
descriptionString = style.FgBlue.Sprint(
|
||||
utils.UnixToDateSmart(now, commit.UnixTimestamp, timeFormat, shortTimeFormat),
|
||||
)
|
||||
}
|
||||
|
||||
actionString := ""
|
||||
if commit.Action != models.ActionNone {
|
||||
todoString := lo.Ternary(commit.Action == models.ActionConflict, "conflict", commit.Action.String())
|
||||
@@ -368,20 +446,12 @@ func displayCommit(
|
||||
}
|
||||
|
||||
cols := make([]string, 0, 7)
|
||||
if commit.Divergence != models.DivergenceNone {
|
||||
cols = append(cols, hashColor.Sprint(lo.Ternary(commit.Divergence == models.DivergenceLeft, "↑", "↓")))
|
||||
} else if icons.IsIconEnabled() {
|
||||
cols = append(cols, hashColor.Sprint(icons.IconForCommit(commit)))
|
||||
}
|
||||
cols = append(cols, hashColor.Sprint(commit.ShortHash()))
|
||||
cols = append(cols, bisectString)
|
||||
if fullDescription {
|
||||
cols = append(cols, style.FgBlue.Sprint(
|
||||
utils.UnixToDateSmart(now, commit.UnixTimestamp, timeFormat, shortTimeFormat),
|
||||
))
|
||||
}
|
||||
cols = append(
|
||||
cols,
|
||||
divergenceString,
|
||||
hashString,
|
||||
bisectString,
|
||||
descriptionString,
|
||||
actionString,
|
||||
authorFunc(commit.AuthorName),
|
||||
graphLine+mark+tagString+theme.DefaultTextColor.Sprint(name),
|
||||
|
||||
@@ -359,6 +359,185 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
|
||||
hash3 ◯ commit3
|
||||
`),
|
||||
},
|
||||
{
|
||||
testName: "graph in divergence view - all commits visible",
|
||||
commits: []*models.Commit{
|
||||
{Name: "commit1", Hash: "hash1r", Parents: []string{"hash2r"}, Divergence: models.DivergenceRight},
|
||||
{Name: "commit2", Hash: "hash2r", Parents: []string{"hash3r", "hash5r"}, Divergence: models.DivergenceRight},
|
||||
{Name: "commit3", Hash: "hash3r", Parents: []string{"hash4r"}, Divergence: models.DivergenceRight},
|
||||
{Name: "commit1", Hash: "hash1l", Parents: []string{"hash2l"}, Divergence: models.DivergenceLeft},
|
||||
{Name: "commit2", Hash: "hash2l", Parents: []string{"hash3l", "hash4l"}, Divergence: models.DivergenceLeft},
|
||||
{Name: "commit3", Hash: "hash3l", Parents: []string{"hash4l"}, Divergence: models.DivergenceLeft},
|
||||
{Name: "commit4", Hash: "hash4l", Parents: []string{"hash5l"}, Divergence: models.DivergenceLeft},
|
||||
{Name: "commit5", Hash: "hash5l", Parents: []string{"hash6l"}, Divergence: models.DivergenceLeft},
|
||||
},
|
||||
startIdx: 0,
|
||||
endIdx: 8,
|
||||
showGraph: true,
|
||||
bisectInfo: git_commands.NewNullBisectInfo(),
|
||||
cherryPickedCommitHashSet: set.New[string](),
|
||||
showYouAreHereLabel: false,
|
||||
now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
expected: formatExpected(`
|
||||
↓ hash1r ◯ commit1
|
||||
↓ hash2r ⏣─╮ commit2
|
||||
↓ hash3r ◯ │ commit3
|
||||
↑ hash1l ◯ commit1
|
||||
↑ hash2l ⏣─╮ commit2
|
||||
↑ hash3l ◯ │ commit3
|
||||
↑ hash4l ◯─╯ commit4
|
||||
↑ hash5l ◯ commit5
|
||||
`),
|
||||
},
|
||||
{
|
||||
testName: "graph in divergence view - not all remote commits visible",
|
||||
commits: []*models.Commit{
|
||||
{Name: "commit1", Hash: "hash1r", Parents: []string{"hash2r"}, Divergence: models.DivergenceRight},
|
||||
{Name: "commit2", Hash: "hash2r", Parents: []string{"hash3r", "hash5r"}, Divergence: models.DivergenceRight},
|
||||
{Name: "commit3", Hash: "hash3r", Parents: []string{"hash4r"}, Divergence: models.DivergenceRight},
|
||||
{Name: "commit1", Hash: "hash1l", Parents: []string{"hash2l"}, Divergence: models.DivergenceLeft},
|
||||
{Name: "commit2", Hash: "hash2l", Parents: []string{"hash3l", "hash4l"}, Divergence: models.DivergenceLeft},
|
||||
{Name: "commit3", Hash: "hash3l", Parents: []string{"hash4l"}, Divergence: models.DivergenceLeft},
|
||||
{Name: "commit4", Hash: "hash4l", Parents: []string{"hash5l"}, Divergence: models.DivergenceLeft},
|
||||
{Name: "commit5", Hash: "hash5l", Parents: []string{"hash6l"}, Divergence: models.DivergenceLeft},
|
||||
},
|
||||
startIdx: 2,
|
||||
endIdx: 8,
|
||||
showGraph: true,
|
||||
bisectInfo: git_commands.NewNullBisectInfo(),
|
||||
cherryPickedCommitHashSet: set.New[string](),
|
||||
showYouAreHereLabel: false,
|
||||
now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
expected: formatExpected(`
|
||||
↓ hash3r ◯ │ commit3
|
||||
↑ hash1l ◯ commit1
|
||||
↑ hash2l ⏣─╮ commit2
|
||||
↑ hash3l ◯ │ commit3
|
||||
↑ hash4l ◯─╯ commit4
|
||||
↑ hash5l ◯ commit5
|
||||
`),
|
||||
},
|
||||
{
|
||||
testName: "graph in divergence view - not all local commits",
|
||||
commits: []*models.Commit{
|
||||
{Name: "commit1", Hash: "hash1r", Parents: []string{"hash2r"}, Divergence: models.DivergenceRight},
|
||||
{Name: "commit2", Hash: "hash2r", Parents: []string{"hash3r", "hash5r"}, Divergence: models.DivergenceRight},
|
||||
{Name: "commit3", Hash: "hash3r", Parents: []string{"hash4r"}, Divergence: models.DivergenceRight},
|
||||
{Name: "commit1", Hash: "hash1l", Parents: []string{"hash2l"}, Divergence: models.DivergenceLeft},
|
||||
{Name: "commit2", Hash: "hash2l", Parents: []string{"hash3l", "hash4l"}, Divergence: models.DivergenceLeft},
|
||||
{Name: "commit3", Hash: "hash3l", Parents: []string{"hash4l"}, Divergence: models.DivergenceLeft},
|
||||
{Name: "commit4", Hash: "hash4l", Parents: []string{"hash5l"}, Divergence: models.DivergenceLeft},
|
||||
{Name: "commit5", Hash: "hash5l", Parents: []string{"hash6l"}, Divergence: models.DivergenceLeft},
|
||||
},
|
||||
startIdx: 0,
|
||||
endIdx: 5,
|
||||
showGraph: true,
|
||||
bisectInfo: git_commands.NewNullBisectInfo(),
|
||||
cherryPickedCommitHashSet: set.New[string](),
|
||||
showYouAreHereLabel: false,
|
||||
now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
expected: formatExpected(`
|
||||
↓ hash1r ◯ commit1
|
||||
↓ hash2r ⏣─╮ commit2
|
||||
↓ hash3r ◯ │ commit3
|
||||
↑ hash1l ◯ commit1
|
||||
↑ hash2l ⏣─╮ commit2
|
||||
`),
|
||||
},
|
||||
{
|
||||
testName: "graph in divergence view - no remote commits visible",
|
||||
commits: []*models.Commit{
|
||||
{Name: "commit1", Hash: "hash1r", Parents: []string{"hash2r"}, Divergence: models.DivergenceRight},
|
||||
{Name: "commit2", Hash: "hash2r", Parents: []string{"hash3r", "hash5r"}, Divergence: models.DivergenceRight},
|
||||
{Name: "commit3", Hash: "hash3r", Parents: []string{"hash4r"}, Divergence: models.DivergenceRight},
|
||||
{Name: "commit1", Hash: "hash1l", Parents: []string{"hash2l"}, Divergence: models.DivergenceLeft},
|
||||
{Name: "commit2", Hash: "hash2l", Parents: []string{"hash3l", "hash4l"}, Divergence: models.DivergenceLeft},
|
||||
{Name: "commit3", Hash: "hash3l", Parents: []string{"hash4l"}, Divergence: models.DivergenceLeft},
|
||||
{Name: "commit4", Hash: "hash4l", Parents: []string{"hash5l"}, Divergence: models.DivergenceLeft},
|
||||
{Name: "commit5", Hash: "hash5l", Parents: []string{"hash6l"}, Divergence: models.DivergenceLeft},
|
||||
},
|
||||
startIdx: 4,
|
||||
endIdx: 8,
|
||||
showGraph: true,
|
||||
bisectInfo: git_commands.NewNullBisectInfo(),
|
||||
cherryPickedCommitHashSet: set.New[string](),
|
||||
showYouAreHereLabel: false,
|
||||
now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
expected: formatExpected(`
|
||||
↑ hash2l ⏣─╮ commit2
|
||||
↑ hash3l ◯ │ commit3
|
||||
↑ hash4l ◯─╯ commit4
|
||||
↑ hash5l ◯ commit5
|
||||
`),
|
||||
},
|
||||
{
|
||||
testName: "graph in divergence view - no local commits visible",
|
||||
commits: []*models.Commit{
|
||||
{Name: "commit1", Hash: "hash1r", Parents: []string{"hash2r"}, Divergence: models.DivergenceRight},
|
||||
{Name: "commit2", Hash: "hash2r", Parents: []string{"hash3r", "hash5r"}, Divergence: models.DivergenceRight},
|
||||
{Name: "commit3", Hash: "hash3r", Parents: []string{"hash4r"}, Divergence: models.DivergenceRight},
|
||||
{Name: "commit1", Hash: "hash1l", Parents: []string{"hash2l"}, Divergence: models.DivergenceLeft},
|
||||
{Name: "commit2", Hash: "hash2l", Parents: []string{"hash3l", "hash4l"}, Divergence: models.DivergenceLeft},
|
||||
{Name: "commit3", Hash: "hash3l", Parents: []string{"hash4l"}, Divergence: models.DivergenceLeft},
|
||||
{Name: "commit4", Hash: "hash4l", Parents: []string{"hash5l"}, Divergence: models.DivergenceLeft},
|
||||
{Name: "commit5", Hash: "hash5l", Parents: []string{"hash6l"}, Divergence: models.DivergenceLeft},
|
||||
},
|
||||
startIdx: 0,
|
||||
endIdx: 2,
|
||||
showGraph: true,
|
||||
bisectInfo: git_commands.NewNullBisectInfo(),
|
||||
cherryPickedCommitHashSet: set.New[string](),
|
||||
showYouAreHereLabel: false,
|
||||
now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
expected: formatExpected(`
|
||||
↓ hash1r ◯ commit1
|
||||
↓ hash2r ⏣─╮ commit2
|
||||
`),
|
||||
},
|
||||
{
|
||||
testName: "graph in divergence view - no remote commits present",
|
||||
commits: []*models.Commit{
|
||||
{Name: "commit1", Hash: "hash1l", Parents: []string{"hash2l"}, Divergence: models.DivergenceLeft},
|
||||
{Name: "commit2", Hash: "hash2l", Parents: []string{"hash3l", "hash4l"}, Divergence: models.DivergenceLeft},
|
||||
{Name: "commit3", Hash: "hash3l", Parents: []string{"hash4l"}, Divergence: models.DivergenceLeft},
|
||||
{Name: "commit4", Hash: "hash4l", Parents: []string{"hash5l"}, Divergence: models.DivergenceLeft},
|
||||
{Name: "commit5", Hash: "hash5l", Parents: []string{"hash6l"}, Divergence: models.DivergenceLeft},
|
||||
},
|
||||
startIdx: 0,
|
||||
endIdx: 5,
|
||||
showGraph: true,
|
||||
bisectInfo: git_commands.NewNullBisectInfo(),
|
||||
cherryPickedCommitHashSet: set.New[string](),
|
||||
showYouAreHereLabel: false,
|
||||
now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
expected: formatExpected(`
|
||||
↑ hash1l ◯ commit1
|
||||
↑ hash2l ⏣─╮ commit2
|
||||
↑ hash3l ◯ │ commit3
|
||||
↑ hash4l ◯─╯ commit4
|
||||
↑ hash5l ◯ commit5
|
||||
`),
|
||||
},
|
||||
{
|
||||
testName: "graph in divergence view - no local commits present",
|
||||
commits: []*models.Commit{
|
||||
{Name: "commit1", Hash: "hash1r", Parents: []string{"hash2r"}, Divergence: models.DivergenceRight},
|
||||
{Name: "commit2", Hash: "hash2r", Parents: []string{"hash3r", "hash5r"}, Divergence: models.DivergenceRight},
|
||||
{Name: "commit3", Hash: "hash3r", Parents: []string{"hash4r"}, Divergence: models.DivergenceRight},
|
||||
},
|
||||
startIdx: 0,
|
||||
endIdx: 3,
|
||||
showGraph: true,
|
||||
bisectInfo: git_commands.NewNullBisectInfo(),
|
||||
cherryPickedCommitHashSet: set.New[string](),
|
||||
showYouAreHereLabel: false,
|
||||
now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
expected: formatExpected(`
|
||||
↓ hash1r ◯ commit1
|
||||
↓ hash2r ⏣─╮ commit2
|
||||
↓ hash3r ◯ │ commit3
|
||||
`),
|
||||
},
|
||||
{
|
||||
testName: "custom time format",
|
||||
commits: []*models.Commit{
|
||||
@@ -396,7 +575,6 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
|
||||
common := utils.NewDummyCommon()
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
if !focusing || s.focus {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
result := GetCommitListDisplayStrings(
|
||||
|
||||
@@ -66,7 +66,6 @@ M file1
|
||||
defer color.ForceSetColorLevel(oldColorLevel)
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
viewModel := filetree.NewFileTree(func() []*models.File { return s.files }, utils.NewDummyLog(), true)
|
||||
viewModel.SetTree()
|
||||
@@ -128,7 +127,6 @@ M file1
|
||||
defer color.ForceSetColorLevel(oldColorLevel)
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
viewModel := filetree.NewCommitFileTreeViewModel(func() []*models.CommitFile { return s.files }, utils.NewDummyLog(), true)
|
||||
viewModel.SetRef(&models.Commit{})
|
||||
|
||||
@@ -84,7 +84,6 @@ func RenderAux(pipeSets [][]*Pipe, commits []*models.Commit, selectedCommitHash
|
||||
wg.Add(maxProcs)
|
||||
|
||||
for i := 0; i < maxProcs; i++ {
|
||||
i := i
|
||||
go func() {
|
||||
from := i * perProc
|
||||
to := (i + 1) * perProc
|
||||
|
||||
@@ -217,7 +217,6 @@ func TestRenderCommitGraph(t *testing.T) {
|
||||
defer color.ForceSetColorLevel(oldColorLevel)
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
getStyle := func(c *models.Commit) style.TextStyle { return style.FgDefault }
|
||||
lines := RenderCommitGraph(test.commits, "blah", getStyle)
|
||||
@@ -454,7 +453,6 @@ func TestRenderPipeSet(t *testing.T) {
|
||||
defer color.ForceSetColorLevel(oldColorLevel)
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
actualStr := renderPipeSet(test.pipes, "selected", test.prevCommit)
|
||||
t.Log("actual cells:")
|
||||
|
||||
@@ -2,6 +2,7 @@ package presentation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
||||
@@ -24,7 +25,10 @@ func FormatStatus(
|
||||
status := ""
|
||||
|
||||
if currentBranch.IsRealBranch() {
|
||||
status += ColoredBranchStatus(currentBranch, itemOperation, tr, userConfig) + " "
|
||||
status += BranchStatus(currentBranch, itemOperation, tr, time.Now(), userConfig)
|
||||
if status != "" {
|
||||
status += " "
|
||||
}
|
||||
}
|
||||
|
||||
if workingTreeState != enums.REBASE_MODE_NONE {
|
||||
@@ -40,7 +44,7 @@ func FormatStatus(
|
||||
}
|
||||
repoName = fmt.Sprintf("%s(%s%s)", repoName, icon, style.FgCyan.Sprint(linkedWorktreeName))
|
||||
}
|
||||
status += fmt.Sprintf("%s → %s ", repoName, name)
|
||||
status += fmt.Sprintf("%s → %s", repoName, name)
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
@@ -292,7 +292,15 @@ func (self *HandlerCreator) finalHandler(customCommand config.CustomCommand, ses
|
||||
if strings.TrimSpace(output) == "" {
|
||||
output = self.c.Tr.EmptyOutput
|
||||
}
|
||||
return self.c.Alert(cmdStr, output)
|
||||
|
||||
title := cmdStr
|
||||
if customCommand.OutputTitle != "" {
|
||||
title, err = resolveTemplate(customCommand.OutputTitle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return self.c.Alert(title, output)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user