Compare commits

..
Author SHA1 Message Date
Luka Markušić 936ae69429 Open file on unspecified line 2023-02-03 11:07:42 +01:00
3694 changed files with 16287 additions and 19567 deletions
+31 -9
View File
@@ -46,6 +46,36 @@ jobs:
# we're passing -short so that we skip the integration tests, which will be run in parallel below
run: |
go test ./... -short
integration-tests-old:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
parallelism: [5]
index: [0,1,2,3,4]
name: "Integration Tests (Old pattern) (${{ matrix.index }}/${{ matrix.parallelism }})"
env:
GOFLAGS: -mod=vendor
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: 1.18.x
- name: Cache build
uses: actions/cache@v1
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-test
restore-keys: |
${{runner.os}}-go-
- name: Test code
# for file.allow thing see https://vielmetti.typepad.com/logbook/2022/10/git-security-fixes-lead-to-fatal-transport-file-not-allowed-error-in-ci-systems-cve-2022-39253.html
run: |
git config --global protocol.file.allow always && PARALLEL_TOTAL=${{ matrix.parallelism }} PARALLEL_INDEX=${{ matrix.index }} go test pkg/integration/deprecated/*.go
integration-tests:
runs-on: ubuntu-latest
name: "Integration Tests"
@@ -68,9 +98,8 @@ jobs:
restore-keys: |
${{runner.os}}-go-
- name: Test code
# LONG_WAIT_BEFORE_FAIL means that for a given test assertion, we'll wait longer before failing
run: |
LONG_WAIT_BEFORE_FAIL=true go test pkg/integration/clients/*.go
go test pkg/integration/clients/*.go
build:
runs-on: ubuntu-latest
env:
@@ -135,13 +164,6 @@ jobs:
# ensure our vendor directory matches up with our go modules
run: |
go mod vendor && git diff --exit-code || (echo "Unexpected change to vendor directory. Run 'go mod vendor' locally and commit the changes" && exit 1)
- name: Check Integration Test List
# ensure our integration test list is up to date
run: |
go generate pkg/integration/tests/tests.go && git diff --exit-code || (echo "Integration test list not up to date. Run 'go generate pkg/integration/tests/tests.go' locally and commit the changes" && exit 1)
shell: bash # needed so that we get "-o pipefail"
- name: Check Filenames
run: scripts/check_filenames.sh
lint:
runs-on: ubuntu-latest
env:
+12 -4
View File
@@ -20,12 +20,12 @@ jobs:
run: echo "UUID_APP_TAG=$(uuidgen)" >> $GITHUB_ENV
- name: Docker metadata
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@v3
with:
images: registry.uffizzi.com/${{ env.UUID_APP_TAG }}
tags: type=raw,value=60d
- name: Build and Push Image to registry.uffizzi.com ephemeral registry
uses: docker/build-push-action@v3
uses: docker/build-push-action@v2
with:
push: true
context: ./
@@ -60,11 +60,17 @@ jobs:
name: preview-spec
path: docker-compose.rendered.yml
retention-days: 2
- name: Serialize PR Event to File
run: |
cat << EOF > event.json
${{ toJSON(github.event) }}
EOF
- name: Upload PR Event as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: ${{ github.event_path }}
path: event.json
retention-days: 2
delete-preview:
@@ -73,9 +79,11 @@ jobs:
if: ${{ github.event.action == 'closed' }}
steps:
# If this PR is closing, we will not render a compose file nor pass it to the next workflow.
- name: Serialize PR Event to File
run: echo '${{ toJSON(github.event) }}' > event.json
- name: Upload PR Event as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: ${{ github.event_path }}
path: event.json
retention-days: 2
+4 -10
View File
@@ -7,11 +7,11 @@ on:
types:
- completed
jobs:
cache-compose-file:
name: Cache Compose File
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
outputs:
compose-file-cache-key: ${{ env.COMPOSE_FILE_HASH }}
pr-number: ${{ env.PR_NUMBER }}
@@ -29,9 +29,6 @@ jobs:
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "preview-spec"
})[0];
if (matchArtifact === undefined) {
throw TypeError('Build Artifact not found!');
}
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
@@ -42,18 +39,16 @@ jobs:
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/preview-spec.zip`, Buffer.from(download.data));
- name: 'Unzip artifact'
run: unzip preview-spec.zip
- name: Read Event into ENV
run: |
echo 'EVENT_JSON<<EOF' >> $GITHUB_ENV
cat event.json >> $GITHUB_ENV
echo -e '\nEOF' >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
- name: Hash Rendered Compose File
id: hash
# If the previous workflow was triggered by a PR close event, we will not have a compose file artifact.
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
run: echo "COMPOSE_FILE_HASH=$(md5sum docker-compose.rendered.yml | awk '{ print $1 }')" >> $GITHUB_ENV
- name: Cache Rendered Compose File
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
uses: actions/cache@v3
@@ -75,8 +70,7 @@ jobs:
name: Use Remote Workflow to Preview on Uffizzi
needs:
- cache-compose-file
if: ${{ github.event.workflow_run.conclusion == 'success' }}
uses: UffizziCloud/preview-action/.github/workflows/reusable.yaml@v2
uses: UffizziCloud/preview-action/.github/workflows/reusable.yaml@v2.6.1
with:
# If this workflow was triggered by a PR close event, cache-key will be an empty string
# and this reusable workflow will delete the preview deployment.
@@ -87,4 +81,4 @@ jobs:
permissions:
contents: read
pull-requests: write
id-token: write
id-token: write
+8 -1
View File
@@ -35,7 +35,14 @@ lazygit.exe
test/git_server/data
test/results/**
# we'll scrap these lines once we've fully moved over to the new integration test approach
test/integration/*/actual/
test/integration/*/used_config/
# these sample hooks waste too much space
test/integration/*/expected/**/hooks/
test/integration/*/expected_remote/**/hooks/
test/integration_new/**
oryxBuildBinary
__debug_bin
+2 -2
View File
@@ -7,7 +7,7 @@
"request": "launch",
"mode": "auto",
"program": "main.go",
"args": ["--debug", "--use-config-file=${workspaceFolder}/.vscode/debugger_config.yml"],
"args": ["--debug", "--use-config-file=.vscode/debugger_config.yml"],
"console": "integratedTerminal",
"presentation": {
"hidden": true
@@ -19,7 +19,7 @@
"request": "launch",
"mode": "auto",
"program": "main.go",
"args": ["--logs", "--use-config-file=${workspaceFolder}/.vscode/debugger_config.yml"],
"args": ["--logs", "--use-config-file=.vscode/debugger_config.yml"],
"console": "integratedTerminal",
"presentation": {
"hidden": true
+4 -16
View File
@@ -24,7 +24,7 @@
{
"label": "Run current file integration test",
"type": "shell",
"command": "go generate pkg/integration/tests/tests.go && go run cmd/integration_test/main.go cli ${relativeFile}",
"command": "go run cmd/integration_test/main.go cli ${relativeFile}",
"problemMatcher": [],
"group": {
"kind": "test",
@@ -37,7 +37,7 @@
{
"label": "Run current file integration test (slow)",
"type": "shell",
"command": "go generate pkg/integration/tests/tests.go && go run cmd/integration_test/main.go cli --slow ${relativeFile}",
"command": "go run cmd/integration_test/main.go cli --slow ${relativeFile}",
"problemMatcher": [],
"group": {
"kind": "test",
@@ -49,7 +49,7 @@
{
"label": "Run current file integration test (sandbox)",
"type": "shell",
"command": "go generate pkg/integration/tests/tests.go && go run cmd/integration_test/main.go cli --sandbox ${relativeFile}",
"command": "go run cmd/integration_test/main.go cli --sandbox ${relativeFile}",
"problemMatcher": [],
"group": {
"kind": "test",
@@ -69,18 +69,6 @@
"presentation": {
"focus": true
}
},
{
"label": "Sync tests list",
"type": "shell",
"command": "go generate pkg/integration/tests/tests.go",
"problemMatcher": [],
"group": {
"kind": "test",
},
"presentation": {
"focus": true
}
},
}
],
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+5
View File
@@ -17,6 +17,11 @@ Usage:
If you pass no test names, it runs all tests
Accepted environment variables:
KEY_PRESS_DELAY (e.g. 200): the number of milliseconds to wait between keypresses
MODE:
* ask (default): if a snapshot test fails, asks if you want to update the snapshot
* check: if a snapshot test fails, exits with an error
* update: if a snapshot test fails, updates the snapshot
* sandbox: uses the test's setup step to run the test in a sandbox where you can do whatever you want
TUI mode:
> go run cmd/integration_test/main.go tui
+40 -34
View File
@@ -9,7 +9,7 @@ Default path for the config file:
For old installations (slightly embarrassing: I didn't realise at the time that you didn't need to supply a vendor name to the path so I just used my name):
- Linux: `~/.config/jesseduffield/lazygit/config.yml`
- MacOS: `~/Library/Application\ Support/jesseduffield/lazygit/config.yml`
- MacOS: `~/Library/Application Support/jesseduffield/lazygit/config.yml`
- Windows: `%APPDATA%\jesseduffield\lazygit\config.yml`
If you want to change the config directory:
@@ -57,14 +57,12 @@ gui:
showFileTree: true # for rendering changes files in a tree format
showListFooter: true # for seeing the '5 of 20' message in list panels
showRandomTip: true
experimentalShowBranchHeads: false # visualize branch heads with (*) in commits list
showBottomLine: true # for hiding the bottom information line (unless it has important information to tell you)
showCommandLog: true
showIcons: false
commandLogSize: 8
splitDiff: 'auto' # one of 'auto' | 'always'
skipRewordInEditorWarning: false # for skipping the confirmation before launching the reword editor
border: 'single' # one of 'single' | 'double' | 'rounded' | 'hidden'
git:
paging:
colorArg: always
@@ -97,12 +95,9 @@ git:
parseEmoji: false
diffContextSize: 3 # how many lines of context are shown around a change in diffs
os:
editPreset: '' # see 'Configuring File Editing' section
edit: ''
editAtLine: ''
editAtLineAndWait: ''
open: ''
openLink: ''
editCommand: '' # see 'Configuring File Editing' section
editCommandTemplate: ''
openCommand: ''
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'.
@@ -120,6 +115,8 @@ keybinding:
quit: 'q'
quit-alt1: '<c-c>' # alternative/alias of quit
return: '<esc>' # return to previous menu, will quit if there's nowhere to return
# When set to a printable character, this will work for returning from non-prompt panels
return-alt1: null
quitWithoutChangingDirectory: 'Q'
togglePanel: '<tab>' # goto the next panel
prevItem: '<up>' # go one line up
@@ -139,12 +136,13 @@ keybinding:
jumpToBlock: ['1', '2', '3', '4', '5'] # goto the Nth block / panel
nextMatch: 'n'
prevMatch: 'N'
optionMenu: null # show help menu
optionMenu: 'x' # show help menu
optionMenu-alt1: '?' # show help menu
select: '<space>'
goInto: '<enter>'
openRecentRepos: '<c-r>'
confirm: '<enter>'
confirm-alt1: 'y'
remove: 'd'
new: 'n'
edit: 'e'
@@ -172,6 +170,7 @@ keybinding:
diffingMenu-alt: '<c-e>' # deprecated
copyToClipboard: '<c-o>'
submitEditorText: '<enter>'
appendNewline: '<a-enter>'
extrasMenu: '@'
toggleWhitespaceInDiffView: '<c-w>'
increaseContextInDiffView: '}'
@@ -204,7 +203,6 @@ keybinding:
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'
@@ -271,41 +269,40 @@ os:
### Configuring File Editing
There are two commands for opening files, `o` for "open" and `e` for "edit". `o`
acts as if the file was double-clicked in the Finder/Explorer, so it also works
for non-text files, whereas `e` opens the file in an editor. `e` can also jump
to the right line in the file if you invoke it from the staging panel, for
example.
Lazygit will edit a file with the first set editor in the following:
To tell lazygit which editor to use for the `e` command, the easiest way to do
that is to provide an editPreset config, e.g.
1. config.yaml
```yaml
os:
editPreset: 'vscode'
editCommand: 'vim' # as an example
```
Supported presets are `vim`, `emacs`, `nano`, `vscode`, `sublime`, `bbedit`, and
`xcode`. In many cases lazygit will be able to guess the right preset from your
$(git config core.editor), or an environment variable such as $VISUAL or $EDITOR.
2. \$(git config core.editor)
3. \$GIT_EDITOR
4. \$VISUAL
5. \$EDITOR
6. \$(which vi)
If for some reason you are not happy with the default commands from a preset, or
there simply is no preset for your editor, you can customize the commands by
setting the `edit`, `editAtLine`, and `editAtLineAndWait` options, e.g.:
Lazygit will log an error if none of these options are set.
You can specify the current line number when you're in the patch explorer.
```yaml
os:
edit: 'myeditor {{filename}}'
editAtLine: 'myeditor --line={{line}} {{filename}}'
editAtLineAndWait: 'myeditor --block --line={{line}} {{filename}}'
editInTerminal: true
editCommand: 'vim'
editCommandTemplate: '{{editor}} +{{line}} -- {{filename}}'
```
The `editInTerminal` option is used to decide whether lazygit needs to suspend
itself to the background before calling the editor.
or
Contributions of new editor presets are welcome; see the `getPreset` function in
[`editor_presets.go`](https://github.com/jesseduffield/lazygit/blob/master/pkg/config/editor_presets.go).
```yaml
os:
editCommand: 'code'
editCommandTemplate: '{{editor}} --goto -- {{filename}}:{{line}}'
```
`{{editor}}` in `editCommandTemplate` is replaced with the value of `editCommand`.
### Overriding default config file location
@@ -321,6 +318,15 @@ or
LG_CONFIG_FILE="$HOME/.base_lg_conf,$HOME/.light_theme_lg_conf" lazygit
```
### Recommended Config Values
for users of VSCode
```yaml
os:
openCommand: 'code -rg {{filename}}'
```
## Color Attributes
For color attributes you can choose an array of attributes (with max one color attribute)
@@ -474,7 +480,7 @@ services:
Where:
- `gitDomain` stands for the domain used by git itself (i.e. the one present on clone URLs), e.g. `git.work.com`
- `provider` is one of `github`, `bitbucket`, `bitbucketServer`, `azuredevops`, `gitlab` or `gitea`
- `provider` is one of `github`, `bitbucket`, `bitbucketServer`, `azuredevops` or `gitlab`
- `webDomain` is the URL where your git service exposes a web interface and APIs, e.g. `gitservice.work.com`
## Predefined commit message prefix
+8 -10
View File
@@ -11,14 +11,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>m</kbd>: view merge/rebase options
<kbd>ctrl+p</kbd>: view custom patch options
<kbd>R</kbd>: refresh
<kbd>?</kbd>: open menu
<kbd>x</kbd>: open menu
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode
<kbd>ctrl+s</kbd>: view filter-by-path options
<kbd>W</kbd>: open diff menu
<kbd>ctrl+e</kbd>: open diff menu
<kbd>@</kbd>: open command log menu
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
<kbd>:</kbd>: execute custom command
@@ -93,6 +92,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
<kbd>d</kbd>: view 'discard changes' options
<kbd>space</kbd>: toggle staged
<kbd>ctrl+b</kbd>: Filter files (staged/unstaged)
@@ -131,7 +131,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>r</kbd>: rebase checked-out branch onto this branch
<kbd>M</kbd>: merge into currently checked out branch
<kbd>f</kbd>: fast-forward this branch from its upstream
<kbd>T</kbd>: create tag
<kbd>g</kbd>: view reset options
<kbd>R</kbd>: rename branch
<kbd>u</kbd>: set/unset upstream
@@ -143,8 +142,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>e</kbd>: edit file
<kbd>o</kbd>: open file
<kbd>◀</kbd>: select previous conflict
<kbd>▶</kbd>: select next conflict
<kbd>◄</kbd>: select previous conflict
<kbd>►</kbd>: select next conflict
<kbd>▲</kbd>: select previous hunk
<kbd>▼</kbd>: select next hunk
<kbd>z</kbd>: undo
@@ -164,8 +163,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Main Panel (Patch Building)
<pre>
<kbd>◀</kbd>: select previous hunk
<kbd>▶</kbd>: select next hunk
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
@@ -179,8 +178,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Main Panel (Staging)
<pre>
<kbd>◀</kbd>: select previous hunk
<kbd>▶</kbd>: select next hunk
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
@@ -215,7 +214,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Remote Branches
<pre>
<kbd>ctrl+o</kbd>: copy branch name to clipboard
<kbd>space</kbd>: checkout
<kbd>n</kbd>: new branch
<kbd>M</kbd>: merge into currently checked out branch
+8 -10
View File
@@ -11,14 +11,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>m</kbd>: view merge/rebase options
<kbd>ctrl+p</kbd>: view custom patch options
<kbd>R</kbd>: リフレッシュ
<kbd>?</kbd>: メニューを開く
<kbd>x</kbd>: メニューを開く
<kbd>+</kbd>: 次のスクリーンモード (normal/half/fullscreen)
<kbd>_</kbd>: 前のスクリーンモード
<kbd>ctrl+s</kbd>: view filter-by-path options
<kbd>W</kbd>: 差分メニューを開く
<kbd>ctrl+e</kbd>: 差分メニューを開く
<kbd>@</kbd>: コマンドログメニューを開く
<kbd>ctrl+w</kbd>: 空白文字の差分の表示有無を切り替え
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
<kbd>:</kbd>: カスタムコマンドを実行
@@ -153,6 +152,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>ctrl+o</kbd>: ファイル名をクリップボードにコピー
<kbd>ctrl+w</kbd>: 空白文字の差分の表示有無を切り替え
<kbd>d</kbd>: view 'discard changes' options
<kbd>space</kbd>: ステージ/アンステージ
<kbd>ctrl+b</kbd>: ファイルをフィルタ (ステージ/アンステージ)
@@ -191,7 +191,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>r</kbd>: rebase checked-out branch onto this branch
<kbd>M</kbd>: 現在のブランチにマージ
<kbd>f</kbd>: fast-forward this branch from its upstream
<kbd>T</kbd>: タグを作成
<kbd>g</kbd>: view reset options
<kbd>R</kbd>: ブランチ名を変更
<kbd>u</kbd>: set/unset upstream
@@ -203,8 +202,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>e</kbd>: ファイルを編集
<kbd>o</kbd>: ファイルを開く
<kbd>◀</kbd>: 前のコンフリクトを選択
<kbd>▶</kbd>: 次のコンフリクトを選択
<kbd>◄</kbd>: 前のコンフリクトを選択
<kbd>►</kbd>: 次のコンフリクトを選択
<kbd>▲</kbd>: 前のhunkを選択
<kbd>▼</kbd>: 次のhunkを選択
<kbd>z</kbd>: アンドゥ
@@ -224,8 +223,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## メインパネル (Patch Building)
<pre>
<kbd>◀</kbd>: 前のhunkを選択
<kbd>▶</kbd>: 次のhunkを選択
<kbd>◄</kbd>: 前のhunkを選択
<kbd>►</kbd>: 次のhunkを選択
<kbd>v</kbd>: 範囲選択を切り替え
<kbd>V</kbd>: 範囲選択を切り替え
<kbd>a</kbd>: hunk選択を切り替え
@@ -239,8 +238,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## メインパネル (Staging)
<pre>
<kbd>◀</kbd>: 前のhunkを選択
<kbd>▶</kbd>: 次のhunkを選択
<kbd>◄</kbd>: 前のhunkを選択
<kbd>►</kbd>: 次のhunkを選択
<kbd>v</kbd>: 範囲選択を切り替え
<kbd>V</kbd>: 範囲選択を切り替え
<kbd>a</kbd>: hunk選択を切り替え
@@ -269,7 +268,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## リモートブランチ
<pre>
<kbd>ctrl+o</kbd>: ブランチ名をクリップボードにコピー
<kbd>space</kbd>: チェックアウト
<kbd>n</kbd>: 新しいブランチを作成
<kbd>M</kbd>: 現在のブランチにマージ
+8 -10
View File
@@ -11,14 +11,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>m</kbd>: view merge/rebase options
<kbd>ctrl+p</kbd>: 커스텀 Patch 옵션 보기
<kbd>R</kbd>: 새로고침
<kbd>?</kbd>: 매뉴 열기
<kbd>x</kbd>: 매뉴 열기
<kbd>+</kbd>: 다음 스크린 모드 (normal/half/fullscreen)
<kbd>_</kbd>: 이전 스크린 모드
<kbd>ctrl+s</kbd>: view filter-by-path options
<kbd>W</kbd>: Diff 메뉴 열기
<kbd>ctrl+e</kbd>: Diff 메뉴 열기
<kbd>@</kbd>: 명령어 로그 메뉴 열기
<kbd>ctrl+w</kbd>: 공백문자를 Diff 뷰에서 표시 여부 전환
<kbd>}</kbd>: diff 보기의 변경 사항 주위에 표시되는 컨텍스트의 크기를 늘리기
<kbd>{</kbd>: diff 보기의 변경 사항 주위에 표시되는 컨텍스트 크기 줄이기
<kbd>:</kbd>: execute custom command
@@ -88,8 +87,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>e</kbd>: 파일 편집
<kbd>o</kbd>: 파일 닫기
<kbd>◀</kbd>: 이전 충돌을 선택
<kbd>▶</kbd>: 다음 충돌을 선택
<kbd>◄</kbd>: 이전 충돌을 선택
<kbd>►</kbd>: 다음 충돌을 선택
<kbd>▲</kbd>: 이전 hunk를 선택
<kbd>▼</kbd>: 다음 hunk를 선택
<kbd>z</kbd>: 되돌리기
@@ -109,8 +108,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## 메인 패널 (Patch Building)
<pre>
<kbd>◀</kbd>: 이전 hunk를 선택
<kbd>▶</kbd>: 다음 hunk를 선택
<kbd>◄</kbd>: 이전 hunk를 선택
<kbd>►</kbd>: 다음 hunk를 선택
<kbd>v</kbd>: 드래그 선택 전환
<kbd>V</kbd>: 드래그 선택 전환
<kbd>a</kbd>: toggle select hunk
@@ -124,8 +123,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## 메인 패널 (Staging)
<pre>
<kbd>◀</kbd>: 이전 hunk를 선택
<kbd>▶</kbd>: 다음 hunk를 선택
<kbd>◄</kbd>: 이전 hunk를 선택
<kbd>►</kbd>: 다음 hunk를 선택
<kbd>v</kbd>: 드래그 선택 전환
<kbd>V</kbd>: 드래그 선택 전환
<kbd>a</kbd>: toggle select hunk
@@ -158,7 +157,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>r</kbd>: 체크아웃된 브랜치를 이 브랜치에 리베이스
<kbd>M</kbd>: 현재 브랜치에 병합
<kbd>f</kbd>: fast-forward this branch from its upstream
<kbd>T</kbd>: 태그를 생성
<kbd>g</kbd>: view reset options
<kbd>R</kbd>: 브랜치 이름 변경
<kbd>u</kbd>: set/unset upstream
@@ -200,7 +198,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## 원격 브랜치
<pre>
<kbd>ctrl+o</kbd>: 브랜치명을 클립보드에 복사
<kbd>space</kbd>: 체크아웃
<kbd>n</kbd>: 새 브랜치 생성
<kbd>M</kbd>: 현재 브랜치에 병합
@@ -274,6 +271,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>ctrl+o</kbd>: 파일명을 클립보드에 복사
<kbd>ctrl+w</kbd>: 공백문자를 Diff 뷰에서 표시 여부 전환
<kbd>d</kbd>: view 'discard changes' options
<kbd>space</kbd>: Staged 전환
<kbd>ctrl+b</kbd>: 파일을 필터하기 (Staged/unstaged)
+8 -10
View File
@@ -11,14 +11,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>m</kbd>: bekijk merge/rebase opties
<kbd>ctrl+p</kbd>: bekijk aangepaste patch opties
<kbd>R</kbd>: verversen
<kbd>?</kbd>: open menu
<kbd>x</kbd>: open menu
<kbd>+</kbd>: volgende scherm modus (normaal/half/groot)
<kbd>_</kbd>: vorige scherm modus
<kbd>ctrl+s</kbd>: bekijk scoping opties
<kbd>W</kbd>: open diff menu
<kbd>ctrl+e</kbd>: open diff menu
<kbd>@</kbd>: open command log menu
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
<kbd>:</kbd>: voer aangepaste commando uit
@@ -46,6 +45,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>ctrl+o</kbd>: kopieer de bestandsnaam naar het klembord
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
<kbd>d</kbd>: bekijk 'veranderingen ongedaan maken' opties
<kbd>space</kbd>: toggle staged
<kbd>ctrl+b</kbd>: Filter files (staged/unstaged)
@@ -84,7 +84,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>r</kbd>: rebase branch
<kbd>M</kbd>: merge in met huidige checked out branch
<kbd>f</kbd>: fast-forward deze branch vanaf zijn upstream
<kbd>T</kbd>: creëer tag
<kbd>g</kbd>: bekijk reset opties
<kbd>R</kbd>: hernoem branch
<kbd>u</kbd>: set/unset upstream
@@ -143,8 +142,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>e</kbd>: verander bestand
<kbd>o</kbd>: open bestand
<kbd>◀</kbd>: selecteer voorgaand conflict
<kbd>▶</kbd>: selecteer volgende conflict
<kbd>◄</kbd>: selecteer voorgaand conflict
<kbd>►</kbd>: selecteer volgende conflict
<kbd>▲</kbd>: selecteer bovenste hunk
<kbd>▼</kbd>: selecteer onderste hunk
<kbd>z</kbd>: ongedaan maken
@@ -164,8 +163,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Patch Bouwen
<pre>
<kbd>◀</kbd>: selecteer de vorige hunk
<kbd>▶</kbd>: selecteer de volgende hunk
<kbd>◄</kbd>: selecteer de vorige hunk
<kbd>►</kbd>: selecteer de volgende hunk
<kbd>v</kbd>: toggle drag selecteer
<kbd>V</kbd>: toggle drag selecteer
<kbd>a</kbd>: toggle selecteer hunk
@@ -194,7 +193,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Remote Branches
<pre>
<kbd>ctrl+o</kbd>: kopieer branch name naar klembord
<kbd>space</kbd>: uitchecken
<kbd>n</kbd>: nieuwe branch
<kbd>M</kbd>: merge in met huidige checked out branch
@@ -218,8 +216,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Staging
<pre>
<kbd>◀</kbd>: selecteer de vorige hunk
<kbd>▶</kbd>: selecteer de volgende hunk
<kbd>◄</kbd>: selecteer de vorige hunk
<kbd>►</kbd>: selecteer de volgende hunk
<kbd>v</kbd>: toggle drag selecteer
<kbd>V</kbd>: toggle drag selecteer
<kbd>a</kbd>: toggle selecteer hunk
+8 -10
View File
@@ -11,14 +11,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>m</kbd>: widok scalenia/opcje zmiany bazy
<kbd>ctrl+p</kbd>: view custom patch options
<kbd>R</kbd>: odśwież
<kbd>?</kbd>: open menu
<kbd>x</kbd>: open menu
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode
<kbd>ctrl+s</kbd>: view filter-by-path options
<kbd>W</kbd>: open diff menu
<kbd>ctrl+e</kbd>: open diff menu
<kbd>@</kbd>: open command log menu
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
<kbd>:</kbd>: wykonaj własną komendę
@@ -91,7 +90,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>r</kbd>: zmiana bazy gałęzi
<kbd>M</kbd>: scal do obecnej gałęzi
<kbd>f</kbd>: fast-forward this branch from its upstream
<kbd>T</kbd>: create tag
<kbd>g</kbd>: wyświetl opcje resetu
<kbd>R</kbd>: rename branch
<kbd>u</kbd>: set/unset upstream
@@ -101,8 +99,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Main Panel (Patch Building)
<pre>
<kbd>◀</kbd>: poprzedni kawałek
<kbd>▶</kbd>: następny kawałek
<kbd>◄</kbd>: poprzedni kawałek
<kbd>►</kbd>: następny kawałek
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
@@ -117,6 +115,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
<kbd>d</kbd>: pokaż opcje porzucania zmian
<kbd>space</kbd>: przełącz stan poczekalni
<kbd>ctrl+b</kbd>: Filter files (staged/unstaged)
@@ -156,8 +155,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Poczekalnia
<pre>
<kbd>◀</kbd>: poprzedni kawałek
<kbd>▶</kbd>: następny kawałek
<kbd>◄</kbd>: poprzedni kawałek
<kbd>►</kbd>: następny kawałek
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
@@ -192,7 +191,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Remote Branches
<pre>
<kbd>ctrl+o</kbd>: copy branch name to clipboard
<kbd>space</kbd>: przełącz
<kbd>n</kbd>: nowa gałąź
<kbd>M</kbd>: scal do obecnej gałęzi
@@ -218,8 +216,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>e</kbd>: edytuj plik
<kbd>o</kbd>: otwórz plik
<kbd>◀</kbd>: poprzedni konflikt
<kbd>▶</kbd>: następny konflikt
<kbd>◄</kbd>: poprzedni konflikt
<kbd>►</kbd>: następny konflikt
<kbd>▲</kbd>: wybierz poprzedni kawałek
<kbd>▼</kbd>: wybierz następny kawałek
<kbd>z</kbd>: cofnij
+8 -10
View File
@@ -11,14 +11,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>m</kbd>: 查看 合并/变基 选项
<kbd>ctrl+p</kbd>: 查看自定义补丁选项
<kbd>R</kbd>: 刷新
<kbd>?</kbd>: 打开菜单
<kbd>x</kbd>: 打开菜单
<kbd>+</kbd>: 下一屏模式(正常/半屏/全屏)
<kbd>_</kbd>: 上一屏模式
<kbd>ctrl+s</kbd>: 查看按路径过滤选项
<kbd>W</kbd>: 打开 diff 菜单
<kbd>ctrl+e</kbd>: 打开 diff 菜单
<kbd>@</kbd>: 打开命令日志菜单
<kbd>ctrl+w</kbd>: 切换是否在差异视图中显示空白字符差异
<kbd>}</kbd>: 扩大差异视图中显示的上下文范围
<kbd>{</kbd>: 缩小差异视图中显示的上下文范围
<kbd>:</kbd>: 执行自定义命令
@@ -73,7 +72,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>r</kbd>: 将已检出的分支变基到该分支
<kbd>M</kbd>: 合并到当前检出的分支
<kbd>f</kbd>: 从上游快进此分支
<kbd>T</kbd>: 创建标签
<kbd>g</kbd>: 查看重置选项
<kbd>R</kbd>: 重命名分支
<kbd>u</kbd>: set/unset upstream
@@ -159,6 +157,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>ctrl+o</kbd>: 将文件名复制到剪贴板
<kbd>ctrl+w</kbd>: 切换是否在差异视图中显示空白字符差异
<kbd>d</kbd>: 查看'放弃更改'选项
<kbd>space</kbd>: 切换暂存状态
<kbd>ctrl+b</kbd>: Filter files (staged/unstaged)
@@ -184,8 +183,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## 构建补丁中
<pre>
<kbd>◀</kbd>: 选择上一个区块
<kbd>▶</kbd>: 选择下一个区块
<kbd>◄</kbd>: 选择上一个区块
<kbd>►</kbd>: 选择下一个区块
<kbd>v</kbd>: 切换拖动选择
<kbd>V</kbd>: 切换拖动选择
<kbd>a</kbd>: 切换选择区块
@@ -212,8 +211,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>e</kbd>: 编辑文件
<kbd>o</kbd>: 打开文件
<kbd>◀</kbd>: 选择上一个冲突
<kbd>▶</kbd>: 选择下一个冲突
<kbd>◄</kbd>: 选择上一个冲突
<kbd>►</kbd>: 选择下一个冲突
<kbd>▲</kbd>: 选择顶部块
<kbd>▼</kbd>: 选择底部块
<kbd>z</kbd>: 撤销
@@ -226,8 +225,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## 正在暂存
<pre>
<kbd>◀</kbd>: 选择上一个区块
<kbd>▶</kbd>: 选择下一个区块
<kbd>◄</kbd>: 选择上一个区块
<kbd>►</kbd>: 选择下一个区块
<kbd>v</kbd>: 切换拖动选择
<kbd>V</kbd>: 切换拖动选择
<kbd>a</kbd>: 切换选择区块
@@ -275,7 +274,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## 远程分支
<pre>
<kbd>ctrl+o</kbd>: 将分支名称复制到剪贴板
<kbd>space</kbd>: 检出
<kbd>n</kbd>: 新分支
<kbd>M</kbd>: 合并到当前检出的分支
+8 -8
View File
@@ -4,21 +4,21 @@ go 1.18
require (
github.com/OpenPeeDeeP/xdg v1.0.0
github.com/atotto/clipboard v0.1.4
github.com/atotto/clipboard v0.1.2
github.com/aybabtme/humanlog v0.4.1
github.com/cli/safeexec v1.0.0
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
github.com/creack/pty v1.1.11
github.com/fsmiamoto/git-todo-parser v0.0.4
github.com/fsmiamoto/git-todo-parser v0.0.2
github.com/fsnotify/fsnotify v1.4.7
github.com/gdamore/tcell/v2 v2.6.0
github.com/gdamore/tcell/v2 v2.5.4
github.com/go-errors/errors v1.4.2
github.com/gookit/color v1.4.2
github.com/imdario/mergo v0.3.11
github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d
github.com/jesseduffield/gocui v0.3.1-0.20230324073941-36f2e87458fa
github.com/jesseduffield/gocui v0.3.1-0.20230105224502-214a0df08da7
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e
@@ -62,14 +62,14 @@ require (
github.com/onsi/ginkgo v1.10.3 // indirect
github.com/onsi/gomega v1.7.1 // indirect
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/rivo/uniseg v0.4.3 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/xanzy/ssh-agent v0.2.1 // indirect
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
golang.org/x/exp v0.0.0-20220318154914-8dddf5d87bd8 // indirect
golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/term v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/sys v0.3.0 // indirect
golang.org/x/term v0.3.0 // indirect
golang.org/x/text v0.5.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)
+13 -19
View File
@@ -8,8 +8,6 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY=
github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aybabtme/humanlog v0.4.1 h1:D8d9um55rrthJsP8IGSHBcti9lTb/XknmDAX6Zy8tek=
github.com/aybabtme/humanlog v0.4.1/go.mod h1:B0bnQX4FTSU3oftPMTTPvENCy8LqixLDvYJA9TUCAGo=
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
@@ -30,15 +28,15 @@ github.com/fatih/color v1.7.1-0.20180516100307-2d684516a886/go.mod h1:Zm6kSWBoL9
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/fsmiamoto/git-todo-parser v0.0.4 h1:fzcGaoAFDHWzJRKw//CSZFrXucsLKplIvOSab3FtWWM=
github.com/fsmiamoto/git-todo-parser v0.0.4/go.mod h1:B+AgTbNE2BARvJqzXygThzqxLIaEWvwr2sxKYYb0Fas=
github.com/fsmiamoto/git-todo-parser v0.0.2 h1:l6Y+9q7jbM+yK/w6kASpHO7ejL9ARCErm3tCEqOT278=
github.com/fsmiamoto/git-todo-parser v0.0.2/go.mod h1:B+AgTbNE2BARvJqzXygThzqxLIaEWvwr2sxKYYb0Fas=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
github.com/gdamore/tcell/v2 v2.6.0 h1:OKbluoP9VYmJwZwq/iLb4BxwKcwGthaa1YNBJIyCySg=
github.com/gdamore/tcell/v2 v2.6.0/go.mod h1:be9omFATkdr0D9qewWW3d+MEvl5dha+Etb5y65J2H8Y=
github.com/gdamore/tcell/v2 v2.5.4 h1:TGU4tSjD3sCL788vFNeJnTdzpNKIw1H5dgLnJRQVv/k=
github.com/gdamore/tcell/v2 v2.5.4/go.mod h1:dZgRy5v4iMobMEcWNYBtREnDZAT9DYmfqIkrgEMxLyw=
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
@@ -74,8 +72,8 @@ github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 h1:EQP2Tv8T
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68/go.mod h1:+LLj9/WUPAP8LqCchs7P+7X0R98HiFujVFANdNaxhGk=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d h1:bO+OmbreIv91rCe8NmscRwhFSqkDJtzWCPV4Y+SQuXE=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
github.com/jesseduffield/gocui v0.3.1-0.20230324073941-36f2e87458fa h1:E9G1mj94rMal1YLaABwdxLUUgKq+xGbElFjHRNaDJUg=
github.com/jesseduffield/gocui v0.3.1-0.20230324073941-36f2e87458fa/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
github.com/jesseduffield/gocui v0.3.1-0.20230105224502-214a0df08da7 h1:a7T87NGCrmFhzWBWvaaPQppjbY9/w0Vl4hoB7RnFaug=
github.com/jesseduffield/gocui v0.3.1-0.20230105224502-214a0df08da7/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0=
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo=
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY=
@@ -138,9 +136,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw=
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/samber/lo v1.31.0 h1:Sfa+/064Tdo4SvlohQUQzBhgSer9v/coGvKQI/XLWAM=
@@ -208,22 +205,19 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+1 -1
View File
@@ -123,7 +123,7 @@ func (app *App) validateGitVersion() (*git_commands.GitVersion, error) {
return nil, minVersionError
}
if version.IsOlderThan(2, 20, 0) {
if version.IsOlderThan(2, 0, 0) {
return nil, minVersionError
}
+49 -249
View File
@@ -1,17 +1,13 @@
package daemon
import (
"encoding/json"
"fmt"
"log"
"os"
"strconv"
"path/filepath"
"strings"
"github.com/fsmiamoto/git-todo-parser/todo"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
"github.com/jesseduffield/lazygit/pkg/env"
)
// Sometimes lazygit will be invoked in daemon mode from a parent lazygit process.
@@ -19,58 +15,33 @@ import (
// For example, if we want to ensure that a git command doesn't hang due to
// waiting for an editor to save a commit message, we can tell git to invoke lazygit
// as the editor via 'GIT_EDITOR=lazygit', and use the env var
// 'LAZYGIT_DAEMON_KIND=1' (exit immediately) to specify that we want to run lazygit
// as a daemon which simply exits immediately.
//
// 'Daemon' is not the best name for this, because it's not a persistent background
// process, but it's close enough.
// 'LAZYGIT_DAEMON_KIND=EXIT_IMMEDIATELY' to specify that we want to run lazygit
// as a daemon which simply exits immediately. Any additional arguments we want
// to pass to a daemon can be done via other env vars.
type DaemonKind int
type DaemonKind string
const (
// for when we fail to parse the daemon kind
DaemonKindUnknown DaemonKind = iota
DaemonKindExitImmediately
DaemonKindCherryPick
DaemonKindMoveTodoUp
DaemonKindMoveTodoDown
DaemonKindInsertBreak
DaemonKindChangeTodoActions
DaemonKindMoveFixupCommitDown
InteractiveRebase DaemonKind = "INTERACTIVE_REBASE"
ExitImmediately DaemonKind = "EXIT_IMMEDIATELY"
)
const (
DaemonKindEnvKey string = "LAZYGIT_DAEMON_KIND"
// Contains json-encoded arguments to the daemon
DaemonInstructionEnvKey string = "LAZYGIT_DAEMON_INSTRUCTION"
RebaseTODOEnvKey string = "LAZYGIT_REBASE_TODO"
)
func getInstruction() Instruction {
jsonData := os.Getenv(DaemonInstructionEnvKey)
mapping := map[DaemonKind]func(string) Instruction{
DaemonKindExitImmediately: deserializeInstruction[*ExitImmediatelyInstruction],
DaemonKindCherryPick: deserializeInstruction[*CherryPickCommitsInstruction],
DaemonKindChangeTodoActions: deserializeInstruction[*ChangeTodoActionsInstruction],
DaemonKindMoveFixupCommitDown: deserializeInstruction[*MoveFixupCommitDownInstruction],
DaemonKindMoveTodoUp: deserializeInstruction[*MoveTodoUpInstruction],
DaemonKindMoveTodoDown: deserializeInstruction[*MoveTodoDownInstruction],
DaemonKindInsertBreak: deserializeInstruction[*InsertBreakInstruction],
}
return mapping[getDaemonKind()](jsonData)
type Daemon interface {
Run() error
}
func Handle(common *common.Common) {
if !InDaemonMode() {
d := getDaemon(common)
if d == nil {
return
}
instruction := getInstruction()
if err := instruction.run(common); err != nil {
if err := d.Run(); err != nil {
log.Fatal(err)
}
@@ -78,229 +49,58 @@ func Handle(common *common.Common) {
}
func InDaemonMode() bool {
return getDaemonKind() != DaemonKindUnknown
return getDaemonKind() != ""
}
func getDaemonKind() DaemonKind {
intValue, err := strconv.Atoi(os.Getenv(DaemonKindEnvKey))
if err != nil {
return DaemonKindUnknown
func getDaemon(common *common.Common) Daemon {
switch getDaemonKind() {
case InteractiveRebase:
return &rebaseDaemon{c: common}
case ExitImmediately:
return &exitImmediatelyDaemon{c: common}
}
return DaemonKind(intValue)
}
// An Instruction is a command to be run by lazygit in daemon mode.
// It is serialized to json and passed to lazygit via environment variables
type Instruction interface {
Kind() DaemonKind
SerializedInstructions() string
// runs the instruction
run(common *common.Common) error
}
func serializeInstruction[T any](instruction T) string {
jsonData, err := json.Marshal(instruction)
if err != nil {
// this should never happen
panic(err)
}
return string(jsonData)
}
func deserializeInstruction[T Instruction](jsonData string) Instruction {
var instruction T
err := json.Unmarshal([]byte(jsonData), &instruction)
if err != nil {
panic(err)
}
return instruction
}
func ToEnvVars(instruction Instruction) []string {
return []string{
fmt.Sprintf("%s=%d", DaemonKindEnvKey, instruction.Kind()),
fmt.Sprintf("%s=%s", DaemonInstructionEnvKey, instruction.SerializedInstructions()),
}
}
type ExitImmediatelyInstruction struct{}
func (self *ExitImmediatelyInstruction) Kind() DaemonKind {
return DaemonKindExitImmediately
}
func (self *ExitImmediatelyInstruction) SerializedInstructions() string {
return serializeInstruction(self)
}
func (self *ExitImmediatelyInstruction) run(common *common.Common) error {
return nil
}
func NewExitImmediatelyInstruction() Instruction {
return &ExitImmediatelyInstruction{}
func getDaemonKind() DaemonKind {
return DaemonKind(os.Getenv(DaemonKindEnvKey))
}
type CherryPickCommitsInstruction struct {
Todo string
type rebaseDaemon struct {
c *common.Common
}
func NewCherryPickCommitsInstruction(commits []*models.Commit) Instruction {
todoLines := lo.Map(commits, func(commit *models.Commit, _ int) TodoLine {
return TodoLine{
Action: "pick",
Commit: commit,
func (self *rebaseDaemon) Run() error {
self.c.Log.Info("Lazygit invoked as interactive rebase demon")
self.c.Log.Info("args: ", os.Args)
if strings.HasSuffix(os.Args[1], "git-rebase-todo") {
if err := os.WriteFile(os.Args[1], []byte(os.Getenv(RebaseTODOEnvKey)), 0o644); err != nil {
return err
}
})
todo := TodoLinesToString(todoLines)
return &CherryPickCommitsInstruction{
Todo: todo,
} else if strings.HasSuffix(os.Args[1], filepath.Join(gitDir(), "COMMIT_EDITMSG")) { // TODO: test
// if we are rebasing and squashing, we'll see a COMMIT_EDITMSG
// but in this case we don't need to edit it, so we'll just return
} else {
self.c.Log.Info("Lazygit demon did not match on any use cases")
}
return nil
}
func (self *CherryPickCommitsInstruction) Kind() DaemonKind {
return DaemonKindCherryPick
}
func (self *CherryPickCommitsInstruction) SerializedInstructions() string {
return serializeInstruction(self)
}
func (self *CherryPickCommitsInstruction) run(common *common.Common) error {
return handleInteractiveRebase(common, func(path string) error {
return utils.PrependStrToTodoFile(path, []byte(self.Todo))
})
}
type ChangeTodoActionsInstruction struct {
Changes []ChangeTodoAction
}
func NewChangeTodoActionsInstruction(changes []ChangeTodoAction) Instruction {
return &ChangeTodoActionsInstruction{
Changes: changes,
func gitDir() string {
dir := env.GetGitDirEnv()
if dir == "" {
return ".git"
}
return dir
}
func (self *ChangeTodoActionsInstruction) Kind() DaemonKind {
return DaemonKindChangeTodoActions
type exitImmediatelyDaemon struct {
c *common.Common
}
func (self *ChangeTodoActionsInstruction) SerializedInstructions() string {
return serializeInstruction(self)
}
func (self *ChangeTodoActionsInstruction) run(common *common.Common) error {
return handleInteractiveRebase(common, func(path string) error {
for _, c := range self.Changes {
if err := utils.EditRebaseTodo(path, c.Sha, todo.Pick, c.NewAction); err != nil {
return err
}
}
return nil
})
}
// Takes the sha of some commit, and the sha of a fixup commit that was created
// at the end of the branch, then moves the fixup commit down to right after the
// original commit, changing its type to "fixup"
type MoveFixupCommitDownInstruction struct {
OriginalSha string
FixupSha string
}
func NewMoveFixupCommitDownInstruction(originalSha string, fixupSha string) Instruction {
return &MoveFixupCommitDownInstruction{
OriginalSha: originalSha,
FixupSha: fixupSha,
}
}
func (self *MoveFixupCommitDownInstruction) Kind() DaemonKind {
return DaemonKindMoveFixupCommitDown
}
func (self *MoveFixupCommitDownInstruction) SerializedInstructions() string {
return serializeInstruction(self)
}
func (self *MoveFixupCommitDownInstruction) run(common *common.Common) error {
return handleInteractiveRebase(common, func(path string) error {
return utils.MoveFixupCommitDown(path, self.OriginalSha, self.FixupSha)
})
}
type MoveTodoUpInstruction struct {
Sha string
}
func NewMoveTodoUpInstruction(sha string) Instruction {
return &MoveTodoUpInstruction{
Sha: sha,
}
}
func (self *MoveTodoUpInstruction) Kind() DaemonKind {
return DaemonKindMoveTodoUp
}
func (self *MoveTodoUpInstruction) SerializedInstructions() string {
return serializeInstruction(self)
}
func (self *MoveTodoUpInstruction) run(common *common.Common) error {
return handleInteractiveRebase(common, func(path string) error {
return utils.MoveTodoUp(path, self.Sha, todo.Pick)
})
}
type MoveTodoDownInstruction struct {
Sha string
}
func NewMoveTodoDownInstruction(sha string) Instruction {
return &MoveTodoDownInstruction{
Sha: sha,
}
}
func (self *MoveTodoDownInstruction) Kind() DaemonKind {
return DaemonKindMoveTodoDown
}
func (self *MoveTodoDownInstruction) SerializedInstructions() string {
return serializeInstruction(self)
}
func (self *MoveTodoDownInstruction) run(common *common.Common) error {
return handleInteractiveRebase(common, func(path string) error {
return utils.MoveTodoDown(path, self.Sha, todo.Pick)
})
}
type InsertBreakInstruction struct{}
func NewInsertBreakInstruction() Instruction {
return &InsertBreakInstruction{}
}
func (self *InsertBreakInstruction) Kind() DaemonKind {
return DaemonKindInsertBreak
}
func (self *InsertBreakInstruction) SerializedInstructions() string {
return serializeInstruction(self)
}
func (self *InsertBreakInstruction) run(common *common.Common) error {
return handleInteractiveRebase(common, func(path string) error {
return utils.PrependStrToTodoFile(path, []byte("break\n"))
})
func (self *exitImmediatelyDaemon) Run() error {
return nil
}
-64
View File
@@ -1,64 +0,0 @@
package daemon
import (
"os"
"path/filepath"
"strings"
"github.com/fsmiamoto/git-todo-parser/todo"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/env"
)
type TodoLine struct {
Action string
Commit *models.Commit
}
func (self *TodoLine) ToString() string {
if self.Action == "break" {
return self.Action + "\n"
} else {
return self.Action + " " + self.Commit.Sha + " " + self.Commit.Name + "\n"
}
}
func TodoLinesToString(todoLines []TodoLine) string {
lines := slices.Map(todoLines, func(todoLine TodoLine) string {
return todoLine.ToString()
})
return strings.Join(slices.Reverse(lines), "")
}
type ChangeTodoAction struct {
Sha string
NewAction todo.TodoCommand
}
func handleInteractiveRebase(common *common.Common, f func(path string) error) error {
common.Log.Info("Lazygit invoked as interactive rebase demon")
common.Log.Info("args: ", os.Args)
path := os.Args[1]
if strings.HasSuffix(path, "git-rebase-todo") {
return f(path)
} else if strings.HasSuffix(path, filepath.Join(gitDir(), "COMMIT_EDITMSG")) { // TODO: test
// if we are rebasing and squashing, we'll see a COMMIT_EDITMSG
// but in this case we don't need to edit it, so we'll just return
} else {
common.Log.Info("Lazygit demon did not match on any use cases")
}
return nil
}
func gitDir() string {
dir := env.GetGitDirEnv()
if dir == "" {
return ".git"
}
return dir
}
+27 -28
View File
@@ -87,34 +87,33 @@ func writeString(file *os.File, str string) {
func localisedTitle(tr *i18n.TranslationSet, str string) string {
contextTitleMap := map[string]string{
"global": tr.GlobalTitle,
"navigation": tr.NavigationTitle,
"branches": tr.BranchesTitle,
"localBranches": tr.LocalBranchesTitle,
"files": tr.FilesTitle,
"status": tr.StatusTitle,
"submodules": tr.SubmodulesTitle,
"subCommits": tr.SubCommitsTitle,
"remoteBranches": tr.RemoteBranchesTitle,
"remotes": tr.RemotesTitle,
"reflogCommits": tr.ReflogCommitsTitle,
"tags": tr.TagsTitle,
"commitFiles": tr.CommitFilesTitle,
"commitMessage": tr.CommitMessageTitle,
"commitDescription": tr.CommitDescriptionTitle,
"commits": tr.CommitsTitle,
"confirmation": tr.ConfirmationTitle,
"information": tr.InformationTitle,
"main": tr.NormalTitle,
"patchBuilding": tr.PatchBuildingTitle,
"mergeConflicts": tr.MergingTitle,
"staging": tr.StagingTitle,
"menu": tr.MenuTitle,
"search": tr.SearchTitle,
"secondary": tr.SecondaryTitle,
"stash": tr.StashTitle,
"suggestions": tr.SuggestionsCheatsheetTitle,
"extras": tr.ExtrasTitle,
"global": tr.GlobalTitle,
"navigation": tr.NavigationTitle,
"branches": tr.BranchesTitle,
"localBranches": tr.LocalBranchesTitle,
"files": tr.FilesTitle,
"status": tr.StatusTitle,
"submodules": tr.SubmodulesTitle,
"subCommits": tr.SubCommitsTitle,
"remoteBranches": tr.RemoteBranchesTitle,
"remotes": tr.RemotesTitle,
"reflogCommits": tr.ReflogCommitsTitle,
"tags": tr.TagsTitle,
"commitFiles": tr.CommitFilesTitle,
"commitMessage": tr.CommitMessageTitle,
"commits": tr.CommitsTitle,
"confirmation": tr.ConfirmationTitle,
"information": tr.InformationTitle,
"main": tr.NormalTitle,
"patchBuilding": tr.PatchBuildingTitle,
"mergeConflicts": tr.MergingTitle,
"staging": tr.StagingTitle,
"menu": tr.MenuTitle,
"search": tr.SearchTitle,
"secondary": tr.SecondaryTitle,
"stash": tr.StashTitle,
"suggestions": tr.SuggestionsCheatsheetTitle,
"extras": tr.ExtrasTitle,
}
title, ok := contextTitleMap[str]
+4 -9
View File
@@ -117,14 +117,9 @@ func NewGitCommandAux(
workingTreeCommands := git_commands.NewWorkingTreeCommands(gitCommon, submoduleCommands, fileLoader)
rebaseCommands := git_commands.NewRebaseCommands(gitCommon, commitCommands, workingTreeCommands)
stashCommands := git_commands.NewStashCommands(gitCommon, fileLoader, workingTreeCommands)
// TODO: have patch builder take workingTreeCommands in its entirety
patchBuilder := patch.NewPatchBuilder(cmn.Log, workingTreeCommands.ApplyPatch,
func(from string, to string, reverse bool, filename string, plain bool) (string, error) {
// TODO: make patch builder take Gui.IgnoreWhitespaceInDiffView into
// account. For now we just pass false.
return workingTreeCommands.ShowFileDiff(from, to, reverse, filename, plain, false)
})
patchCommands := git_commands.NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchBuilder)
// TODO: have patch manager take workingTreeCommands in its entirety
patchManager := patch.NewPatchManager(cmn.Log, workingTreeCommands.ApplyPatch, workingTreeCommands.ShowFileDiff)
patchCommands := git_commands.NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchManager)
bisectCommands := git_commands.NewBisectCommands(gitCommon)
branchLoader := git_commands.NewBranchLoader(cmn, branchCommands.GetRawBranches, branchCommands.CurrentBranchInfo, configCommands)
@@ -133,7 +128,7 @@ func NewGitCommandAux(
reflogCommitLoader := git_commands.NewReflogCommitLoader(cmn, cmd)
remoteLoader := git_commands.NewRemoteLoader(cmn, cmd, repo.Remotes)
stashLoader := git_commands.NewStashLoader(cmn, cmd)
tagLoader := git_commands.NewTagLoader(cmn, cmd)
tagLoader := git_commands.NewTagLoader(cmn, version, cmd)
return &GitCommand{
Branch: branchCommands,
+13 -37
View File
@@ -8,8 +8,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
)
var ErrInvalidCommitIndex = errors.New("invalid commit index")
type CommitCommands struct {
*GitCommon
}
@@ -20,6 +18,11 @@ func NewCommitCommands(gitCommon *GitCommon) *CommitCommands {
}
}
// RewordLastCommit rewords the topmost commit with the given message
func (self *CommitCommands) RewordLastCommit(message string) error {
return self.cmd.New("git commit --allow-empty --amend --only -m " + self.cmd.Quote(message)).Run()
}
// ResetAuthor resets the author of the topmost commit
func (self *CommitCommands) ResetAuthor() error {
return self.cmd.New("git commit --allow-empty --only --no-edit --amend --reset-author").Run()
@@ -42,7 +45,11 @@ func (self *CommitCommands) ResetToCommit(sha string, strength string, envVars [
}
func (self *CommitCommands) CommitCmdObj(message string) oscommands.ICmdObj {
messageArgs := self.commitMessageArgs(message)
splitMessage := strings.Split(message, "\n")
lineArgs := ""
for _, line := range splitMessage {
lineArgs += fmt.Sprintf(" -m %s", self.cmd.Quote(line))
}
skipHookPrefix := self.UserConfig.Git.SkipHookPrefix
noVerifyFlag := ""
@@ -50,23 +57,7 @@ func (self *CommitCommands) CommitCmdObj(message string) oscommands.ICmdObj {
noVerifyFlag = " --no-verify"
}
return self.cmd.New(fmt.Sprintf("git commit%s%s%s", noVerifyFlag, self.signoffFlag(), messageArgs))
}
// RewordLastCommit rewords the topmost commit with the given message
func (self *CommitCommands) RewordLastCommit(message string) error {
messageArgs := self.commitMessageArgs(message)
return self.cmd.New(fmt.Sprintf("git commit --allow-empty --amend --only%s", messageArgs)).Run()
}
func (self *CommitCommands) commitMessageArgs(message string) string {
msg, description, _ := strings.Cut(message, "\n")
descriptionArgs := ""
if description != "" {
descriptionArgs = fmt.Sprintf(" -m %s", self.cmd.Quote(description))
}
return fmt.Sprintf(" -m %s%s", self.cmd.Quote(msg), descriptionArgs)
return self.cmd.New(fmt.Sprintf("git commit%s%s%s", noVerifyFlag, self.signoffFlag(), lineArgs))
}
// runs git commit without the -m argument meaning it will invoke the user's editor
@@ -158,19 +149,14 @@ func (self *CommitCommands) AmendHeadCmdObj() oscommands.ICmdObj {
return self.cmd.New("git commit --amend --no-edit --allow-empty")
}
func (self *CommitCommands) ShowCmdObj(sha string, filterPath string, ignoreWhitespace bool) oscommands.ICmdObj {
func (self *CommitCommands) ShowCmdObj(sha string, filterPath string) oscommands.ICmdObj {
contextSize := self.UserConfig.Git.DiffContextSize
filterPathArg := ""
if filterPath != "" {
filterPathArg = fmt.Sprintf(" -- %s", self.cmd.Quote(filterPath))
}
ignoreWhitespaceArg := ""
if ignoreWhitespace {
ignoreWhitespaceArg = " --ignore-all-space"
}
cmdStr := fmt.Sprintf("git show --submodule --color=%s --unified=%d --stat -p %s%s%s",
self.UserConfig.Git.Paging.ColorArg, contextSize, sha, ignoreWhitespaceArg, filterPathArg)
cmdStr := fmt.Sprintf("git show --submodule --color=%s --unified=%d --no-renames --stat -p %s %s", self.UserConfig.Git.Paging.ColorArg, contextSize, sha, filterPathArg)
return self.cmd.New(cmdStr).DontLog()
}
@@ -187,13 +173,3 @@ func (self *CommitCommands) RevertMerge(sha string, parentNumber int) error {
func (self *CommitCommands) CreateFixupCommit(sha string) error {
return self.cmd.New(fmt.Sprintf("git commit --fixup=%s", sha)).Run()
}
// a value of 0 means the head commit, 1 is the parent commit, etc
func (self *CommitCommands) GetCommitMessageFromHistory(value int) (string, error) {
hash, _ := self.cmd.New(fmt.Sprintf("git log -1 --skip=%d --pretty=%%H", value)).DontLog().RunWithOutput()
formattedHash := strings.TrimSpace(hash)
if len(formattedHash) == 0 {
return "", ErrInvalidCommitIndex
}
return self.GetCommitMessage(formattedHash)
}
+31 -33
View File
@@ -15,6 +15,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/gui/style"
)
// context:
@@ -67,6 +68,10 @@ type GetCommitsOptions struct {
func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit, error) {
commits := []*models.Commit{}
var rebasingCommits []*models.Commit
rebaseMode, err := self.getRebaseMode()
if err != nil {
return nil, err
}
if opts.IncludeRebaseCommits && opts.FilterPath == "" {
var err error
@@ -89,7 +94,7 @@ func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit,
if commit.Sha == firstPushedCommit {
passedFirstPushedCommit = true
}
commit.Status = map[bool]models.CommitStatus{true: models.StatusUnpushed, false: models.StatusPushed}[!passedFirstPushedCommit]
commit.Status = map[bool]string{true: "unpushed", false: "pushed"}[!passedFirstPushedCommit]
commits = append(commits, commit)
return false, nil
})
@@ -101,6 +106,12 @@ func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit,
return commits, nil
}
if rebaseMode != enums.REBASE_MODE_NONE {
currentCommit := commits[len(rebasingCommits)]
youAreHere := style.FgYellow.Sprintf("<-- %s ---", self.Tr.YouAreHere)
currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name)
}
commits, err = self.setCommitMergedStatuses(opts.RefName, commits)
if err != nil {
return nil, err
@@ -113,7 +124,7 @@ func (self *CommitLoader) MergeRebasingCommits(commits []*models.Commit) ([]*mod
// chances are we have as many commits as last time so we'll set the capacity to be the old length
result := make([]*models.Commit, 0, len(commits))
for i, commit := range commits {
if !commit.IsTODO() { // removing the existing rebase commits so we can add the refreshed ones
if commit.Status != "rebasing" { // removing the existing rebase commits so we can add the refreshed ones
result = append(result, commits[i:]...)
break
}
@@ -194,8 +205,8 @@ func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode
return nil, nil
}
commitShas := slices.FilterMap(commits, func(commit *models.Commit) (string, bool) {
return commit.Sha, commit.Sha != ""
commitShas := slices.Map(commits, func(commit *models.Commit) string {
return commit.Sha
})
// note that we're not filtering these as we do non-rebasing commits just because
@@ -209,26 +220,20 @@ func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode
),
).DontLog()
fullCommits := map[string]*models.Commit{}
hydratedCommits := make([]*models.Commit, 0, len(commits))
i := 0
err = cmdObj.RunAndProcessLines(func(line string) (bool, error) {
commit := self.extractCommitFromLine(line)
fullCommits[commit.Sha] = commit
matchingCommit := commits[i]
commit.Action = matchingCommit.Action
commit.Status = matchingCommit.Status
hydratedCommits = append(hydratedCommits, commit)
i++
return false, nil
})
if err != nil {
return nil, err
}
hydratedCommits := make([]*models.Commit, 0, len(commits))
for _, rebasingCommit := range commits {
if rebasingCommit.Sha == "" {
hydratedCommits = append(hydratedCommits, rebasingCommit)
} else if commit := fullCommits[rebasingCommit.Sha]; commit != nil {
commit.Action = rebasingCommit.Action
commit.Status = rebasingCommit.Status
hydratedCommits = append(hydratedCommits, commit)
}
}
return hydratedCommits, nil
}
@@ -311,17 +316,15 @@ func (self *CommitLoader) getInteractiveRebasingCommits() ([]*models.Commit, err
}
for _, t := range todos {
if t.Command == todo.UpdateRef {
t.Msg = strings.TrimPrefix(t.Ref, "refs/heads/")
} else if t.Commit == "" {
if t.Commit == "" {
// Command does not have a commit associated, skip
continue
}
commits = slices.Prepend(commits, &models.Commit{
Sha: t.Commit,
Name: t.Msg,
Status: models.StatusRebasing,
Action: t.Command,
Status: "rebasing",
Action: t.Command.String(),
})
}
@@ -340,7 +343,7 @@ func (self *CommitLoader) commitFromPatch(content string) *models.Commit {
return &models.Commit{
Sha: sha,
Name: name,
Status: models.StatusRebasing,
Status: "rebasing",
}
}
@@ -357,11 +360,11 @@ func (self *CommitLoader) setCommitMergedStatuses(refName string, commits []*mod
if strings.HasPrefix(ancestor, commit.Sha) {
passedAncestor = true
}
if commit.Status != models.StatusPushed {
if commit.Status != "pushed" {
continue
}
if passedAncestor {
commits[i].Status = models.StatusMerged
commits[i].Status = "merged"
}
}
return commits, nil
@@ -398,9 +401,7 @@ func ignoringWarnings(commandOutput string) string {
func (self *CommitLoader) getFirstPushedCommit(refName string) (string, error) {
output, err := self.cmd.
New(
fmt.Sprintf("git merge-base %s %s@{u}",
self.cmd.Quote(refName),
self.cmd.Quote(strings.TrimPrefix(refName, "refs/heads/"))),
fmt.Sprintf("git merge-base %s %s@{u}", self.cmd.Quote(refName), self.cmd.Quote(refName)),
).
DontLog().
RunWithOutput()
@@ -418,11 +419,9 @@ func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj {
limitFlag = " -300"
}
followFlag := ""
filterFlag := ""
if opts.FilterPath != "" {
followFlag = " --follow"
filterFlag = fmt.Sprintf(" %s", self.cmd.Quote(opts.FilterPath))
filterFlag = fmt.Sprintf(" --follow -- %s", self.cmd.Quote(opts.FilterPath))
}
config := self.UserConfig.Git.Log
@@ -438,14 +437,13 @@ func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj {
return self.cmd.New(
fmt.Sprintf(
"git log %s%s%s --oneline %s%s --abbrev=%d%s --no-show-signature --%s",
"git -c log.showSignature=false log %s%s%s --oneline %s%s --abbrev=%d%s",
self.cmd.Quote(opts.RefName),
orderFlag,
allFlag,
prettyFormat,
limitFlag,
40,
followFlag,
filterFlag,
),
).DontLog()
+19 -45
View File
@@ -42,20 +42,7 @@ func TestGetCommits(t *testing.T) {
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
runner: oscommands.NewFakeRunner(t).
Expect(`git merge-base "HEAD" "HEAD"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
Expect(`git log "HEAD" --topo-order --oneline --pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s" --abbrev=40 --no-show-signature --`, "", nil),
expectedCommits: []*models.Commit{},
expectedError: nil,
},
{
testName: "should use proper upstream name for branch",
logOrder: "topo-order",
rebaseMode: enums.REBASE_MODE_NONE,
currentBranchName: "mybranch",
opts: GetCommitsOptions{RefName: "refs/heads/mybranch", IncludeRebaseCommits: false},
runner: oscommands.NewFakeRunner(t).
Expect(`git merge-base "refs/heads/mybranch" "mybranch"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
Expect(`git log "refs/heads/mybranch" --topo-order --oneline --pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s" --abbrev=40 --no-show-signature --`, "", nil),
Expect(`git -c log.showSignature=false log "HEAD" --topo-order --oneline --pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s" --abbrev=40`, "", nil),
expectedCommits: []*models.Commit{},
expectedError: nil,
@@ -70,7 +57,7 @@ func TestGetCommits(t *testing.T) {
// here it's seeing which commits are yet to be pushed
Expect(`git merge-base "HEAD" "HEAD"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
// here it's actually getting all the commits in a formatted form, one per line
Expect(`git log "HEAD" --topo-order --oneline --pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s" --abbrev=40 --no-show-signature --`, commitsOutput, nil).
Expect(`git -c log.showSignature=false log "HEAD" --topo-order --oneline --pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s" --abbrev=40`, commitsOutput, nil).
// here it's seeing where our branch diverged from the master branch so that we can mark that commit and parent commits as 'merged'
Expect(`git merge-base "HEAD" "master"`, "26c07b1ab33860a1a7591a0638f9925ccf497ffa", nil),
@@ -78,8 +65,8 @@ func TestGetCommits(t *testing.T) {
{
Sha: "0eea75e8c631fba6b58135697835d58ba4c18dbc",
Name: "better typing for rebase mode",
Status: models.StatusUnpushed,
Action: models.ActionNone,
Status: "unpushed",
Action: "",
Tags: []string{},
ExtraInfo: "(HEAD -> better-tests)",
AuthorName: "Jesse Duffield",
@@ -92,8 +79,8 @@ func TestGetCommits(t *testing.T) {
{
Sha: "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164",
Name: "fix logging",
Status: models.StatusPushed,
Action: models.ActionNone,
Status: "pushed",
Action: "",
Tags: []string{},
ExtraInfo: "(origin/better-tests)",
AuthorName: "Jesse Duffield",
@@ -106,8 +93,8 @@ func TestGetCommits(t *testing.T) {
{
Sha: "e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c",
Name: "refactor",
Status: models.StatusPushed,
Action: models.ActionNone,
Status: "pushed",
Action: "",
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
@@ -120,8 +107,8 @@ func TestGetCommits(t *testing.T) {
{
Sha: "d8084cd558925eb7c9c38afeed5725c21653ab90",
Name: "WIP",
Status: models.StatusPushed,
Action: models.ActionNone,
Status: "pushed",
Action: "",
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
@@ -134,8 +121,8 @@ func TestGetCommits(t *testing.T) {
{
Sha: "65f910ebd85283b5cce9bf67d03d3f1a9ea3813a",
Name: "WIP",
Status: models.StatusPushed,
Action: models.ActionNone,
Status: "pushed",
Action: "",
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
@@ -148,8 +135,8 @@ func TestGetCommits(t *testing.T) {
{
Sha: "26c07b1ab33860a1a7591a0638f9925ccf497ffa",
Name: "WIP",
Status: models.StatusMerged,
Action: models.ActionNone,
Status: "merged",
Action: "",
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
@@ -162,8 +149,8 @@ func TestGetCommits(t *testing.T) {
{
Sha: "3d4470a6c072208722e5ae9a54bcb9634959a1c5",
Name: "WIP",
Status: models.StatusMerged,
Action: models.ActionNone,
Status: "merged",
Action: "",
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
@@ -176,8 +163,8 @@ func TestGetCommits(t *testing.T) {
{
Sha: "053a66a7be3da43aacdc7aa78e1fe757b82c4dd2",
Name: "refactoring the config struct",
Status: models.StatusMerged,
Action: models.ActionNone,
Status: "merged",
Action: "",
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
@@ -198,20 +185,7 @@ func TestGetCommits(t *testing.T) {
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
runner: oscommands.NewFakeRunner(t).
Expect(`git merge-base "HEAD" "HEAD"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
Expect(`git log "HEAD" --oneline --pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s" --abbrev=40 --no-show-signature --`, "", nil),
expectedCommits: []*models.Commit{},
expectedError: nil,
},
{
testName: "should set filter path",
logOrder: "default",
rebaseMode: enums.REBASE_MODE_NONE,
currentBranchName: "master",
opts: GetCommitsOptions{RefName: "HEAD", FilterPath: "src"},
runner: oscommands.NewFakeRunner(t).
Expect(`git merge-base "HEAD" "HEAD"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
Expect(`git log "HEAD" --oneline --pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s" --abbrev=40 --follow --no-show-signature -- "src"`, "", nil),
Expect(`git -c log.showSignature=false log "HEAD" --oneline --pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s" --abbrev=40`, "", nil),
expectedCommits: []*models.Commit{},
expectedError: nil,
+22 -90
View File
@@ -9,32 +9,12 @@ import (
)
func TestCommitRewordCommit(t *testing.T) {
type scenario struct {
testName string
runner *oscommands.FakeCmdObjRunner
input string
}
scenarios := []scenario{
{
"Single line reword",
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test"}, "", nil),
"test",
},
{
"Multi line reword",
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test", "-m", "line 2\nline 3"}, "", nil),
"test\nline 2\nline 3",
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildCommitCommands(commonDeps{runner: s.runner})
runner := oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test"}, "", nil)
instance := buildCommitCommands(commonDeps{runner: runner})
assert.NoError(t, instance.RewordLastCommit(s.input))
s.runner.CheckForMissingCalls()
})
}
assert.NoError(t, instance.RewordLastCommit("test"))
runner.CheckForMissingCalls()
}
func TestCommitResetToCommit(t *testing.T) {
@@ -197,41 +177,30 @@ func TestCommitCreateFixupCommit(t *testing.T) {
func TestCommitShowCmdObj(t *testing.T) {
type scenario struct {
testName string
filterPath string
contextSize int
ignoreWhitespace bool
expected string
testName string
filterPath string
contextSize int
expected string
}
scenarios := []scenario{
{
testName: "Default case without filter path",
filterPath: "",
contextSize: 3,
ignoreWhitespace: false,
expected: "git show --submodule --color=always --unified=3 --stat -p 1234567890",
testName: "Default case without filter path",
filterPath: "",
contextSize: 3,
expected: "git show --submodule --color=always --unified=3 --no-renames --stat -p 1234567890 ",
},
{
testName: "Default case with filter path",
filterPath: "file.txt",
contextSize: 3,
ignoreWhitespace: false,
expected: `git show --submodule --color=always --unified=3 --stat -p 1234567890 -- "file.txt"`,
testName: "Default case with filter path",
filterPath: "file.txt",
contextSize: 3,
expected: `git show --submodule --color=always --unified=3 --no-renames --stat -p 1234567890 -- "file.txt"`,
},
{
testName: "Show diff with custom context size",
filterPath: "",
contextSize: 77,
ignoreWhitespace: false,
expected: "git show --submodule --color=always --unified=77 --stat -p 1234567890",
},
{
testName: "Show diff, ignoring whitespace",
filterPath: "",
contextSize: 77,
ignoreWhitespace: true,
expected: "git show --submodule --color=always --unified=77 --stat -p 1234567890 --ignore-all-space",
testName: "Show diff with custom context size",
filterPath: "",
contextSize: 77,
expected: "git show --submodule --color=always --unified=77 --no-renames --stat -p 1234567890 ",
},
}
@@ -243,7 +212,7 @@ func TestCommitShowCmdObj(t *testing.T) {
instance := buildCommitCommands(commonDeps{userConfig: userConfig})
cmdStr := instance.ShowCmdObj("1234567890", s.filterPath, s.ignoreWhitespace).ToString()
cmdStr := instance.ShowCmdObj("1234567890", s.filterPath).ToString()
assert.Equal(t, s.expected, cmdStr)
})
}
@@ -294,40 +263,3 @@ Merge pull request #1750 from mark2185/fix-issue-template
})
}
}
func TestGetCommitMessageFromHistory(t *testing.T) {
type scenario struct {
testName string
runner *oscommands.FakeCmdObjRunner
test func(string, error)
}
scenarios := []scenario{
{
"Empty message",
oscommands.NewFakeRunner(t).Expect("git log -1 --skip=2 --pretty=%H", "", nil).Expect("git rev-list --format=%B --max-count=1 ", "", nil),
func(output string, err error) {
assert.Error(t, err)
},
},
{
"Default case to retrieve a commit in history",
oscommands.NewFakeRunner(t).Expect("git log -1 --skip=2 --pretty=%H", "sha3 \n", nil).Expect("git rev-list --format=%B --max-count=1 sha3", `commit sha3
use generics to DRY up context code`, nil),
func(output string, err error) {
assert.NoError(t, err)
assert.Equal(t, "use generics to DRY up context code", output)
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildCommitCommands(commonDeps{runner: s.runner})
output, err := instance.GetCommitMessageFromHistory(2)
s.test(output, err)
})
}
}
+9 -82
View File
@@ -3,10 +3,8 @@ package git_commands
import (
"os"
"strconv"
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -29,7 +27,7 @@ func (self *FileCommands) Cat(fileName string) (string, error) {
return string(buf), nil
}
func (self *FileCommands) GetEditCmdStrLegacy(filename string, lineNumber int) (string, error) {
func (self *FileCommands) GetEditor() (string, error) {
editor := self.UserConfig.OS.EditCommand
if editor == "" {
@@ -52,6 +50,14 @@ func (self *FileCommands) GetEditCmdStrLegacy(filename string, lineNumber int) (
if editor == "" {
return "", errors.New("No editor defined in config file, $GIT_EDITOR, $VISUAL, $EDITOR, or git config")
}
return editor, nil
}
func (self *FileCommands) GetEditCmdStr(filename string, lineNumber int) (string, error) {
editor, err := self.GetEditor()
if err != nil {
return "", err
}
templateValues := map[string]string{
"editor": editor,
@@ -74,82 +80,3 @@ func (self *FileCommands) GetEditCmdStrLegacy(filename string, lineNumber int) (
}
return utils.ResolvePlaceholderString(editCmdTemplate, templateValues), nil
}
func (self *FileCommands) GetEditCmdStr(filename string) (string, bool) {
// Legacy support for old config; to be removed at some point
if self.UserConfig.OS.Edit == "" && self.UserConfig.OS.EditCommandTemplate != "" {
if cmdStr, err := self.GetEditCmdStrLegacy(filename, 1); err == nil {
return cmdStr, true
}
}
template, editInTerminal := config.GetEditTemplate(&self.UserConfig.OS, self.guessDefaultEditor)
templateValues := map[string]string{
"filename": self.cmd.Quote(filename),
}
cmdStr := utils.ResolvePlaceholderString(template, templateValues)
return cmdStr, editInTerminal
}
func (self *FileCommands) GetEditAtLineCmdStr(filename string, lineNumber int) (string, bool) {
// Legacy support for old config; to be removed at some point
if self.UserConfig.OS.EditAtLine == "" && self.UserConfig.OS.EditCommandTemplate != "" {
if cmdStr, err := self.GetEditCmdStrLegacy(filename, lineNumber); err == nil {
return cmdStr, true
}
}
template, editInTerminal := config.GetEditAtLineTemplate(&self.UserConfig.OS, self.guessDefaultEditor)
templateValues := map[string]string{
"filename": self.cmd.Quote(filename),
"line": strconv.Itoa(lineNumber),
}
cmdStr := utils.ResolvePlaceholderString(template, templateValues)
return cmdStr, editInTerminal
}
func (self *FileCommands) GetEditAtLineAndWaitCmdStr(filename string, lineNumber int) string {
// Legacy support for old config; to be removed at some point
if self.UserConfig.OS.EditAtLineAndWait == "" && self.UserConfig.OS.EditCommandTemplate != "" {
if cmdStr, err := self.GetEditCmdStrLegacy(filename, lineNumber); err == nil {
return cmdStr
}
}
template := config.GetEditAtLineAndWaitTemplate(&self.UserConfig.OS, self.guessDefaultEditor)
templateValues := map[string]string{
"filename": self.cmd.Quote(filename),
"line": strconv.Itoa(lineNumber),
}
cmdStr := utils.ResolvePlaceholderString(template, templateValues)
return cmdStr
}
func (self *FileCommands) guessDefaultEditor() string {
// Try to query a few places where editors get configured
editor := self.config.GetCoreEditor()
if editor == "" {
editor = self.os.Getenv("GIT_EDITOR")
}
if editor == "" {
editor = self.os.Getenv("VISUAL")
}
if editor == "" {
editor = self.os.Getenv("EDITOR")
}
if editor != "" {
// At this point, it might be more than just the name of the editor;
// e.g. it might be "code -w" or "vim -u myvim.rc". So assume that
// everything up to the first space is the editor name.
editor = strings.Split(editor, " ")[0]
}
return editor
}
+2 -201
View File
@@ -10,7 +10,7 @@ import (
"github.com/stretchr/testify/assert"
)
func TestEditFileCmdStrLegacy(t *testing.T) {
func TestEditFileCmdStr(t *testing.T) {
type scenario struct {
filename string
configEditCommand string
@@ -172,206 +172,7 @@ func TestEditFileCmdStrLegacy(t *testing.T) {
getenv: s.getenv,
})
s.test(instance.GetEditCmdStrLegacy(s.filename, 1))
s.test(instance.GetEditCmdStr(s.filename, 1))
s.runner.CheckForMissingCalls()
}
}
func TestEditFileCmd(t *testing.T) {
type scenario struct {
filename string
osConfig config.OSConfig
expectedCmdStr string
expectedEditInTerminal bool
}
scenarios := []scenario{
{
filename: "test",
osConfig: config.OSConfig{},
expectedCmdStr: `vim -- "test"`,
expectedEditInTerminal: true,
},
{
filename: "test",
osConfig: config.OSConfig{
Edit: "nano {{filename}}",
},
expectedCmdStr: `nano "test"`,
expectedEditInTerminal: true,
},
{
filename: "file/with space",
osConfig: config.OSConfig{
EditPreset: "sublime",
},
expectedCmdStr: `subl -- "file/with space"`,
expectedEditInTerminal: false,
},
}
for _, s := range scenarios {
userConfig := config.GetDefaultConfig()
userConfig.OS = s.osConfig
instance := buildFileCommands(commonDeps{
userConfig: userConfig,
})
cmdStr, editInTerminal := instance.GetEditCmdStr(s.filename)
assert.Equal(t, s.expectedCmdStr, cmdStr)
assert.Equal(t, s.expectedEditInTerminal, editInTerminal)
}
}
func TestEditFileAtLineCmd(t *testing.T) {
type scenario struct {
filename string
lineNumber int
osConfig config.OSConfig
expectedCmdStr string
expectedEditInTerminal bool
}
scenarios := []scenario{
{
filename: "test",
lineNumber: 42,
osConfig: config.OSConfig{},
expectedCmdStr: `vim +42 -- "test"`,
expectedEditInTerminal: true,
},
{
filename: "test",
lineNumber: 35,
osConfig: config.OSConfig{
EditAtLine: "nano +{{line}} {{filename}}",
},
expectedCmdStr: `nano +35 "test"`,
expectedEditInTerminal: true,
},
{
filename: "file/with space",
lineNumber: 12,
osConfig: config.OSConfig{
EditPreset: "sublime",
},
expectedCmdStr: `subl -- "file/with space":12`,
expectedEditInTerminal: false,
},
}
for _, s := range scenarios {
userConfig := config.GetDefaultConfig()
userConfig.OS = s.osConfig
instance := buildFileCommands(commonDeps{
userConfig: userConfig,
})
cmdStr, editInTerminal := instance.GetEditAtLineCmdStr(s.filename, s.lineNumber)
assert.Equal(t, s.expectedCmdStr, cmdStr)
assert.Equal(t, s.expectedEditInTerminal, editInTerminal)
}
}
func TestEditFileAtLineAndWaitCmd(t *testing.T) {
type scenario struct {
filename string
lineNumber int
osConfig config.OSConfig
expectedCmdStr string
}
scenarios := []scenario{
{
filename: "test",
lineNumber: 42,
osConfig: config.OSConfig{},
expectedCmdStr: `vim +42 -- "test"`,
},
{
filename: "file/with space",
lineNumber: 12,
osConfig: config.OSConfig{
EditPreset: "sublime",
},
expectedCmdStr: `subl --wait -- "file/with space":12`,
},
}
for _, s := range scenarios {
userConfig := config.GetDefaultConfig()
userConfig.OS = s.osConfig
instance := buildFileCommands(commonDeps{
userConfig: userConfig,
})
cmdStr := instance.GetEditAtLineAndWaitCmdStr(s.filename, s.lineNumber)
assert.Equal(t, s.expectedCmdStr, cmdStr)
}
}
func TestGuessDefaultEditor(t *testing.T) {
type scenario struct {
gitConfigMockResponses map[string]string
getenv func(string) string
expectedResult string
}
scenarios := []scenario{
{
gitConfigMockResponses: nil,
getenv: func(env string) string {
return ""
},
expectedResult: "",
},
{
gitConfigMockResponses: map[string]string{"core.editor": "nano"},
getenv: func(env string) string {
return ""
},
expectedResult: "nano",
},
{
gitConfigMockResponses: map[string]string{"core.editor": "code -w"},
getenv: func(env string) string {
return ""
},
expectedResult: "code",
},
{
gitConfigMockResponses: nil,
getenv: func(env string) string {
if env == "VISUAL" {
return "emacs"
}
return ""
},
expectedResult: "emacs",
},
{
gitConfigMockResponses: nil,
getenv: func(env string) string {
if env == "EDITOR" {
return "bbedit -w"
}
return ""
},
expectedResult: "bbedit",
},
}
for _, s := range scenarios {
instance := buildFileCommands(commonDeps{
gitConfig: git_config.NewFakeGitConfig(s.gitConfigMockResponses),
getenv: s.getenv,
})
assert.Equal(t, s.expectedResult, instance.guessDefaultEditor())
}
}
+51 -66
View File
@@ -3,9 +3,7 @@ package git_commands
import (
"fmt"
"github.com/fsmiamoto/git-todo-parser/todo"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/app/daemon"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
@@ -18,7 +16,7 @@ type PatchCommands struct {
status *StatusCommands
stash *StashCommands
PatchBuilder *patch.PatchBuilder
PatchManager *patch.PatchManager
}
func NewPatchCommands(
@@ -27,7 +25,7 @@ func NewPatchCommands(
commit *CommitCommands,
status *StatusCommands,
stash *StashCommands,
patchBuilder *patch.PatchBuilder,
patchManager *patch.PatchManager,
) *PatchCommands {
return &PatchCommands{
GitCommon: gitCommon,
@@ -35,7 +33,7 @@ func NewPatchCommands(
commit: commit,
status: status,
stash: stash,
PatchBuilder: patchBuilder,
PatchManager: patchManager,
}
}
@@ -46,8 +44,10 @@ func (self *PatchCommands) DeletePatchesFromCommit(commits []*models.Commit, com
}
// apply each patch in reverse
if err := self.PatchBuilder.ApplyPatches(true); err != nil {
_ = self.rebase.AbortRebase()
if err := self.PatchManager.ApplyPatches(true); err != nil {
if err := self.rebase.AbortRebase(); err != nil {
return err
}
return err
}
@@ -57,7 +57,7 @@ func (self *PatchCommands) DeletePatchesFromCommit(commits []*models.Commit, com
}
self.rebase.onSuccessfulContinue = func() error {
self.PatchBuilder.Reset()
self.PatchManager.Reset()
return nil
}
@@ -72,9 +72,10 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
}
// apply each patch forward
if err := self.PatchBuilder.ApplyPatches(false); err != nil {
// Don't abort the rebase here; this might cause conflicts, so give
// the user a chance to resolve them
if err := self.PatchManager.ApplyPatches(false); err != nil {
if err := self.rebase.AbortRebase(); err != nil {
return err
}
return err
}
@@ -84,7 +85,7 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
}
self.rebase.onSuccessfulContinue = func() error {
self.PatchBuilder.Reset()
self.PatchManager.Reset()
return nil
}
@@ -105,24 +106,24 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
baseIndex := sourceCommitIdx + 1
changes := []daemon.ChangeTodoAction{
{Sha: commits[sourceCommitIdx].Sha, NewAction: todo.Edit},
{Sha: commits[destinationCommitIdx].Sha, NewAction: todo.Edit},
}
self.os.LogCommand(logTodoChanges(changes), false)
todoLines := self.rebase.BuildTodoLines(commits[0:baseIndex], func(commit *models.Commit, i int) string {
if i == sourceCommitIdx || i == destinationCommitIdx {
return "edit"
} else {
return "pick"
}
})
err := self.rebase.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: commits[baseIndex].Sha,
overrideEditor: true,
instruction: daemon.NewChangeTodoActionsInstruction(changes),
}).Run()
err := self.rebase.PrepareInteractiveRebaseCommand(commits[baseIndex].Sha, todoLines, true).Run()
if err != nil {
return err
}
// apply each patch in reverse
if err := self.PatchBuilder.ApplyPatches(true); err != nil {
_ = self.rebase.AbortRebase()
if err := self.PatchManager.ApplyPatches(true); err != nil {
if err := self.rebase.AbortRebase(); err != nil {
return err
}
return err
}
@@ -131,12 +132,6 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
return err
}
patch, err := self.diffHeadAgainstCommit(commits[sourceCommitIdx])
if err != nil {
_ = self.rebase.AbortRebase()
return err
}
if self.rebase.onSuccessfulContinue != nil {
return errors.New("You are midway through another rebase operation. Please abort to start again")
}
@@ -144,9 +139,10 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
self.rebase.onSuccessfulContinue = func() error {
// now we should be up to the destination, so let's apply forward these patches to that.
// ideally we would ensure we're on the right commit but I'm not sure if that check is necessary
if err := self.rebase.workingTree.ApplyPatch(patch, "index", "3way"); err != nil {
// Don't abort the rebase here; this might cause conflicts, so give
// the user a chance to resolve them
if err := self.PatchManager.ApplyPatches(false); err != nil {
if err := self.rebase.AbortRebase(); err != nil {
return err
}
return err
}
@@ -156,7 +152,7 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
}
self.rebase.onSuccessfulContinue = func() error {
self.PatchBuilder.Reset()
self.PatchManager.Reset()
return nil
}
@@ -177,9 +173,11 @@ func (self *PatchCommands) MovePatchIntoIndex(commits []*models.Commit, commitId
return err
}
if err := self.PatchBuilder.ApplyPatches(true); err != nil {
if err := self.PatchManager.ApplyPatches(true); err != nil {
if self.status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
_ = self.rebase.AbortRebase()
if err := self.rebase.AbortRebase(); err != nil {
return err
}
}
return err
}
@@ -189,21 +187,17 @@ func (self *PatchCommands) MovePatchIntoIndex(commits []*models.Commit, commitId
return err
}
patch, err := self.diffHeadAgainstCommit(commits[commitIdx])
if err != nil {
_ = self.rebase.AbortRebase()
return err
}
if self.rebase.onSuccessfulContinue != nil {
return errors.New("You are midway through another rebase operation. Please abort to start again")
}
self.rebase.onSuccessfulContinue = func() error {
// add patches to index
if err := self.rebase.workingTree.ApplyPatch(patch, "index", "3way"); err != nil {
if err := self.PatchManager.ApplyPatches(false); err != nil {
if self.status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
_ = self.rebase.AbortRebase()
if err := self.rebase.AbortRebase(); err != nil {
return err
}
}
return err
}
@@ -214,7 +208,7 @@ func (self *PatchCommands) MovePatchIntoIndex(commits []*models.Commit, commitId
}
}
self.PatchBuilder.Reset()
self.PatchManager.Reset()
return nil
}
@@ -226,8 +220,10 @@ func (self *PatchCommands) PullPatchIntoNewCommit(commits []*models.Commit, comm
return err
}
if err := self.PatchBuilder.ApplyPatches(true); err != nil {
_ = self.rebase.AbortRebase()
if err := self.PatchManager.ApplyPatches(true); err != nil {
if err := self.rebase.AbortRebase(); err != nil {
return err
}
return err
}
@@ -236,20 +232,18 @@ func (self *PatchCommands) PullPatchIntoNewCommit(commits []*models.Commit, comm
return err
}
patch, err := self.diffHeadAgainstCommit(commits[commitIdx])
if err != nil {
_ = self.rebase.AbortRebase()
return err
}
if err := self.rebase.workingTree.ApplyPatch(patch, "index", "3way"); err != nil {
_ = self.rebase.AbortRebase()
// add patches to index
if err := self.PatchManager.ApplyPatches(false); err != nil {
if err := self.rebase.AbortRebase(); err != nil {
return err
}
return err
}
head_message, _ := self.commit.GetHeadCommitMessage()
new_message := fmt.Sprintf("Split from \"%s\"", head_message)
if err := self.commit.CommitCmdObj(new_message).Run(); err != nil {
err := self.commit.CommitCmdObj(new_message).Run()
if err != nil {
return err
}
@@ -257,15 +251,6 @@ func (self *PatchCommands) PullPatchIntoNewCommit(commits []*models.Commit, comm
return errors.New("You are midway through another rebase operation. Please abort to start again")
}
self.PatchBuilder.Reset()
self.PatchManager.Reset()
return self.rebase.ContinueRebase()
}
// We have just applied a patch in reverse to discard it from a commit; if we
// now try to apply the patch again to move it to a later commit, or to the
// index, then this would conflict "with itself" in case the patch contained
// only some lines of a range of adjacent added lines. To solve this, we
// get the diff of HEAD and the original commit and then apply that.
func (self *PatchCommands) diffHeadAgainstCommit(commit *models.Commit) (string, error) {
return self.cmd.New(fmt.Sprintf("git diff HEAD..%s", commit.Sha)).RunWithOutput()
}
+144 -149
View File
@@ -2,17 +2,15 @@ package git_commands
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/fsmiamoto/git-todo-parser/todo"
"github.com/go-errors/errors"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/app/daemon"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
type RebaseCommands struct {
@@ -36,7 +34,7 @@ func NewRebaseCommands(
}
func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int, message string) error {
if models.IsHeadCommit(commits, index) {
if index == 0 {
// we've selected the top commit so no rebase is required
return self.commit.RewordLastCommit(message)
}
@@ -56,16 +54,12 @@ func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int, me
}
func (self *RebaseCommands) RewordCommitInEditor(commits []*models.Commit, index int) (oscommands.ICmdObj, error) {
changes := []daemon.ChangeTodoAction{{
Sha: commits[index].Sha,
NewAction: todo.Reword,
}}
self.os.LogCommand(logTodoChanges(changes), false)
todo, sha, err := self.BuildSingleActionTodo(commits, index, "reword")
if err != nil {
return nil, err
}
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: getBaseShaOrRoot(commits, index+1),
instruction: daemon.NewChangeTodoActionsInstruction(changes),
}), nil
return self.PrepareInteractiveRebaseCommand(sha, todo, false), nil
}
func (self *RebaseCommands) ResetCommitAuthor(commits []*models.Commit, index int) error {
@@ -81,7 +75,7 @@ func (self *RebaseCommands) SetCommitAuthor(commits []*models.Commit, index int,
}
func (self *RebaseCommands) GenericAmend(commits []*models.Commit, index int, f func() error) error {
if models.IsHeadCommit(commits, index) {
if index == 0 {
// we've selected the top commit so no rebase is required
return f()
}
@@ -101,79 +95,33 @@ func (self *RebaseCommands) GenericAmend(commits []*models.Commit, index int, f
}
func (self *RebaseCommands) MoveCommitDown(commits []*models.Commit, index int) error {
baseShaOrRoot := getBaseShaOrRoot(commits, index+2)
sha := commits[index].Sha
self.os.LogCommand(fmt.Sprintf("Moving TODO down: %s", utils.ShortSha(sha)), false)
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: baseShaOrRoot,
instruction: daemon.NewMoveTodoDownInstruction(sha),
overrideEditor: true,
}).Run()
}
func (self *RebaseCommands) MoveCommitUp(commits []*models.Commit, index int) error {
baseShaOrRoot := getBaseShaOrRoot(commits, index+1)
sha := commits[index].Sha
self.os.LogCommand(fmt.Sprintf("Moving TODO up: %s", utils.ShortSha(sha)), false)
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: baseShaOrRoot,
instruction: daemon.NewMoveTodoUpInstruction(sha),
overrideEditor: true,
}).Run()
}
func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, index int, action todo.TodoCommand) error {
baseIndex := index + 1
if action == todo.Squash || action == todo.Fixup {
baseIndex++
// we must ensure that we have at least two commits after the selected one
if len(commits) <= index+2 {
// assuming they aren't picking the bottom commit
return errors.New(self.Tr.NoRoom)
}
baseShaOrRoot := getBaseShaOrRoot(commits, baseIndex)
orderedCommits := append(commits[0:index], commits[index+1], commits[index])
changes := []daemon.ChangeTodoAction{{
Sha: commits[index].Sha,
NewAction: action,
}}
self.os.LogCommand(logTodoChanges(changes), false)
todoLines := self.BuildTodoLinesSingleAction(orderedCommits, "pick")
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: baseShaOrRoot,
overrideEditor: true,
instruction: daemon.NewChangeTodoActionsInstruction(changes),
}).Run()
return self.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todoLines, true).Run()
}
func (self *RebaseCommands) EditRebase(branchRef string) error {
self.os.LogCommand(fmt.Sprintf("Beginning interactive rebase at '%s'", branchRef), false)
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: branchRef,
instruction: daemon.NewInsertBreakInstruction(),
}).Run()
}
func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, index int, action string) error {
todo, sha, err := self.BuildSingleActionTodo(commits, index, action)
if err != nil {
return err
}
func logTodoChanges(changes []daemon.ChangeTodoAction) string {
changeTodoStr := strings.Join(slices.Map(changes, func(c daemon.ChangeTodoAction) string {
return fmt.Sprintf("%s:%s", c.Sha, c.NewAction)
}), "\n")
return fmt.Sprintf("Changing TODO actions: %s", changeTodoStr)
}
type PrepareInteractiveRebaseCommandOpts struct {
baseShaOrRoot string
instruction daemon.Instruction
overrideEditor bool
return self.PrepareInteractiveRebaseCommand(sha, todo, true).Run()
}
// PrepareInteractiveRebaseCommand returns the cmd for an interactive rebase
// we tell git to run lazygit to edit the todo list, and we pass the client
// lazygit a todo string to write to the todo file
func (self *RebaseCommands) PrepareInteractiveRebaseCommand(opts PrepareInteractiveRebaseCommandOpts) oscommands.ICmdObj {
func (self *RebaseCommands) PrepareInteractiveRebaseCommand(baseSha string, todoLines []TodoLine, overrideEditor bool) oscommands.ICmdObj {
todo := self.buildTodo(todoLines)
ex := oscommands.GetLazygitPath()
debug := "FALSE"
@@ -181,89 +129,134 @@ func (self *RebaseCommands) PrepareInteractiveRebaseCommand(opts PrepareInteract
debug = "TRUE"
}
rebaseMergesArg := " --rebase-merges"
if self.version.IsOlderThan(2, 22, 0) {
rebaseMergesArg = ""
}
cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty --empty=keep --no-autosquash%s %s",
rebaseMergesArg, opts.baseShaOrRoot)
cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty %s", baseSha)
self.Log.WithField("command", cmdStr).Debug("RunCommand")
cmdObj := self.cmd.New(cmdStr)
gitSequenceEditor := ex
if opts.instruction != nil {
cmdObj.AddEnvVars(daemon.ToEnvVars(opts.instruction)...)
} else {
if todo == "" {
gitSequenceEditor = "true"
} else {
self.os.LogCommand(fmt.Sprintf("Creating TODO file for interactive rebase: \n\n%s", todo), false)
}
cmdObj.AddEnvVars(
daemon.DaemonKindEnvKey+"="+string(daemon.InteractiveRebase),
daemon.RebaseTODOEnvKey+"="+todo,
"DEBUG="+debug,
"LANG=en_US.UTF-8", // Force using EN as language
"LC_ALL=en_US.UTF-8", // Force using EN as language
"GIT_SEQUENCE_EDITOR="+gitSequenceEditor,
)
if opts.overrideEditor {
if overrideEditor {
cmdObj.AddEnvVars("GIT_EDITOR=" + ex)
}
return cmdObj
}
// AmendTo amends the given commit with whatever files are staged
func (self *RebaseCommands) AmendTo(commits []*models.Commit, commitIndex int) error {
commit := commits[commitIndex]
// produces TodoLines where every commit is picked (or dropped for merge commits) except for the commit at the given index, which
// will have the given action applied to it.
func (self *RebaseCommands) BuildSingleActionTodo(commits []*models.Commit, actionIndex int, action string) ([]TodoLine, string, error) {
baseIndex := actionIndex + 1
if err := self.commit.CreateFixupCommit(commit.Sha); err != nil {
if len(commits) <= baseIndex {
return nil, "", errors.New(self.Tr.CannotRebaseOntoFirstCommit)
}
if action == "squash" || action == "fixup" {
baseIndex++
if len(commits) <= baseIndex {
return nil, "", errors.New(self.Tr.CannotSquashOntoSecondCommit)
}
}
todoLines := self.BuildTodoLines(commits[0:baseIndex], func(commit *models.Commit, i int) string {
if i == actionIndex {
return action
} else if commit.IsMerge() {
// your typical interactive rebase will actually drop merge commits by default. Damn git CLI, you scary!
// doing this means we don't need to worry about rebasing over merges which always causes problems.
// you typically shouldn't be doing rebases that pass over merge commits anyway.
return "drop"
} else {
return "pick"
}
})
return todoLines, commits[baseIndex].Sha, nil
}
// AmendTo amends the given commit with whatever files are staged
func (self *RebaseCommands) AmendTo(sha string) error {
if err := self.commit.CreateFixupCommit(sha); err != nil {
return err
}
// Get the sha of the commit we just created
fixupSha, err := self.cmd.New("git rev-parse --verify HEAD").RunWithOutput()
return self.SquashAllAboveFixupCommits(sha)
}
// EditRebaseTodo sets the action at a given index in the git-rebase-todo file
func (self *RebaseCommands) EditRebaseTodo(index int, action string) error {
fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo")
bytes, err := os.ReadFile(fileName)
if err != nil {
return err
}
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: getBaseShaOrRoot(commits, commitIndex+1),
overrideEditor: true,
instruction: daemon.NewMoveFixupCommitDownInstruction(commit.Sha, fixupSha),
}).Run()
content := strings.Split(string(bytes), "\n")
commitCount := self.getTodoCommitCount(content)
// we have the most recent commit at the bottom whereas the todo file has
// it at the bottom, so we need to subtract our index from the commit count
contentIndex := commitCount - 1 - index
splitLine := strings.Split(content[contentIndex], " ")
content[contentIndex] = action + " " + strings.Join(splitLine[1:], " ")
result := strings.Join(content, "\n")
return os.WriteFile(fileName, []byte(result), 0o644)
}
// EditRebaseTodo sets the action for a given rebase commit in the git-rebase-todo file
func (self *RebaseCommands) EditRebaseTodo(commit *models.Commit, action todo.TodoCommand) error {
return utils.EditRebaseTodo(
filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo"), commit.Sha, commit.Action, action)
func (self *RebaseCommands) getTodoCommitCount(content []string) int {
// count lines that are not blank and are not comments
commitCount := 0
for _, line := range content {
if line != "" && !strings.HasPrefix(line, "#") {
commitCount++
}
}
return commitCount
}
// MoveTodoDown moves a rebase todo item down by one position
func (self *RebaseCommands) MoveTodoDown(commit *models.Commit) error {
func (self *RebaseCommands) MoveTodoDown(index int) error {
fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo")
return utils.MoveTodoDown(fileName, commit.Sha, commit.Action)
}
bytes, err := os.ReadFile(fileName)
if err != nil {
return err
}
// MoveTodoDown moves a rebase todo item down by one position
func (self *RebaseCommands) MoveTodoUp(commit *models.Commit) error {
fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo")
return utils.MoveTodoUp(fileName, commit.Sha, commit.Action)
content := strings.Split(string(bytes), "\n")
commitCount := self.getTodoCommitCount(content)
contentIndex := commitCount - 1 - index
rearrangedContent := append(content[0:contentIndex-1], content[contentIndex], content[contentIndex-1])
rearrangedContent = append(rearrangedContent, content[contentIndex+1:]...)
result := strings.Join(rearrangedContent, "\n")
return os.WriteFile(fileName, []byte(result), 0o644)
}
// SquashAllAboveFixupCommits squashes all fixup! commits above the given one
func (self *RebaseCommands) SquashAllAboveFixupCommits(commit *models.Commit) error {
shaOrRoot := commit.Sha + "^"
if commit.IsFirstCommit() {
shaOrRoot = "--root"
}
func (self *RebaseCommands) SquashAllAboveFixupCommits(sha string) error {
return self.runSkipEditorCommand(
self.cmd.New(
fmt.Sprintf(
"git rebase --interactive --rebase-merges --autostash --autosquash %s",
shaOrRoot,
"git rebase --interactive --rebase-merges --autostash --autosquash %s^",
sha,
),
),
)
@@ -283,22 +276,17 @@ func (self *RebaseCommands) BeginInteractiveRebaseForCommit(commits []*models.Co
return errors.New(self.Tr.DisabledForGPG)
}
changes := []daemon.ChangeTodoAction{{
Sha: commits[commitIndex].Sha,
NewAction: todo.Edit,
}}
self.os.LogCommand(logTodoChanges(changes), false)
todo, sha, err := self.BuildSingleActionTodo(commits, commitIndex, "edit")
if err != nil {
return err
}
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: getBaseShaOrRoot(commits, commitIndex+1),
overrideEditor: true,
instruction: daemon.NewChangeTodoActionsInstruction(changes),
}).Run()
return self.PrepareInteractiveRebaseCommand(sha, todo, true).Run()
}
// RebaseBranch interactive rebases onto a branch
func (self *RebaseCommands) RebaseBranch(branchName string) error {
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{baseShaOrRoot: branchName}).Run()
return self.PrepareInteractiveRebaseCommand(branchName, nil, false).Run()
}
func (self *RebaseCommands) GenericMergeOrRebaseActionCmdObj(commandType string, command string) oscommands.ICmdObj {
@@ -339,16 +327,14 @@ func (self *RebaseCommands) GenericMergeOrRebaseAction(commandType string, comma
}
func (self *RebaseCommands) runSkipEditorCommand(cmdObj oscommands.ICmdObj) error {
instruction := daemon.NewExitImmediatelyInstruction()
lazyGitPath := oscommands.GetLazygitPath()
return cmdObj.
AddEnvVars(
daemon.DaemonKindEnvKey+"="+string(daemon.ExitImmediately),
"GIT_EDITOR="+lazyGitPath,
"GIT_SEQUENCE_EDITOR="+lazyGitPath,
"EDITOR="+lazyGitPath,
"VISUAL="+lazyGitPath,
).
AddEnvVars(daemon.ToEnvVars(instruction)...).
Run()
}
@@ -382,27 +368,36 @@ func (self *RebaseCommands) DiscardOldFileChanges(commits []*models.Commit, comm
// CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD
func (self *RebaseCommands) CherryPickCommits(commits []*models.Commit) error {
commitLines := lo.Map(commits, func(commit *models.Commit, _ int) string {
return fmt.Sprintf("%s %s", utils.ShortSha(commit.Sha), commit.Name)
todoLines := self.BuildTodoLinesSingleAction(commits, "pick")
return self.PrepareInteractiveRebaseCommand("HEAD", todoLines, false).Run()
}
func (self *RebaseCommands) buildTodo(todoLines []TodoLine) string {
lines := slices.Map(todoLines, func(todoLine TodoLine) string {
return todoLine.ToString()
})
self.os.LogCommand(fmt.Sprintf("Cherry-picking commits:\n%s", strings.Join(commitLines, "\n")), false)
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: "HEAD",
instruction: daemon.NewCherryPickCommitsInstruction(commits),
}).Run()
return strings.Join(slices.Reverse(lines), "")
}
// we can't start an interactive rebase from the first commit without passing the
// '--root' arg
func getBaseShaOrRoot(commits []*models.Commit, index int) string {
// We assume that the commits slice contains the initial commit of the repo.
// Technically this assumption could prove false, but it's unlikely you'll
// be starting a rebase from 300 commits ago (which is the original commit limit
// at time of writing)
if index < len(commits) {
return commits[index].Sha
} else {
return "--root"
}
func (self *RebaseCommands) BuildTodoLines(commits []*models.Commit, f func(*models.Commit, int) string) []TodoLine {
return slices.MapWithIndex(commits, func(commit *models.Commit, i int) TodoLine {
return TodoLine{Action: f(commit, i), Commit: commit}
})
}
func (self *RebaseCommands) BuildTodoLinesSingleAction(commits []*models.Commit, action string) []TodoLine {
return self.BuildTodoLines(commits, func(commit *models.Commit, i int) string {
return action
})
}
type TodoLine struct {
Action string
Commit *models.Commit
}
func (self *TodoLine) ToString() string {
return self.Action + " " + self.Commit.Sha + " " + self.Commit.Name + "\n"
}
+4 -6
View File
@@ -2,7 +2,6 @@ package git_commands
import (
"regexp"
"strconv"
"testing"
"github.com/go-errors/errors"
@@ -27,7 +26,7 @@ func TestRebaseRebaseBranch(t *testing.T) {
testName: "successful rebase",
arg: "master",
runner: oscommands.NewFakeRunner(t).
Expect(`git rebase --interactive --autostash --keep-empty --empty=keep --no-autosquash master`, "", nil),
Expect(`git rebase --interactive --autostash --keep-empty master`, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
@@ -36,7 +35,7 @@ func TestRebaseRebaseBranch(t *testing.T) {
testName: "unsuccessful rebase",
arg: "master",
runner: oscommands.NewFakeRunner(t).
Expect(`git rebase --interactive --autostash --keep-empty --empty=keep --no-autosquash master`, "", errors.New("error")),
Expect(`git rebase --interactive --autostash --keep-empty master`, "", errors.New("error")),
test: func(err error) {
assert.Error(t, err)
},
@@ -63,8 +62,7 @@ func TestRebaseSkipEditorCommand(t *testing.T) {
`^VISUAL=.*$`,
`^EDITOR=.*$`,
`^GIT_EDITOR=.*$`,
`^GIT_SEQUENCE_EDITOR=.*$`,
"^" + daemon.DaemonKindEnvKey + "=" + strconv.Itoa(int(daemon.DaemonKindExitImmediately)) + "$",
"^" + daemon.DaemonKindEnvKey + "=" + string(daemon.ExitImmediately) + "$",
} {
regexStr := regexStr
foundMatch := lo.ContainsBy(envVars, func(envVar string) bool {
@@ -126,7 +124,7 @@ func TestRebaseDiscardOldFileChanges(t *testing.T) {
commitIndex: 0,
fileName: "test999.txt",
runner: oscommands.NewFakeRunner(t).
Expect(`git rebase --interactive --autostash --keep-empty --empty=keep --no-autosquash abcdef`, "", nil).
Expect(`git rebase --interactive --autostash --keep-empty abcdef`, "", nil).
Expect(`git cat-file -e HEAD^:"test999.txt"`, "", nil).
Expect(`git checkout HEAD^ -- "test999.txt"`, "", nil).
Expect(`git commit --amend --no-edit --allow-empty`, "", nil).
@@ -52,7 +52,7 @@ func (self *ReflogCommitLoader) GetReflogCommits(lastReflogCommit *models.Commit
Sha: fields[0],
Name: fields[2],
UnixTimestamp: int64(unixTimestamp),
Status: models.StatusReflog,
Status: "reflog",
Parents: parents,
}
@@ -51,35 +51,35 @@ func TestGetReflogCommits(t *testing.T) {
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from A to B",
Status: models.StatusReflog,
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from B to A",
Status: models.StatusReflog,
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from A to B",
Status: models.StatusReflog,
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from master to A",
Status: models.StatusReflog,
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
{
Sha: "f4ddf2f0d4be4ccc7efa",
Name: "checkout: moving from A to master",
Status: models.StatusReflog,
Status: "reflog",
UnixTimestamp: 1643149435,
Parents: []string{"51baa8c1"},
},
@@ -95,7 +95,7 @@ func TestGetReflogCommits(t *testing.T) {
lastReflogCommit: &models.Commit{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from B to A",
Status: models.StatusReflog,
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
@@ -103,7 +103,7 @@ func TestGetReflogCommits(t *testing.T) {
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from A to B",
Status: models.StatusReflog,
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
@@ -119,7 +119,7 @@ func TestGetReflogCommits(t *testing.T) {
lastReflogCommit: &models.Commit{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from B to A",
Status: models.StatusReflog,
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
@@ -128,7 +128,7 @@ func TestGetReflogCommits(t *testing.T) {
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from A to B",
Status: models.StatusReflog,
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
+5 -1
View File
@@ -29,7 +29,11 @@ func (self *SyncCommands) PushCmdObj(opts PushOpts) (oscommands.ICmdObj, error)
cmdStr := "git push"
if opts.Force {
cmdStr += " --force-with-lease"
if self.version.IsOlderThan(2, 30, 0) {
cmdStr += " --force-with-lease"
} else {
cmdStr += " --force-with-lease --force-if-includes"
}
}
if opts.SetUpstream {
+31 -1
View File
@@ -10,6 +10,7 @@ import (
func TestSyncPush(t *testing.T) {
type scenario struct {
testName string
version *GitVersion
opts PushOpts
test func(oscommands.ICmdObj, error)
}
@@ -17,6 +18,7 @@ func TestSyncPush(t *testing.T) {
scenarios := []scenario{
{
testName: "Push with force disabled",
version: &GitVersion{2, 29, 3, ""},
opts: PushOpts{Force: false},
test: func(cmdObj oscommands.ICmdObj, err error) {
assert.Equal(t, cmdObj.ToString(), "git push")
@@ -25,14 +27,25 @@ func TestSyncPush(t *testing.T) {
},
{
testName: "Push with force enabled",
version: &GitVersion{2, 29, 3, ""},
opts: PushOpts{Force: true},
test: func(cmdObj oscommands.ICmdObj, err error) {
assert.Equal(t, cmdObj.ToString(), "git push --force-with-lease")
assert.NoError(t, err)
},
},
{
testName: "Push with force enabled (>= 2.30.0)",
version: &GitVersion{2, 30, 0, ""},
opts: PushOpts{Force: true},
test: func(cmdObj oscommands.ICmdObj, err error) {
assert.Equal(t, cmdObj.ToString(), "git push --force-with-lease --force-if-includes")
assert.NoError(t, err)
},
},
{
testName: "Push with force disabled, upstream supplied",
version: &GitVersion{2, 29, 3, ""},
opts: PushOpts{
Force: false,
UpstreamRemote: "origin",
@@ -45,6 +58,7 @@ func TestSyncPush(t *testing.T) {
},
{
testName: "Push with force disabled, setting upstream",
version: &GitVersion{2, 29, 3, ""},
opts: PushOpts{
Force: false,
UpstreamRemote: "origin",
@@ -58,6 +72,7 @@ func TestSyncPush(t *testing.T) {
},
{
testName: "Push with force enabled, setting upstream",
version: &GitVersion{2, 29, 3, ""},
opts: PushOpts{
Force: true,
UpstreamRemote: "origin",
@@ -69,8 +84,23 @@ func TestSyncPush(t *testing.T) {
assert.NoError(t, err)
},
},
{
testName: "Push with force enabled, setting upstream (>= 2.30.0)",
version: &GitVersion{2, 30, 0, ""},
opts: PushOpts{
Force: true,
UpstreamRemote: "origin",
UpstreamBranch: "master",
SetUpstream: true,
},
test: func(cmdObj oscommands.ICmdObj, err error) {
assert.Equal(t, cmdObj.ToString(), `git push --force-with-lease --force-if-includes --set-upstream "origin" "master"`)
assert.NoError(t, err)
},
},
{
testName: "Push with remote branch but no origin",
version: &GitVersion{2, 29, 3, ""},
opts: PushOpts{
Force: true,
UpstreamRemote: "",
@@ -87,7 +117,7 @@ func TestSyncPush(t *testing.T) {
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildSyncCommands(commonDeps{})
instance := buildSyncCommands(commonDeps{gitVersion: s.version})
s.test(instance.PushCmdObj(s.opts))
})
}
+5 -13
View File
@@ -14,20 +14,12 @@ func NewTagCommands(gitCommon *GitCommon) *TagCommands {
}
}
func (self *TagCommands) CreateLightweight(tagName string, ref string) error {
if len(ref) > 0 {
return self.cmd.New(fmt.Sprintf("git tag -- %s %s", self.cmd.Quote(tagName), self.cmd.Quote(ref))).Run()
} else {
return self.cmd.New(fmt.Sprintf("git tag -- %s", self.cmd.Quote(tagName))).Run()
}
func (self *TagCommands) CreateLightweight(tagName string, commitSha string) error {
return self.cmd.New(fmt.Sprintf("git tag -- %s %s", self.cmd.Quote(tagName), commitSha)).Run()
}
func (self *TagCommands) CreateAnnotated(tagName, ref, msg string) error {
if len(ref) > 0 {
return self.cmd.New(fmt.Sprintf("git tag %s %s -m %s", self.cmd.Quote(tagName), self.cmd.Quote(ref), self.cmd.Quote(msg))).Run()
} else {
return self.cmd.New(fmt.Sprintf("git tag %s -m %s", self.cmd.Quote(tagName), self.cmd.Quote(msg))).Run()
}
func (self *TagCommands) CreateAnnotated(tagName, commitSha, msg string) error {
return self.cmd.New(fmt.Sprintf("git tag %s %s -m %s", tagName, commitSha, self.cmd.Quote(msg))).Run()
}
func (self *TagCommands) Delete(tagName string) error {
@@ -35,5 +27,5 @@ func (self *TagCommands) Delete(tagName string) error {
}
func (self *TagCommands) Push(remoteName string, tagName string) error {
return self.cmd.New(fmt.Sprintf("git push %s tag %s", self.cmd.Quote(remoteName), self.cmd.Quote(tagName))).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
return self.cmd.New(fmt.Sprintf("git push %s %s", self.cmd.Quote(remoteName), self.cmd.Quote(tagName))).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
}
+15 -17
View File
@@ -1,7 +1,7 @@
package git_commands
import (
"regexp"
"fmt"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/models"
@@ -12,42 +12,40 @@ import (
type TagLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
version *GitVersion
cmd oscommands.ICmdObjBuilder
}
func NewTagLoader(
common *common.Common,
version *GitVersion,
cmd oscommands.ICmdObjBuilder,
) *TagLoader {
return &TagLoader{
Common: common,
cmd: cmd,
Common: common,
version: version,
cmd: cmd,
}
}
func (self *TagLoader) GetTags() ([]*models.Tag, error) {
// get remote branches, sorted by creation date (descending)
// see: https://git-scm.com/docs/git-tag#Documentation/git-tag.txt---sortltkeygt
tagsOutput, err := self.cmd.New(`git tag --list -n --sort=-creatordate`).DontLog().RunWithOutput()
sortKey := "-creatordate"
if self.version.IsOlderThan(2, 7, 0) {
sortKey = "-v:refname"
}
tagsOutput, err := self.cmd.New(fmt.Sprintf(`git tag --list --sort=%s`, sortKey)).DontLog().RunWithOutput()
if err != nil {
return nil, err
}
split := utils.SplitLines(tagsOutput)
lineRegex := regexp.MustCompile(`^([^\s]+)(\s+)?(.*)$`)
tags := slices.Map(split, func(line string) *models.Tag {
matches := lineRegex.FindStringSubmatch(line)
tagName := matches[1]
message := ""
if len(matches) > 3 {
message = matches[3]
}
tags := slices.Map(split, func(tagName string) *models.Tag {
return &models.Tag{
Name: tagName,
Message: message,
Name: tagName,
}
})
+47 -14
View File
@@ -9,14 +9,18 @@ import (
"github.com/stretchr/testify/assert"
)
const tagsOutput = `tag1 this is my message
tag2
tag3 this is my other message
const tagsOutput = `v0.34
v0.33
v0.32.2
v0.32.1
v0.32
testtag
`
func TestGetTags(t *testing.T) {
type scenario struct {
testName string
gitVersion *GitVersion
runner *oscommands.FakeCmdObjRunner
expectedTags []*models.Tag
expectedError error
@@ -24,20 +28,48 @@ func TestGetTags(t *testing.T) {
scenarios := []scenario{
{
testName: "should return no tags if there are none",
testName: "should return no tags if there are none",
gitVersion: &GitVersion{2, 7, 0, ""},
runner: oscommands.NewFakeRunner(t).
Expect(`git tag --list -n --sort=-creatordate`, "", nil),
Expect(`git tag --list --sort=-creatordate`, "", nil),
expectedTags: []*models.Tag{},
expectedError: nil,
},
{
testName: "should return tags if present",
testName: "should return no tags if there are none (< 2.7.0)",
gitVersion: &GitVersion{2, 6, 7, ""},
runner: oscommands.NewFakeRunner(t).
Expect(`git tag --list -n --sort=-creatordate`, tagsOutput, nil),
Expect(`git tag --list --sort=-v:refname`, "", nil),
expectedTags: []*models.Tag{},
expectedError: nil,
},
{
testName: "should return tags if present",
gitVersion: &GitVersion{2, 7, 0, ""},
runner: oscommands.NewFakeRunner(t).
Expect(`git tag --list --sort=-creatordate`, tagsOutput, nil),
expectedTags: []*models.Tag{
{Name: "tag1", Message: "this is my message"},
{Name: "tag2", Message: ""},
{Name: "tag3", Message: "this is my other message"},
{Name: "v0.34"},
{Name: "v0.33"},
{Name: "v0.32.2"},
{Name: "v0.32.1"},
{Name: "v0.32"},
{Name: "testtag"},
},
expectedError: nil,
},
{
testName: "should return tags if present (< 2.7.0)",
gitVersion: &GitVersion{2, 6, 7, ""},
runner: oscommands.NewFakeRunner(t).
Expect(`git tag --list --sort=-v:refname`, tagsOutput, nil),
expectedTags: []*models.Tag{
{Name: "v0.34"},
{Name: "v0.33"},
{Name: "v0.32.2"},
{Name: "v0.32.1"},
{Name: "v0.32"},
{Name: "testtag"},
},
expectedError: nil,
},
@@ -46,10 +78,11 @@ func TestGetTags(t *testing.T) {
for _, scenario := range scenarios {
scenario := scenario
t.Run(scenario.testName, func(t *testing.T) {
loader := &TagLoader{
Common: utils.NewDummyCommon(),
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
}
loader := NewTagLoader(
utils.NewDummyCommon(),
scenario.gitVersion,
oscommands.NewDummyCmdObjBuilder(scenario.runner),
)
tags, err := loader.GetTags()
+1 -5
View File
@@ -32,7 +32,7 @@ func ParseGitVersion(versionStr string) (*GitVersion, error) {
// versionStr should be something like:
// git version 2.39.0
// git version 2.37.1 (Apple Git-137.1)
re := regexp.MustCompile(`[^\d]*(\d+)(\.\d+)?(\.\d+)?(.*)`)
re := regexp.MustCompile(`[^\d]+(\d+)(\.\d+)?(\.\d+)?(.*)`)
matches := re.FindStringSubmatch(versionStr)
if len(matches) < 5 {
@@ -65,7 +65,3 @@ func (v *GitVersion) IsOlderThan(major, minor, patch int) bool {
required := major*1000*1000 + minor*1000 + patch
return actual < required
}
func (v *GitVersion) IsOlderThanVersion(version *GitVersion) bool {
return v.IsOlderThan(version.Major, version.Minor, version.Patch)
}
+5 -14
View File
@@ -287,15 +287,11 @@ func (self *WorkingTreeCommands) SaveTemporaryPatch(patch string) (string, error
// ShowFileDiff get the diff of specified from and to. Typically this will be used for a single commit so it'll be 123abc^..123abc
// but when we're in diff mode it could be any 'from' to any 'to'. The reverse flag is also here thanks to diff mode.
func (self *WorkingTreeCommands) ShowFileDiff(from string, to string, reverse bool, fileName string, plain bool,
ignoreWhitespace bool,
) (string, error) {
return self.ShowFileDiffCmdObj(from, to, reverse, fileName, plain, ignoreWhitespace).RunWithOutput()
func (self *WorkingTreeCommands) ShowFileDiff(from string, to string, reverse bool, fileName string, plain bool) (string, error) {
return self.ShowFileDiffCmdObj(from, to, reverse, fileName, plain).RunWithOutput()
}
func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reverse bool, fileName string, plain bool,
ignoreWhitespace bool,
) oscommands.ICmdObj {
func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reverse bool, fileName string, plain bool) oscommands.ICmdObj {
colorArg := self.UserConfig.Git.Paging.ColorArg
contextSize := self.UserConfig.Git.DiffContextSize
if plain {
@@ -307,16 +303,11 @@ func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reve
reverseFlag = " -R"
}
ignoreWhitespaceFlag := ""
if ignoreWhitespace {
ignoreWhitespaceFlag = " --ignore-all-space"
}
return self.cmd.
New(
fmt.Sprintf(
"git diff --submodule --no-ext-diff --unified=%d --no-renames --color=%s%s%s%s%s -- %s",
contextSize, colorArg, pad(from), pad(to), reverseFlag, ignoreWhitespaceFlag, self.cmd.Quote(fileName)),
"git diff --submodule --no-ext-diff --unified=%d --no-renames --color=%s%s%s%s -- %s",
contextSize, colorArg, pad(from), pad(to), reverseFlag, self.cmd.Quote(fileName)),
).
DontLog()
}
+20 -34
View File
@@ -323,52 +323,38 @@ func TestWorkingTreeDiff(t *testing.T) {
func TestWorkingTreeShowFileDiff(t *testing.T) {
type scenario struct {
testName string
from string
to string
reverse bool
plain bool
ignoreWhitespace bool
contextSize int
runner *oscommands.FakeCmdObjRunner
testName string
from string
to string
reverse bool
plain bool
contextSize int
runner *oscommands.FakeCmdObjRunner
}
const expectedResult = "pretend this is an actual git diff"
scenarios := []scenario{
{
testName: "Default case",
from: "1234567890",
to: "0987654321",
reverse: false,
plain: false,
ignoreWhitespace: false,
contextSize: 3,
testName: "Default case",
from: "1234567890",
to: "0987654321",
reverse: false,
plain: false,
contextSize: 3,
runner: oscommands.NewFakeRunner(t).
Expect(`git diff --submodule --no-ext-diff --unified=3 --no-renames --color=always 1234567890 0987654321 -- "test.txt"`, expectedResult, nil),
},
{
testName: "Show diff with custom context size",
from: "1234567890",
to: "0987654321",
reverse: false,
plain: false,
ignoreWhitespace: false,
contextSize: 123,
testName: "Show diff with custom context size",
from: "1234567890",
to: "0987654321",
reverse: false,
plain: false,
contextSize: 123,
runner: oscommands.NewFakeRunner(t).
Expect(`git diff --submodule --no-ext-diff --unified=123 --no-renames --color=always 1234567890 0987654321 -- "test.txt"`, expectedResult, nil),
},
{
testName: "Default case (ignore whitespace)",
from: "1234567890",
to: "0987654321",
reverse: false,
plain: false,
ignoreWhitespace: true,
contextSize: 3,
runner: oscommands.NewFakeRunner(t).
Expect(`git diff --submodule --no-ext-diff --unified=3 --no-renames --color=always 1234567890 0987654321 --ignore-all-space -- "test.txt"`, expectedResult, nil),
},
}
for _, s := range scenarios {
@@ -379,7 +365,7 @@ func TestWorkingTreeShowFileDiff(t *testing.T) {
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner, userConfig: userConfig})
result, err := instance.ShowFileDiff(s.from, s.to, s.reverse, "test.txt", s.plain, s.ignoreWhitespace)
result, err := instance.ShowFileDiff(s.from, s.to, s.reverse, "test.txt", s.plain)
assert.NoError(t, err)
assert.Equal(t, expectedResult, result)
s.runner.CheckForMissingCalls()
+1 -16
View File
@@ -4,7 +4,7 @@ package hosting_service
// at regoio.herokuapp.com
var defaultUrlRegexStrings = []string{
`^(?:https?|ssh)://[^/]+/(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
`^.*?@.*:(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
`^git@.*:(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
}
var defaultRepoURLTemplate = "https://{{.webDomain}}/{{.owner}}/{{.repo}}"
@@ -64,22 +64,12 @@ var bitbucketServerServiceDef = ServiceDefinition{
repoURLTemplate: "https://{{.webDomain}}/projects/{{.project}}/repos/{{.repo}}",
}
var giteaServiceDef = ServiceDefinition{
provider: "gitea",
pullRequestURLIntoDefaultBranch: "/compare/{{.From}}",
pullRequestURLIntoTargetBranch: "/compare/{{.To}}...{{.From}}",
commitURL: "/commit/{{.CommitSha}}",
regexStrings: defaultUrlRegexStrings,
repoURLTemplate: defaultRepoURLTemplate,
}
var serviceDefinitions = []ServiceDefinition{
githubServiceDef,
bitbucketServiceDef,
gitLabServiceDef,
azdoServiceDef,
bitbucketServerServiceDef,
giteaServiceDef,
}
var defaultServiceDomains = []ServiceDomain{
@@ -103,9 +93,4 @@ var defaultServiceDomains = []ServiceDomain{
gitDomain: "dev.azure.com",
webDomain: "dev.azure.com",
},
{
serviceDefinition: giteaServiceDef,
gitDomain: "try.gitea.io",
webDomain: "try.gitea.io",
},
}
@@ -13,7 +13,7 @@ import (
"github.com/jesseduffield/generics/slices"
)
// This package is for handling logic specific to a git hosting service like github, gitlab, bitbucket, gitea, etc.
// This package is for handling logic specific to a git hosting service like github, gitlab, bitbucket, etc.
// Different git hosting services have different URL formats for when you want to open a PR or view a commit,
// and this package's responsibility is to determine which service you're using based on the remote URL,
// and then which URL you need for whatever use case you have.
@@ -3,8 +3,8 @@ package hosting_service
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/fakes"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/test"
"github.com/stretchr/testify/assert"
)
@@ -86,16 +86,6 @@ func TestGetPullRequestURL(t *testing.T) {
assert.Equal(t, "https://github.com/peter/calculator/compare/feature%2Foperations...feature%2Fsum-operation?expand=1", url)
},
},
{
testName: "Opens a link to new pull request on github with specific target branch (different git username)",
from: "feature/sum-operation",
to: "feature/operations",
remoteUrl: "ssh://org-12345@github.com:peter/calculator.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://github.com/peter/calculator/compare/feature%2Foperations...feature%2Fsum-operation?expand=1", url)
},
},
{
testName: "Opens a link to new pull request on github with https remote url with specific target branch",
from: "feature/sum-operation",
@@ -264,60 +254,6 @@ func TestGetPullRequestURL(t *testing.T) {
assert.Equal(t, "https://mycompany.bitbucket.com/projects/myproject/repos/myrepo/pull-requests?create&targetBranch=dev&sourceBranch=feature%2Fnew", url)
},
},
{
testName: "Opens a link to new pull request on Gitea Server (SSH)",
from: "feature/new",
remoteUrl: "ssh://git@mycompany.gitea.io/myproject/myrepo.git",
configServiceDomains: map[string]string{
// valid configuration for a gitea server URL
"mycompany.gitea.io": "gitea:mycompany.gitea.io",
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://mycompany.gitea.io/myproject/myrepo/compare/feature%2Fnew", url)
},
},
{
testName: "Opens a link to new pull request on Gitea Server (SSH) with specific target",
from: "feature/new",
to: "dev",
remoteUrl: "ssh://git@mycompany.gitea.io/myproject/myrepo.git",
configServiceDomains: map[string]string{
// valid configuration for a gitea server URL
"mycompany.gitea.io": "gitea:mycompany.gitea.io",
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://mycompany.gitea.io/myproject/myrepo/compare/dev...feature%2Fnew", url)
},
},
{
testName: "Opens a link to new pull request on Gitea Server (HTTP)",
from: "feature/new",
remoteUrl: "https://mycompany.gitea.io/myproject/myrepo.git",
configServiceDomains: map[string]string{
// valid configuration for a gitea server URL
"mycompany.gitea.io": "gitea:mycompany.gitea.io",
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://mycompany.gitea.io/myproject/myrepo/compare/feature%2Fnew", url)
},
},
{
testName: "Opens a link to new pull request on Gitea Server (HTTP) with specific target",
from: "feature/new",
to: "dev",
remoteUrl: "https://mycompany.gitea.io/myproject/myrepo.git",
configServiceDomains: map[string]string{
// valid configuration for a gitea server URL
"mycompany.gitea.io": "gitea:mycompany.gitea.io",
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://mycompany.gitea.io/myproject/myrepo/compare/dev...feature%2Fnew", url)
},
},
{
testName: "Throws an error if git service is unsupported",
from: "feature/divide-operation",
@@ -364,7 +300,7 @@ func TestGetPullRequestURL(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature%2Fprofile-page&t=1", url)
},
expectedLoggedErrors: []string{"Unknown git service type: 'noservice'. Expected one of github, bitbucket, gitlab, azuredevops, bitbucketServer, gitea"},
expectedLoggedErrors: []string{"Unknown git service type: 'noservice'. Expected one of github, bitbucket, gitlab, azuredevops, bitbucketServer"},
},
{
testName: "Escapes reserved URL characters in from branch name",
@@ -392,7 +328,7 @@ func TestGetPullRequestURL(t *testing.T) {
s := s
t.Run(s.testName, func(t *testing.T) {
tr := i18n.EnglishTranslationSet()
log := &fakes.FakeFieldLogger{}
log := &test.FakeFieldLogger{}
hostingServiceMgr := NewHostingServiceMgr(log, &tr, s.remoteUrl, s.configServiceDomains)
s.test(hostingServiceMgr.GetPullRequestURL(s.from, s.to))
log.AssertErrors(t, s.expectedLoggedErrors)
+3 -26
View File
@@ -3,37 +3,18 @@ package models
import (
"fmt"
"github.com/fsmiamoto/git-todo-parser/todo"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// Special commit hash for empty tree object
const EmptyTreeCommitHash = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
type CommitStatus int
const (
StatusNone CommitStatus = iota
StatusUnpushed
StatusPushed
StatusMerged
StatusRebasing
StatusSelected
StatusReflog
)
const (
// Conveniently for us, the todo package starts the enum at 1, and given
// that it doesn't have a "none" value, we're setting ours to 0
ActionNone todo.TodoCommand = 0
)
// Commit : A git commit
type Commit struct {
Sha string
Name string
Status CommitStatus
Action todo.TodoCommand
Status string // one of "unpushed", "pushed", "merged", "rebasing" or "selected"
Action string // one of "", "pick", "edit", "squash", "reword", "drop", "fixup"
Tags []string
ExtraInfo string // something like 'HEAD -> master, tag: v0.15.2'
AuthorName string // something like 'Jesse Duffield'
@@ -82,9 +63,5 @@ func (c *Commit) IsMerge() bool {
// returns true if this commit is not actually in the git log but instead
// is from a TODO file for an interactive rebase.
func (c *Commit) IsTODO() bool {
return c.Action != ActionNone
}
func IsHeadCommit(commits []*Commit, index int) bool {
return !commits[index].IsTODO() && (index == 0 || commits[index-1].IsTODO())
return c.Action != ""
}
+1 -4
View File
@@ -3,9 +3,6 @@ package models
// Tag : A git tag
type Tag struct {
Name string
// this is either the first line of the message of an annotated tag, or the
// first line of a commit message for a lightweight tag
Message string
}
func (t *Tag) FullRefName() string {
@@ -25,5 +22,5 @@ func (t *Tag) ID() string {
}
func (t *Tag) Description() string {
return t.Message
return "tag " + t.Name
}
+1 -1
View File
@@ -52,7 +52,7 @@ func (self *CmdObjBuilder) NewFromArgs(args []string) ICmdObj {
}
func (self *CmdObjBuilder) NewShell(commandStr string) ICmdObj {
var quotedCommand string
quotedCommand := ""
// Windows does not seem to like quotes around the command
if self.platform.OS == "windows" {
quotedCommand = strings.NewReplacer(
+11 -24
View File
@@ -323,23 +323,6 @@ func (self *cmdObjRunner) processOutput(reader io.Reader, writer io.Writer, prom
// having a function that returns a function because we need to maintain some state inbetween calls hence the closure
func (self *cmdObjRunner) getCheckForCredentialRequestFunc() func([]byte) (CredentialType, bool) {
var ttyText strings.Builder
prompts := map[string]CredentialType{
`Password:`: Password,
`.+'s password:`: Password,
`Password\s*for\s*'.+':`: Password,
`Username\s*for\s*'.+':`: Username,
`Enter\s*passphrase\s*for\s*key\s*'.+':`: Passphrase,
`Enter\s*PIN\s*for\s*.+\s*key\s*.+:`: PIN,
}
compiledPrompts := map[*regexp.Regexp]CredentialType{}
for pattern, askFor := range prompts {
compiledPattern := regexp.MustCompile(pattern)
compiledPrompts[compiledPattern] = askFor
}
newlineRegex := regexp.MustCompile("\n")
// this function takes each word of output from the command and builds up a string to see if we're being asked for a password
return func(newBytes []byte) (CredentialType, bool) {
_, err := ttyText.Write(newBytes)
@@ -347,18 +330,22 @@ func (self *cmdObjRunner) getCheckForCredentialRequestFunc() func([]byte) (Crede
self.log.Error(err)
}
for pattern, askFor := range compiledPrompts {
if match := pattern.Match([]byte(ttyText.String())); match {
prompts := map[string]CredentialType{
`Password:`: Password,
`.+'s password:`: Password,
`Password\s*for\s*'.+':`: Password,
`Username\s*for\s*'.+':`: Username,
`Enter\s*passphrase\s*for\s*key\s*'.+':`: Passphrase,
`Enter\s*PIN\s*for\s*.+\s*key\s*.+:`: PIN,
}
for pattern, askFor := range prompts {
if match, _ := regexp.MatchString(pattern, ttyText.String()); match {
ttyText.Reset()
return askFor, true
}
}
if indices := newlineRegex.FindIndex([]byte(ttyText.String())); indices != nil {
newText := []byte(ttyText.String()[indices[1]:])
ttyText.Reset()
ttyText.Write(newText)
}
return 0, false
}
}
@@ -1,109 +0,0 @@
package oscommands
import (
"strings"
"testing"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func getRunner() *cmdObjRunner {
log := utils.NewDummyLog()
return &cmdObjRunner{
log: log,
guiIO: NewNullGuiIO(log),
}
}
func TestProcessOutput(t *testing.T) {
defaultPromptUserForCredential := func(ct CredentialType) string {
switch ct {
case Password:
return "password"
case Username:
return "username"
case Passphrase:
return "passphrase"
case PIN:
return "pin"
default:
panic("unexpected credential type")
}
}
scenarios := []struct {
name string
promptUserForCredential func(CredentialType) string
output string
expectedToWrite string
}{
{
name: "no output",
promptUserForCredential: defaultPromptUserForCredential,
output: "",
expectedToWrite: "",
},
{
name: "password prompt",
promptUserForCredential: defaultPromptUserForCredential,
output: "Password:",
expectedToWrite: "password",
},
{
name: "password prompt 2",
promptUserForCredential: defaultPromptUserForCredential,
output: "Bill's password:",
expectedToWrite: "password",
},
{
name: "password prompt 3",
promptUserForCredential: defaultPromptUserForCredential,
output: "Password for 'Bill':",
expectedToWrite: "password",
},
{
name: "username prompt",
promptUserForCredential: defaultPromptUserForCredential,
output: "Username for 'Bill':",
expectedToWrite: "username",
},
{
name: "passphrase prompt",
promptUserForCredential: defaultPromptUserForCredential,
output: "Enter passphrase for key '123':",
expectedToWrite: "passphrase",
},
{
name: "pin prompt",
promptUserForCredential: defaultPromptUserForCredential,
output: "Enter PIN for key '123':",
expectedToWrite: "pin",
},
{
name: "username and password prompt",
promptUserForCredential: defaultPromptUserForCredential,
output: "Password:\nUsername for 'Alice':\n",
expectedToWrite: "passwordusername",
},
{
name: "user submits empty credential",
promptUserForCredential: func(ct CredentialType) string { return "" },
output: "Password:\n",
expectedToWrite: "",
},
}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
runner := getRunner()
reader := strings.NewReader(scenario.output)
writer := &strings.Builder{}
runner.processOutput(reader, writer, scenario.promptUserForCredential)
if writer.String() != scenario.expectedToWrite {
t.Errorf("expected to write '%s' but got '%s'", scenario.expectedToWrite, writer.String())
}
})
}
}
+7 -16
View File
@@ -78,30 +78,21 @@ func FileType(path string) string {
}
func (c *OSCommand) OpenFile(filename string) error {
commandTemplate := c.UserConfig.OS.Open
if commandTemplate == "" {
// Legacy support
commandTemplate = c.UserConfig.OS.OpenCommand
}
if commandTemplate == "" {
commandTemplate = config.GetPlatformDefaultConfig().Open
}
return c.OpenFileAtLine(filename, 1)
}
func (c *OSCommand) OpenFileAtLine(filename string, lineNumber int) error {
commandTemplate := c.UserConfig.OS.OpenCommand
templateValues := map[string]string{
"filename": c.Quote(filename),
"line": fmt.Sprintf("%d", lineNumber),
}
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
return c.Cmd.NewShell(command).Run()
}
func (c *OSCommand) OpenLink(link string) error {
commandTemplate := c.UserConfig.OS.OpenLink
if commandTemplate == "" {
// Legacy support
commandTemplate = c.UserConfig.OS.OpenLinkCommand
}
if commandTemplate == "" {
commandTemplate = config.GetPlatformDefaultConfig().OpenLink
}
commandTemplate := c.UserConfig.OS.OpenLinkCommand
templateValues := map[string]string{
"link": c.Quote(link),
}
@@ -75,7 +75,7 @@ func TestOSCommandOpenFileDarwin(t *testing.T) {
for _, s := range scenarios {
oSCmd := NewDummyOSCommandWithRunner(s.runner)
oSCmd.Platform.OS = "darwin"
oSCmd.UserConfig.OS.Open = "open {{filename}}"
oSCmd.UserConfig.OS.OpenCommand = "open {{filename}}"
s.test(oSCmd.OpenFile(s.filename))
}
@@ -135,7 +135,7 @@ func TestOSCommandOpenFileLinux(t *testing.T) {
for _, s := range scenarios {
oSCmd := NewDummyOSCommandWithRunner(s.runner)
oSCmd.Platform.OS = "linux"
oSCmd.UserConfig.OS.Open = `xdg-open {{filename}} > /dev/null`
oSCmd.UserConfig.OS.OpenCommand = `xdg-open {{filename}} > /dev/null`
s.test(oSCmd.OpenFile(s.filename))
}
@@ -6,7 +6,6 @@ package oscommands
import (
"testing"
"github.com/cli/safeexec"
"github.com/go-errors/errors"
"github.com/stretchr/testify/assert"
)
@@ -20,13 +19,11 @@ func TestOSCommandOpenFileWindows(t *testing.T) {
test func(error)
}
fullCmdPath, _ := safeexec.LookPath("cmd")
scenarios := []scenario{
{
filename: "test",
runner: NewFakeRunner(t).
ExpectArgs([]string{fullCmdPath, "/c", "start", "", "test"}, "", errors.New("error")),
ExpectArgs([]string{"cmd", "/c", "start", "", "test"}, "", errors.New("error")),
test: func(err error) {
assert.Error(t, err)
},
@@ -34,7 +31,7 @@ func TestOSCommandOpenFileWindows(t *testing.T) {
{
filename: "test",
runner: NewFakeRunner(t).
ExpectArgs([]string{fullCmdPath, "/c", "start", "", "test"}, "", nil),
ExpectArgs([]string{"cmd", "/c", "start", "", "test"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
@@ -42,7 +39,7 @@ func TestOSCommandOpenFileWindows(t *testing.T) {
{
filename: "filename with spaces",
runner: NewFakeRunner(t).
ExpectArgs([]string{fullCmdPath, "/c", "start", "", "filename with spaces"}, "", nil),
ExpectArgs([]string{"cmd", "/c", "start", "", "filename with spaces"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
@@ -50,7 +47,7 @@ func TestOSCommandOpenFileWindows(t *testing.T) {
{
filename: "let's_test_with_single_quote",
runner: NewFakeRunner(t).
ExpectArgs([]string{fullCmdPath, "/c", "start", "", "let's_test_with_single_quote"}, "", nil),
ExpectArgs([]string{"cmd", "/c", "start", "", "let's_test_with_single_quote"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
@@ -58,7 +55,7 @@ func TestOSCommandOpenFileWindows(t *testing.T) {
{
filename: "$USER.txt",
runner: NewFakeRunner(t).
ExpectArgs([]string{fullCmdPath, "/c", "start", "", "$USER.txt"}, "", nil),
ExpectArgs([]string{"cmd", "/c", "start", "", "$USER.txt"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
-169
View File
@@ -1,169 +0,0 @@
package patch
import (
"strings"
"github.com/jesseduffield/generics/set"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/samber/lo"
)
type patchPresenter struct {
patch *Patch
// if true, all following fields are ignored
plain bool
isFocused bool
// first line index for selected cursor range
firstLineIndex int
// last line index for selected cursor range
lastLineIndex int
// line indices for tagged lines (e.g. lines added to a custom patch)
incLineIndices *set.Set[int]
}
// formats the patch as a plain string
func formatPlain(patch *Patch) string {
presenter := &patchPresenter{
patch: patch,
plain: true,
incLineIndices: set.New[int](),
}
return presenter.format()
}
func formatRangePlain(patch *Patch, startIdx int, endIdx int) string {
lines := patch.Lines()[startIdx : endIdx+1]
return strings.Join(
lo.Map(lines, func(line *PatchLine, _ int) string {
return line.Content + "\n"
}),
"",
)
}
type FormatViewOpts struct {
IsFocused bool
// first line index for selected cursor range
FirstLineIndex int
// last line index for selected cursor range
LastLineIndex int
// line indices for tagged lines (e.g. lines added to a custom patch)
IncLineIndices *set.Set[int]
}
// formats the patch for rendering within a view, meaning it's coloured and
// highlights selected items
func formatView(patch *Patch, opts FormatViewOpts) string {
includedLineIndices := opts.IncLineIndices
if includedLineIndices == nil {
includedLineIndices = set.New[int]()
}
presenter := &patchPresenter{
patch: patch,
plain: false,
isFocused: opts.IsFocused,
firstLineIndex: opts.FirstLineIndex,
lastLineIndex: opts.LastLineIndex,
incLineIndices: includedLineIndices,
}
return presenter.format()
}
func (self *patchPresenter) format() string {
// if we have no changes in our patch (i.e. no additions or deletions) then
// the patch is effectively empty and we can return an empty string
if !self.patch.ContainsChanges() {
return ""
}
stringBuilder := &strings.Builder{}
lineIdx := 0
appendLine := func(line string) {
_, _ = stringBuilder.WriteString(line + "\n")
lineIdx++
}
appendFormattedLine := func(line string, style style.TextStyle) {
formattedLine := self.formatLine(
line,
style,
lineIdx,
)
appendLine(formattedLine)
}
for _, line := range self.patch.header {
appendFormattedLine(line, theme.DefaultTextColor.SetBold())
}
for _, hunk := range self.patch.hunks {
appendLine(
self.formatLine(
hunk.formatHeaderStart(),
style.FgCyan,
lineIdx,
) +
// we're splitting the line into two parts: the diff header and the context
// We explicitly pass 'included' as false here so that we're only tagging the
// first half of the line as included if the line is indeed included.
self.formatLineAux(
hunk.headerContext,
theme.DefaultTextColor,
lineIdx,
false,
),
)
for _, line := range hunk.bodyLines {
appendFormattedLine(line.Content, self.patchLineStyle(line))
}
}
return stringBuilder.String()
}
func (self *patchPresenter) patchLineStyle(patchLine *PatchLine) style.TextStyle {
switch patchLine.Kind {
case ADDITION:
return style.FgGreen
case DELETION:
return style.FgRed
default:
return theme.DefaultTextColor
}
}
func (self *patchPresenter) formatLine(str string, textStyle style.TextStyle, index int) string {
included := self.incLineIndices.Includes(index)
return self.formatLineAux(str, textStyle, index, included)
}
// 'selected' means you've got it highlighted with your cursor
// 'included' means the line has been included in the patch (only applicable when
// building a patch)
func (self *patchPresenter) formatLineAux(str string, textStyle style.TextStyle, index int, included bool) string {
if self.plain {
return str
}
selected := self.isFocused && index >= self.firstLineIndex && index <= self.lastLineIndex
if selected {
textStyle = textStyle.MergeStyle(theme.SelectedRangeBgColor)
}
firstCharStyle := textStyle
if included {
firstCharStyle = firstCharStyle.MergeStyle(style.BgGreen)
}
if len(str) < 2 {
return firstCharStyle.Sprint(str)
}
return firstCharStyle.Sprint(str[:1]) + textStyle.Sprint(str[1:])
}
+125 -49
View File
@@ -1,67 +1,143 @@
package patch
import "fmt"
import (
"fmt"
"strings"
// Example hunk:
// @@ -16,2 +14,3 @@ func (f *CommitFile) Description() string {
// return f.Name
// -}
// +
// +// test
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
type Hunk struct {
// the line number of the first line in the old file ('16' in the above example)
oldStart int
// the line number of the first line in the new file ('14' in the above example)
newStart int
// the context at the end of the header line (' func (f *CommitFile) Description() string {' in the above example)
headerContext string
// the body of the hunk, excluding the header line
bodyLines []*PatchLine
type PatchHunk struct {
FirstLineIdx int
oldStart int
newStart int
heading string
bodyLines []string
}
// Returns the number of lines in the hunk in the original file ('2' in the above example)
func (self *Hunk) oldLength() int {
return nLinesWithKind(self.bodyLines, []PatchLineKind{CONTEXT, DELETION})
func (hunk *PatchHunk) LastLineIdx() int {
return hunk.FirstLineIdx + len(hunk.bodyLines)
}
// Returns the number of lines in the hunk in the new file ('3' in the above example)
func (self *Hunk) newLength() int {
return nLinesWithKind(self.bodyLines, []PatchLineKind{CONTEXT, ADDITION})
func newHunk(lines []string, firstLineIdx int) *PatchHunk {
header := lines[0]
bodyLines := lines[1:]
oldStart, newStart, heading := headerInfo(header)
return &PatchHunk{
oldStart: oldStart,
newStart: newStart,
heading: heading,
FirstLineIdx: firstLineIdx,
bodyLines: bodyLines,
}
}
// Returns true if the hunk contains any changes (i.e. if it's not just a context hunk).
// We'll end up with a context hunk if we're transforming a patch and one of the hunks
// has no selected lines.
func (self *Hunk) containsChanges() bool {
return nLinesWithKind(self.bodyLines, []PatchLineKind{ADDITION, DELETION}) > 0
func headerInfo(header string) (int, int, string) {
match := hunkHeaderRegexp.FindStringSubmatch(header)
oldStart := utils.MustConvertToInt(match[1])
newStart := utils.MustConvertToInt(match[2])
heading := match[3]
return oldStart, newStart, heading
}
// Returns the number of lines in the hunk, including the header line
func (self *Hunk) lineCount() int {
return len(self.bodyLines) + 1
}
func (hunk *PatchHunk) updatedLines(lineIndices []int, reverse bool) []string {
skippedNewlineMessageIndex := -1
newLines := []string{}
// Returns all lines in the hunk, including the header line
func (self *Hunk) allLines() []*PatchLine {
lines := []*PatchLine{{Content: self.formatHeaderLine(), Kind: HUNK_HEADER}}
lines = append(lines, self.bodyLines...)
return lines
}
lineIdx := hunk.FirstLineIdx
for _, line := range hunk.bodyLines {
lineIdx++ // incrementing at the start to skip the header line
if line == "" {
break
}
isLineSelected := lo.Contains(lineIndices, lineIdx)
// Returns the header line, including the unified diff header and the context
func (self *Hunk) formatHeaderLine() string {
return fmt.Sprintf("%s%s", self.formatHeaderStart(), self.headerContext)
}
firstChar, content := line[:1], line[1:]
transformedFirstChar := transformedFirstChar(firstChar, reverse, isLineSelected)
// Returns the first part of the header line i.e. the unified diff part (excluding any context)
func (self *Hunk) formatHeaderStart() string {
newLengthDisplay := ""
newLength := self.newLength()
// if the new length is 1, it's omitted
if newLength != 1 {
newLengthDisplay = fmt.Sprintf(",%d", newLength)
if isLineSelected || (transformedFirstChar == "\\" && skippedNewlineMessageIndex != lineIdx) || transformedFirstChar == " " {
newLines = append(newLines, transformedFirstChar+content)
continue
}
if transformedFirstChar == "+" {
// we don't want to include the 'newline at end of file' line if it involves an addition we're not including
skippedNewlineMessageIndex = lineIdx + 1
}
}
return fmt.Sprintf("@@ -%d,%d +%d%s @@", self.oldStart, self.oldLength(), self.newStart, newLengthDisplay)
return newLines
}
func transformedFirstChar(firstChar string, reverse bool, isLineSelected bool) string {
if reverse {
if !isLineSelected && firstChar == "+" {
return " "
} else if firstChar == "-" {
return "+"
} else if firstChar == "+" {
return "-"
} else {
return firstChar
}
}
if !isLineSelected && firstChar == "-" {
return " "
}
return firstChar
}
func (hunk *PatchHunk) formatHeader(oldStart int, oldLength int, newStart int, newLength int, heading string) string {
return fmt.Sprintf("@@ -%d,%d +%d,%d @@%s\n", oldStart, oldLength, newStart, newLength, heading)
}
func (hunk *PatchHunk) formatWithChanges(lineIndices []int, reverse bool, startOffset int) (int, string) {
bodyLines := hunk.updatedLines(lineIndices, reverse)
startOffset, header, ok := hunk.updatedHeader(bodyLines, startOffset, reverse)
if !ok {
return startOffset, ""
}
return startOffset, header + strings.Join(bodyLines, "")
}
func (hunk *PatchHunk) updatedHeader(newBodyLines []string, startOffset int, reverse bool) (int, string, bool) {
changeCount := nLinesWithPrefix(newBodyLines, []string{"+", "-"})
oldLength := nLinesWithPrefix(newBodyLines, []string{" ", "-"})
newLength := nLinesWithPrefix(newBodyLines, []string{"+", " "})
if changeCount == 0 {
// if nothing has changed we just return nothing
return startOffset, "", false
}
var oldStart int
if reverse {
oldStart = hunk.newStart
} else {
oldStart = hunk.oldStart
}
var newStartOffset int
// if the hunk went from zero to positive length, we need to increment the starting point by one
// if the hunk went from positive to zero length, we need to decrement the starting point by one
if oldLength == 0 {
newStartOffset = 1
} else if newLength == 0 {
newStartOffset = -1
} else {
newStartOffset = 0
}
newStart := oldStart + startOffset + newStartOffset
newStartOffset = startOffset + newLength - oldLength
formattedHeader := hunk.formatHeader(oldStart, oldLength, newStart, newLength, hunk.heading)
return newStartOffset, formattedHeader, true
}
-85
View File
@@ -1,85 +0,0 @@
package patch
import (
"regexp"
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
)
var hunkHeaderRegexp = regexp.MustCompile(`(?m)^@@ -(\d+)[^\+]+\+(\d+)[^@]+@@(.*)$`)
func Parse(patchStr string) *Patch {
// ignore trailing newline.
lines := strings.Split(strings.TrimSuffix(patchStr, "\n"), "\n")
hunks := []*Hunk{}
patchHeader := []string{}
var currentHunk *Hunk
for _, line := range lines {
if strings.HasPrefix(line, "@@") {
oldStart, newStart, headerContext := headerInfo(line)
currentHunk = &Hunk{
oldStart: oldStart,
newStart: newStart,
headerContext: headerContext,
bodyLines: []*PatchLine{},
}
hunks = append(hunks, currentHunk)
} else if currentHunk != nil {
currentHunk.bodyLines = append(currentHunk.bodyLines, newHunkLine(line))
} else {
patchHeader = append(patchHeader, line)
}
}
return &Patch{
hunks: hunks,
header: patchHeader,
}
}
func headerInfo(header string) (int, int, string) {
match := hunkHeaderRegexp.FindStringSubmatch(header)
oldStart := utils.MustConvertToInt(match[1])
newStart := utils.MustConvertToInt(match[2])
headerContext := match[3]
return oldStart, newStart, headerContext
}
func newHunkLine(line string) *PatchLine {
if line == "" {
return &PatchLine{
Kind: CONTEXT,
Content: "",
}
}
firstChar := line[:1]
kind := parseFirstChar(firstChar)
return &PatchLine{
Kind: kind,
Content: line,
}
}
func parseFirstChar(firstChar string) PatchLineKind {
switch firstChar {
case " ":
return CONTEXT
case "+":
return ADDITION
case "-":
return DELETION
case "\\":
return NEWLINE_MESSAGE
}
return CONTEXT
}
-151
View File
@@ -1,151 +0,0 @@
package patch
import (
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
type Patch struct {
// header of the patch (split on newlines) e.g.
// diff --git a/filename b/filename
// index dcd3485..1ba5540 100644
// --- a/filename
// +++ b/filename
header []string
// hunks of the patch
hunks []*Hunk
}
// Returns a new patch with the specified transformation applied (e.g.
// only selecting a subset of changes).
// Leaves the original patch unchanged.
func (self *Patch) Transform(opts TransformOpts) *Patch {
return transform(self, opts)
}
// Returns the patch as a plain string
func (self *Patch) FormatPlain() string {
return formatPlain(self)
}
// Returns a range of lines from the patch as a plain string (range is inclusive)
func (self *Patch) FormatRangePlain(startIdx int, endIdx int) string {
return formatRangePlain(self, startIdx, endIdx)
}
// Returns the patch as a string with ANSI color codes for displaying in a view
func (self *Patch) FormatView(opts FormatViewOpts) string {
return formatView(self, opts)
}
// Returns the lines of the patch
func (self *Patch) Lines() []*PatchLine {
lines := []*PatchLine{}
for _, line := range self.header {
lines = append(lines, &PatchLine{Content: line, Kind: PATCH_HEADER})
}
for _, hunk := range self.hunks {
lines = append(lines, hunk.allLines()...)
}
return lines
}
// Returns the patch line index of the first line in the given hunk
func (self *Patch) HunkStartIdx(hunkIndex int) int {
hunkIndex = utils.Clamp(hunkIndex, 0, len(self.hunks)-1)
result := len(self.header)
for i := 0; i < hunkIndex; i++ {
result += self.hunks[i].lineCount()
}
return result
}
// Returns the patch line index of the last line in the given hunk
func (self *Patch) HunkEndIdx(hunkIndex int) int {
hunkIndex = utils.Clamp(hunkIndex, 0, len(self.hunks)-1)
return self.HunkStartIdx(hunkIndex) + self.hunks[hunkIndex].lineCount() - 1
}
func (self *Patch) ContainsChanges() bool {
return lo.SomeBy(self.hunks, func(hunk *Hunk) bool {
return hunk.containsChanges()
})
}
// Takes a line index in the patch and returns the line number in the new file.
// If the line is a header line, returns 1.
// If the line is a hunk header line, returns the first file line number in that hunk.
// If the line is out of range below, returns the last file line number in the last hunk.
func (self *Patch) LineNumberOfLine(idx int) int {
if idx < len(self.header) || len(self.hunks) == 0 {
return 1
}
hunkIdx := self.HunkContainingLine(idx)
// cursor out of range, return last file line number
if hunkIdx == -1 {
lastHunk := self.hunks[len(self.hunks)-1]
return lastHunk.newStart + lastHunk.newLength() - 1
}
hunk := self.hunks[hunkIdx]
hunkStartIdx := self.HunkStartIdx(hunkIdx)
idxInHunk := idx - hunkStartIdx
if idxInHunk == 0 {
return hunk.oldStart
}
lines := hunk.bodyLines[:idxInHunk-1]
offset := nLinesWithKind(lines, []PatchLineKind{ADDITION, CONTEXT})
return hunk.oldStart + offset
}
// Returns hunk index containing the line at the given patch line index
func (self *Patch) HunkContainingLine(idx int) int {
for hunkIdx, hunk := range self.hunks {
hunkStartIdx := self.HunkStartIdx(hunkIdx)
if idx >= hunkStartIdx && idx < hunkStartIdx+hunk.lineCount() {
return hunkIdx
}
}
return -1
}
// Returns the patch line index of the next change (i.e. addition or deletion).
func (self *Patch) GetNextChangeIdx(idx int) int {
idx = utils.Clamp(idx, 0, self.LineCount()-1)
lines := self.Lines()
for i, line := range lines[idx:] {
if line.isChange() {
return i + idx
}
}
// there are no changes from the cursor onwards so we'll instead
// return the index of the last change
for i := len(lines) - 1; i >= 0; i-- {
line := lines[i]
if line.isChange() {
return i
}
}
// should not be possible
return 0
}
// Returns the length of the patch in lines
func (self *Patch) LineCount() int {
count := len(self.header)
for _, hunk := range self.hunks {
count += hunk.lineCount()
}
return count
}
-30
View File
@@ -1,30 +0,0 @@
package patch
import "github.com/samber/lo"
type PatchLineKind int
const (
PATCH_HEADER PatchLineKind = iota
HUNK_HEADER
ADDITION
DELETION
CONTEXT
NEWLINE_MESSAGE
)
type PatchLine struct {
Kind PatchLineKind
Content string // something like '+ hello' (note the first character is not removed)
}
func (self *PatchLine) isChange() bool {
return self.Kind == ADDITION || self.Kind == DELETION
}
// Returns the number of lines in the given slice that have one of the given kinds
func nLinesWithKind(lines []*PatchLine, kinds []PatchLineKind) int {
return lo.CountBy(lines, func(line *PatchLine) bool {
return lo.Contains(kinds, line.Kind)
})
}
@@ -33,8 +33,8 @@ type (
loadFileDiffFunc func(from string, to string, reverse bool, filename string, plain bool) (string, error)
)
// PatchBuilder manages the building of a patch for a commit to be applied to another commit (or the working tree, or removed from the current commit). We also support building patches from things like stashes, for which there is less flexibility
type PatchBuilder struct {
// PatchManager manages the building of a patch for a commit to be applied to another commit (or the working tree, or removed from the current commit). We also support building patches from things like stashes, for which there is less flexibility
type PatchManager struct {
// To is the commit sha if we're dealing with files of a commit, or a stash ref for a stash
To string
From string
@@ -53,16 +53,17 @@ type PatchBuilder struct {
loadFileDiff loadFileDiffFunc
}
// NewPatchBuilder returns a new PatchBuilder
func NewPatchBuilder(log *logrus.Entry, applyPatch applyPatchFunc, loadFileDiff loadFileDiffFunc) *PatchBuilder {
return &PatchBuilder{
// NewPatchManager returns a new PatchManager
func NewPatchManager(log *logrus.Entry, applyPatch applyPatchFunc, loadFileDiff loadFileDiffFunc) *PatchManager {
return &PatchManager{
Log: log,
applyPatch: applyPatch,
loadFileDiff: loadFileDiff,
}
}
func (p *PatchBuilder) Start(from, to string, reverse bool, canRebase bool) {
// NewPatchManager returns a new PatchManager
func (p *PatchManager) Start(from, to string, reverse bool, canRebase bool) {
p.To = to
p.From = from
p.reverse = reverse
@@ -70,7 +71,7 @@ func (p *PatchBuilder) Start(from, to string, reverse bool, canRebase bool) {
p.fileInfoMap = map[string]*fileInfo{}
}
func (p *PatchBuilder) addFileWhole(info *fileInfo) {
func (p *PatchManager) addFileWhole(info *fileInfo) {
info.mode = WHOLE
lineCount := len(strings.Split(info.diff, "\n"))
// add every line index
@@ -81,12 +82,12 @@ func (p *PatchBuilder) addFileWhole(info *fileInfo) {
}
}
func (p *PatchBuilder) removeFile(info *fileInfo) {
func (p *PatchManager) removeFile(info *fileInfo) {
info.mode = UNSELECTED
info.includedLineIndices = nil
}
func (p *PatchBuilder) AddFileWhole(filename string) error {
func (p *PatchManager) AddFileWhole(filename string) error {
info, err := p.getFileInfo(filename)
if err != nil {
return err
@@ -97,7 +98,7 @@ func (p *PatchBuilder) AddFileWhole(filename string) error {
return nil
}
func (p *PatchBuilder) RemoveFile(filename string) error {
func (p *PatchManager) RemoveFile(filename string) error {
info, err := p.getFileInfo(filename)
if err != nil {
return err
@@ -116,7 +117,7 @@ func getIndicesForRange(first, last int) []int {
return indices
}
func (p *PatchBuilder) getFileInfo(filename string) (*fileInfo, error) {
func (p *PatchManager) getFileInfo(filename string) (*fileInfo, error) {
info, ok := p.fileInfoMap[filename]
if ok {
return info, nil
@@ -136,7 +137,7 @@ func (p *PatchBuilder) getFileInfo(filename string) (*fileInfo, error) {
return info, nil
}
func (p *PatchBuilder) AddFileLineRange(filename string, firstLineIdx, lastLineIdx int) error {
func (p *PatchManager) AddFileLineRange(filename string, firstLineIdx, lastLineIdx int) error {
info, err := p.getFileInfo(filename)
if err != nil {
return err
@@ -147,7 +148,7 @@ func (p *PatchBuilder) AddFileLineRange(filename string, firstLineIdx, lastLineI
return nil
}
func (p *PatchBuilder) RemoveFileLineRange(filename string, firstLineIdx, lastLineIdx int) error {
func (p *PatchManager) RemoveFileLineRange(filename string, firstLineIdx, lastLineIdx int) error {
info, err := p.getFileInfo(filename)
if err != nil {
return err
@@ -161,46 +162,44 @@ func (p *PatchBuilder) RemoveFileLineRange(filename string, firstLineIdx, lastLi
return nil
}
func (p *PatchBuilder) RenderPatchForFile(filename string, plain bool, reverse bool) string {
func (p *PatchManager) renderPlainPatchForFile(filename string, reverse bool, keepOriginalHeader bool) string {
info, err := p.getFileInfo(filename)
if err != nil {
p.Log.Error(err)
return ""
}
if info.mode == UNSELECTED {
return ""
}
if info.mode == WHOLE && plain {
// Use the whole diff (spares us parsing it and then formatting it).
// TODO: see if this is actually noticeably faster.
// The reverse flag is only for part patches so we're ignoring it here.
switch info.mode {
case WHOLE:
// use the whole diff
// the reverse flag is only for part patches so we're ignoring it here
return info.diff
}
patch := Parse(info.diff).
Transform(TransformOpts{
Reverse: reverse,
IncludedLineIndices: info.includedLineIndices,
})
if plain {
return patch.FormatPlain()
} else {
return patch.FormatView(FormatViewOpts{
IsFocused: false,
})
case PART:
// generate a new diff with just the selected lines
return ModifiedPatchForLines(p.Log, filename, info.diff, info.includedLineIndices, reverse, keepOriginalHeader)
default:
return ""
}
}
func (p *PatchBuilder) renderEachFilePatch(plain bool) []string {
func (p *PatchManager) RenderPatchForFile(filename string, plain bool, reverse bool, keepOriginalHeader bool) string {
patch := p.renderPlainPatchForFile(filename, reverse, keepOriginalHeader)
if plain {
return patch
}
parser := NewPatchParser(p.Log, patch)
// not passing included lines because we don't want to see them in the secondary panel
return parser.Render(false, -1, -1, nil)
}
func (p *PatchManager) renderEachFilePatch(plain bool) []string {
// sort files by name then iterate through and render each patch
filenames := maps.Keys(p.fileInfoMap)
sort.Strings(filenames)
patches := slices.Map(filenames, func(filename string) string {
return p.RenderPatchForFile(filename, plain, false)
return p.RenderPatchForFile(filename, plain, false, true)
})
output := slices.Filter(patches, func(patch string) bool {
return patch != ""
@@ -209,11 +208,17 @@ func (p *PatchBuilder) renderEachFilePatch(plain bool) []string {
return output
}
func (p *PatchBuilder) RenderAggregatedPatch(plain bool) string {
return strings.Join(p.renderEachFilePatch(plain), "")
func (p *PatchManager) RenderAggregatedPatchColored(plain bool) string {
result := ""
for _, patch := range p.renderEachFilePatch(plain) {
if patch != "" {
result += patch + "\n"
}
}
return result
}
func (p *PatchBuilder) GetFileStatus(filename string, parent string) PatchStatus {
func (p *PatchManager) GetFileStatus(filename string, parent string) PatchStatus {
if parent != p.To {
return UNSELECTED
}
@@ -226,7 +231,7 @@ func (p *PatchBuilder) GetFileStatus(filename string, parent string) PatchStatus
return info.mode
}
func (p *PatchBuilder) GetFileIncLineIndices(filename string) ([]int, error) {
func (p *PatchManager) GetFileIncLineIndices(filename string) ([]int, error) {
info, err := p.getFileInfo(filename)
if err != nil {
return nil, err
@@ -234,36 +239,56 @@ func (p *PatchBuilder) GetFileIncLineIndices(filename string) ([]int, error) {
return info.includedLineIndices, nil
}
func (p *PatchBuilder) ApplyPatches(reverse bool) error {
patch := ""
applyFlags := []string{"index", "3way"}
if reverse {
applyFlags = append(applyFlags, "reverse")
}
func (p *PatchManager) ApplyPatches(reverse bool) error {
// for whole patches we'll apply the patch in reverse
// but for part patches we'll apply a reverse patch forwards
for filename, info := range p.fileInfoMap {
if info.mode == UNSELECTED {
continue
}
patch += p.RenderPatchForFile(filename, true, reverse)
applyFlags := []string{"index", "3way"}
reverseOnGenerate := false
if reverse {
if info.mode == WHOLE {
applyFlags = append(applyFlags, "reverse")
} else {
reverseOnGenerate = true
}
}
var err error
// first run we try with the original header, then without
for _, keepOriginalHeader := range []bool{true, false} {
patch := p.RenderPatchForFile(filename, true, reverseOnGenerate, keepOriginalHeader)
if patch == "" {
continue
}
if err = p.applyPatch(patch, applyFlags...); err != nil {
continue
}
break
}
if err != nil {
return err
}
}
return p.applyPatch(patch, applyFlags...)
return nil
}
// clears the patch
func (p *PatchBuilder) Reset() {
func (p *PatchManager) Reset() {
p.To = ""
p.fileInfoMap = map[string]*fileInfo{}
}
func (p *PatchBuilder) Active() bool {
func (p *PatchManager) Active() bool {
return p.To != ""
}
func (p *PatchBuilder) IsEmpty() bool {
func (p *PatchManager) IsEmpty() bool {
for _, fileInfo := range p.fileInfoMap {
if fileInfo.mode == WHOLE || (fileInfo.mode == PART && len(fileInfo.includedLineIndices) > 0) {
return false
@@ -274,16 +299,6 @@ func (p *PatchBuilder) IsEmpty() bool {
}
// if any of these things change we'll need to reset and start a new patch
func (p *PatchBuilder) NewPatchRequired(from string, to string, reverse bool) bool {
func (p *PatchManager) NewPatchRequired(from string, to string, reverse bool) bool {
return from != p.From || to != p.To || reverse != p.reverse
}
func (p *PatchBuilder) AllFilesInPatch() []string {
files := make([]string, 0, len(p.fileInfoMap))
for filename := range p.fileInfoMap {
files = append(files, filename)
}
return files
}
+167
View File
@@ -0,0 +1,167 @@
package patch
import (
"fmt"
"regexp"
"strings"
"github.com/sirupsen/logrus"
)
var (
hunkHeaderRegexp = regexp.MustCompile(`(?m)^@@ -(\d+)[^\+]+\+(\d+)[^@]+@@(.*)$`)
patchHeaderRegexp = regexp.MustCompile(`(?ms)(^diff.*?)^@@`)
)
func GetHeaderFromDiff(diff string) string {
match := patchHeaderRegexp.FindStringSubmatch(diff)
if len(match) <= 1 {
return ""
}
return match[1]
}
func GetHunksFromDiff(diff string) []*PatchHunk {
hunks := []*PatchHunk{}
firstLineIdx := -1
var hunkLines []string //nolint:prealloc
pastDiffHeader := false
for lineIdx, line := range strings.SplitAfter(diff, "\n") {
isHunkHeader := strings.HasPrefix(line, "@@ -")
if isHunkHeader {
if pastDiffHeader { // we need to persist the current hunk
hunks = append(hunks, newHunk(hunkLines, firstLineIdx))
}
pastDiffHeader = true
firstLineIdx = lineIdx
hunkLines = []string{line}
continue
}
if !pastDiffHeader { // skip through the stuff that precedes the first hunk
continue
}
hunkLines = append(hunkLines, line)
}
if pastDiffHeader {
hunks = append(hunks, newHunk(hunkLines, firstLineIdx))
}
return hunks
}
type PatchModifier struct {
Log *logrus.Entry
filename string
hunks []*PatchHunk
header string
}
func NewPatchModifier(log *logrus.Entry, filename string, diffText string) *PatchModifier {
return &PatchModifier{
Log: log,
filename: filename,
hunks: GetHunksFromDiff(diffText),
header: GetHeaderFromDiff(diffText),
}
}
func (d *PatchModifier) ModifiedPatchForLines(lineIndices []int, reverse bool, keepOriginalHeader bool) string {
// step one is getting only those hunks which we care about
hunksInRange := []*PatchHunk{}
outer:
for _, hunk := range d.hunks {
// if there is any line in our lineIndices array that the hunk contains, we append it
for _, lineIdx := range lineIndices {
if lineIdx >= hunk.FirstLineIdx && lineIdx <= hunk.LastLineIdx() {
hunksInRange = append(hunksInRange, hunk)
continue outer
}
}
}
// step 2 is collecting all the hunks with new headers
startOffset := 0
formattedHunks := ""
var formattedHunk string
for _, hunk := range hunksInRange {
startOffset, formattedHunk = hunk.formatWithChanges(lineIndices, reverse, startOffset)
formattedHunks += formattedHunk
}
if formattedHunks == "" {
return ""
}
var fileHeader string
// for staging/unstaging lines we don't want the original header because
// it makes git confused e.g. when dealing with deleted/added files
// but with building and applying patches the original header gives git
// information it needs to cleanly apply patches
if keepOriginalHeader {
fileHeader = d.header
} else {
fileHeader = fmt.Sprintf("--- a/%s\n+++ b/%s\n", d.filename, d.filename)
}
return fileHeader + formattedHunks
}
func (d *PatchModifier) ModifiedPatchForRange(firstLineIdx int, lastLineIdx int, reverse bool, keepOriginalHeader bool) string {
// generate array of consecutive line indices from our range
selectedLines := []int{}
for i := firstLineIdx; i <= lastLineIdx; i++ {
selectedLines = append(selectedLines, i)
}
return d.ModifiedPatchForLines(selectedLines, reverse, keepOriginalHeader)
}
func (d *PatchModifier) OriginalPatchLength() int {
if len(d.hunks) == 0 {
return 0
}
return d.hunks[len(d.hunks)-1].LastLineIdx()
}
func ModifiedPatchForRange(log *logrus.Entry, filename string, diffText string, firstLineIdx int, lastLineIdx int, reverse bool, keepOriginalHeader bool) string {
p := NewPatchModifier(log, filename, diffText)
return p.ModifiedPatchForRange(firstLineIdx, lastLineIdx, reverse, keepOriginalHeader)
}
func ModifiedPatchForLines(log *logrus.Entry, filename string, diffText string, includedLineIndices []int, reverse bool, keepOriginalHeader bool) string {
p := NewPatchModifier(log, filename, diffText)
return p.ModifiedPatchForLines(includedLineIndices, reverse, keepOriginalHeader)
}
// I want to know, given a hunk, what line a given index is on
func (hunk *PatchHunk) LineNumberOfLine(idx int) int {
n := idx - hunk.FirstLineIdx - 1
if n < 0 {
n = 0
} else if n >= len(hunk.bodyLines) {
n = len(hunk.bodyLines) - 1
}
lines := hunk.bodyLines[0:n]
offset := nLinesWithPrefix(lines, []string{"+", " "})
return hunk.newStart + offset
}
func nLinesWithPrefix(lines []string, chars []string) int {
result := 0
for _, line := range lines {
for _, char := range chars {
if line[:1] == char {
result++
}
}
}
return result
}
@@ -1,6 +1,8 @@
package patch
import (
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
@@ -67,20 +69,6 @@ index e48a11c..b2ab81b 100644
...
`
const twoChangesInOneHunk = `diff --git a/filename b/filename
index 9320895..6d79956 100644
--- a/filename
+++ b/filename
@@ -1,5 +1,5 @@
apple
-grape
+kiwi
orange
-pear
+banana
lemon
`
const newFile = `diff --git a/newfile b/newfile
new file mode 100644
index 0000000..4e680cc
@@ -110,7 +98,8 @@ const exampleHunk = `@@ -1,5 +1,5 @@
...
`
func TestTransform(t *testing.T) {
// TestModifyPatchForRange is a function.
func TestModifyPatchForRange(t *testing.T) {
type scenario struct {
testName string
filename string
@@ -127,6 +116,7 @@ func TestTransform(t *testing.T) {
filename: "filename",
firstLineIndex: -1,
lastLineIndex: -1,
reverse: false,
diffText: simpleDiff,
expected: "",
},
@@ -135,6 +125,7 @@ func TestTransform(t *testing.T) {
filename: "filename",
firstLineIndex: 5,
lastLineIndex: 5,
reverse: false,
diffText: simpleDiff,
expected: "",
},
@@ -143,6 +134,7 @@ func TestTransform(t *testing.T) {
filename: "filename",
firstLineIndex: 0,
lastLineIndex: 11,
reverse: false,
diffText: simpleDiff,
expected: `--- a/filename
+++ b/filename
@@ -160,6 +152,7 @@ func TestTransform(t *testing.T) {
filename: "filename",
firstLineIndex: 6,
lastLineIndex: 6,
reverse: false,
diffText: simpleDiff,
expected: `--- a/filename
+++ b/filename
@@ -176,6 +169,7 @@ func TestTransform(t *testing.T) {
filename: "filename",
firstLineIndex: 7,
lastLineIndex: 7,
reverse: false,
diffText: simpleDiff,
expected: `--- a/filename
+++ b/filename
@@ -193,6 +187,7 @@ func TestTransform(t *testing.T) {
filename: "filename",
firstLineIndex: -100,
lastLineIndex: 100,
reverse: false,
diffText: simpleDiff,
expected: `--- a/filename
+++ b/filename
@@ -203,6 +198,59 @@ func TestTransform(t *testing.T) {
...
...
...
`,
},
{
testName: "whole range reversed",
filename: "filename",
firstLineIndex: 0,
lastLineIndex: 11,
reverse: true,
diffText: simpleDiff,
expected: `--- a/filename
+++ b/filename
@@ -1,5 +1,5 @@
apple
+orange
-grape
...
...
...
`,
},
{
testName: "removal reversed",
filename: "filename",
firstLineIndex: 6,
lastLineIndex: 6,
reverse: true,
diffText: simpleDiff,
expected: `--- a/filename
+++ b/filename
@@ -1,5 +1,6 @@
apple
+orange
grape
...
...
...
`,
},
{
testName: "removal reversed",
filename: "filename",
firstLineIndex: 7,
lastLineIndex: 7,
reverse: true,
diffText: simpleDiff,
expected: `--- a/filename
+++ b/filename
@@ -1,5 +1,4 @@
apple
-grape
...
...
...
`,
},
{
@@ -210,6 +258,7 @@ func TestTransform(t *testing.T) {
filename: "filename",
firstLineIndex: -100,
lastLineIndex: 100,
reverse: false,
diffText: addNewlineToEndOfFile,
expected: `--- a/filename
+++ b/filename
@@ -223,21 +272,37 @@ func TestTransform(t *testing.T) {
`,
},
{
testName: "add newline to end of file, reversed",
testName: "add newline to end of file, addition only",
filename: "filename",
firstLineIndex: -100,
lastLineIndex: 100,
firstLineIndex: 8,
lastLineIndex: 8,
reverse: true,
diffText: addNewlineToEndOfFile,
expected: `--- a/filename
+++ b/filename
@@ -60,4 +60,4 @@ grape
@@ -60,4 +60,5 @@ grape
...
...
...
+last line
\ No newline at end of file
last line
`,
},
{
testName: "add newline to end of file, removal only",
filename: "filename",
firstLineIndex: 10,
lastLineIndex: 10,
reverse: true,
diffText: addNewlineToEndOfFile,
expected: `--- a/filename
+++ b/filename
@@ -60,4 +60,3 @@ grape
...
...
...
-last line
\ No newline at end of file
+last line
`,
},
{
@@ -245,24 +310,7 @@ func TestTransform(t *testing.T) {
filename: "filename",
firstLineIndex: -100,
lastLineIndex: 100,
diffText: removeNewlinefromEndOfFile,
expected: `--- a/filename
+++ b/filename
@@ -60,4 +60,4 @@ grape
...
...
...
-last line
+last line
\ No newline at end of file
`,
},
{
testName: "remove newline from end of file, reversed",
filename: "filename",
firstLineIndex: -100,
lastLineIndex: 100,
reverse: true,
reverse: false,
diffText: removeNewlinefromEndOfFile,
expected: `--- a/filename
+++ b/filename
@@ -280,6 +328,7 @@ func TestTransform(t *testing.T) {
filename: "filename",
firstLineIndex: 8,
lastLineIndex: 8,
reverse: false,
diffText: removeNewlinefromEndOfFile,
expected: `--- a/filename
+++ b/filename
@@ -288,24 +337,6 @@ func TestTransform(t *testing.T) {
...
...
-last line
`,
},
{
testName: "remove newline from end of file, removal only, reversed",
filename: "filename",
firstLineIndex: 8,
lastLineIndex: 8,
reverse: true,
diffText: removeNewlinefromEndOfFile,
expected: `--- a/filename
+++ b/filename
@@ -60,5 +60,4 @@ grape
...
...
...
-last line
last line
\ No newline at end of file
`,
},
{
@@ -313,6 +344,7 @@ func TestTransform(t *testing.T) {
filename: "filename",
firstLineIndex: 9,
lastLineIndex: 9,
reverse: false,
diffText: removeNewlinefromEndOfFile,
expected: `--- a/filename
+++ b/filename
@@ -323,23 +355,6 @@ func TestTransform(t *testing.T) {
last line
+last line
\ No newline at end of file
`,
},
{
testName: "remove newline from end of file, addition only, reversed",
filename: "filename",
firstLineIndex: 9,
lastLineIndex: 9,
reverse: true,
diffText: removeNewlinefromEndOfFile,
expected: `--- a/filename
+++ b/filename
@@ -60,3 +60,4 @@ grape
...
...
...
+last line
\ No newline at end of file
`,
},
{
@@ -347,6 +362,7 @@ func TestTransform(t *testing.T) {
filename: "filename",
firstLineIndex: -100,
lastLineIndex: 100,
reverse: false,
diffText: twoHunks,
expected: `--- a/filename
+++ b/filename
@@ -373,6 +389,7 @@ func TestTransform(t *testing.T) {
filename: "filename",
firstLineIndex: 7,
lastLineIndex: 15,
reverse: false,
diffText: twoHunks,
expected: `--- a/filename
+++ b/filename
@@ -391,6 +408,32 @@ func TestTransform(t *testing.T) {
...
...
...
`,
},
{
testName: "staging part of both hunks, reversed",
filename: "filename",
firstLineIndex: 7,
lastLineIndex: 15,
reverse: true,
diffText: twoHunks,
expected: `--- a/filename
+++ b/filename
@@ -1,5 +1,4 @@
apple
-orange
...
...
...
@@ -8,8 +7,7 @@ grape
...
...
...
-pear
lemon
...
...
...
`,
},
{
@@ -398,6 +441,7 @@ func TestTransform(t *testing.T) {
filename: "newfile",
firstLineIndex: -100,
lastLineIndex: 100,
reverse: false,
diffText: newFile,
expected: `--- a/newfile
+++ b/newfile
@@ -412,12 +456,28 @@ func TestTransform(t *testing.T) {
filename: "newfile",
firstLineIndex: 6,
lastLineIndex: 7,
reverse: false,
diffText: newFile,
expected: `--- a/newfile
+++ b/newfile
@@ -0,0 +1,2 @@
+apple
+orange
`,
},
{
testName: "adding a new file, reversed",
filename: "newfile",
firstLineIndex: -100,
lastLineIndex: 100,
reverse: true,
diffText: newFile,
expected: `--- a/newfile
+++ b/newfile
@@ -1,3 +0,0 @@
-apple
-orange
-grape
`,
},
{
@@ -425,10 +485,11 @@ func TestTransform(t *testing.T) {
filename: "newfile",
firstLineIndex: -100,
lastLineIndex: 100,
reverse: false,
diffText: addNewlineToPreviouslyEmptyFile,
expected: `--- a/newfile
+++ b/newfile
@@ -0,0 +1 @@
@@ -0,0 +1,1 @@
+new line
\ No newline at end of file
`,
@@ -438,49 +499,13 @@ func TestTransform(t *testing.T) {
filename: "newfile",
firstLineIndex: -100,
lastLineIndex: 100,
diffText: addNewlineToPreviouslyEmptyFile,
reverse: true,
diffText: addNewlineToPreviouslyEmptyFile,
expected: `--- a/newfile
+++ b/newfile
@@ -0,0 +1 @@
+new line
@@ -1,1 +0,0 @@
-new line
\ No newline at end of file
`,
},
{
testName: "adding part of a hunk",
filename: "filename",
firstLineIndex: 6,
lastLineIndex: 7,
reverse: false,
diffText: twoChangesInOneHunk,
expected: `--- a/filename
+++ b/filename
@@ -1,5 +1,5 @@
apple
-grape
+kiwi
orange
pear
lemon
`,
},
{
testName: "adding part of a hunk, reverse",
filename: "filename",
firstLineIndex: 6,
lastLineIndex: 7,
reverse: true,
diffText: twoChangesInOneHunk,
expected: `--- a/filename
+++ b/filename
@@ -1,5 +1,5 @@
apple
-grape
+kiwi
orange
banana
lemon
`,
},
}
@@ -488,128 +513,37 @@ 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)
result := Parse(s.diffText).
Transform(TransformOpts{
Reverse: s.reverse,
FileNameOverride: s.filename,
IncludedLineIndices: lineIndices,
}).
FormatPlain()
assert.Equal(t, s.expected, result)
})
}
}
func TestParseAndFormatPlain(t *testing.T) {
scenarios := []struct {
testName string
patchStr string
}{
{
testName: "simpleDiff",
patchStr: simpleDiff,
},
{
testName: "addNewlineToEndOfFile",
patchStr: addNewlineToEndOfFile,
},
{
testName: "removeNewlinefromEndOfFile",
patchStr: removeNewlinefromEndOfFile,
},
{
testName: "twoHunks",
patchStr: twoHunks,
},
{
testName: "twoChangesInOneHunk",
patchStr: twoChangesInOneHunk,
},
{
testName: "newFile",
patchStr: newFile,
},
{
testName: "addNewlineToPreviouslyEmptyFile",
patchStr: addNewlineToPreviouslyEmptyFile,
},
{
testName: "exampleHunk",
patchStr: exampleHunk,
},
}
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
// the new length in a hunk header if the value is 1, and currently we always
// omit the new length in such cases.
patch := Parse(s.patchStr)
result := formatPlain(patch)
assert.Equal(t, s.patchStr, result)
result := ModifiedPatchForRange(nil, s.filename, s.diffText, s.firstLineIndex, s.lastLineIndex, s.reverse, false)
if !assert.Equal(t, s.expected, result) {
fmt.Println(result)
}
})
}
}
func TestLineNumberOfLine(t *testing.T) {
type scenario struct {
testName string
patchStr string
indexes []int
expecteds []int
testName string
hunk *PatchHunk
idx int
expected int
}
scenarios := []scenario{
{
testName: "twoHunks",
patchStr: twoHunks,
// this is really more of a characteristic test than anything.
indexes: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 1000},
expecteds: []int{1, 1, 1, 1, 1, 1, 2, 2, 3, 4, 5, 8, 8, 9, 10, 11, 12, 13, 14, 15, 15, 15, 15, 15, 15},
testName: "nothing selected",
hunk: newHunk(strings.SplitAfter(exampleHunk, "\n"), 10),
idx: 15,
expected: 3,
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
for i, idx := range s.indexes {
patch := Parse(s.patchStr)
result := patch.LineNumberOfLine(idx)
assert.Equal(t, s.expecteds[i], result)
}
})
}
}
func TestGetNextStageableLineIndex(t *testing.T) {
type scenario struct {
testName string
patchStr string
indexes []int
expecteds []int
}
scenarios := []scenario{
{
testName: "twoHunks",
patchStr: twoHunks,
indexes: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 1000},
expecteds: []int{6, 6, 6, 6, 6, 6, 6, 7, 15, 15, 15, 15, 15, 15, 15, 15, 16, 16, 16, 16, 16, 16, 16, 16, 16},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
for i, idx := range s.indexes {
patch := Parse(s.patchStr)
result := patch.GetNextChangeIdx(idx)
assert.Equal(t, s.expecteds[i], result)
result := s.hunk.LineNumberOfLine(s.idx)
if !assert.Equal(t, s.expected, result) {
fmt.Println(result)
}
})
}
+228
View File
@@ -0,0 +1,228 @@
package patch
import (
"regexp"
"strings"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/samber/lo"
"github.com/sirupsen/logrus"
)
type PatchLineKind int
const (
PATCH_HEADER PatchLineKind = iota
COMMIT_SHA
COMMIT_DESCRIPTION
HUNK_HEADER
ADDITION
DELETION
CONTEXT
NEWLINE_MESSAGE
)
// the job of this file is to parse a diff, find out where the hunks begin and end, which lines are stageable, and how to find the next hunk from the current position or the next stageable line from the current position.
type PatchLine struct {
Kind PatchLineKind
Content string // something like '+ hello' (note the first character is not removed)
}
type PatchParser struct {
Log *logrus.Entry
PatchLines []*PatchLine
PatchHunks []*PatchHunk
HunkStarts []int
StageableLines []int // rename to mention we're talking about indexes
}
// NewPatchParser builds a new branch list builder
func NewPatchParser(log *logrus.Entry, patch string) *PatchParser {
hunkStarts, stageableLines, patchLines := parsePatch(patch)
patchHunks := GetHunksFromDiff(patch)
return &PatchParser{
Log: log,
HunkStarts: hunkStarts, // deprecated
StageableLines: stageableLines,
PatchLines: patchLines,
PatchHunks: patchHunks,
}
}
// GetHunkContainingLine takes a line index and an offset and finds the hunk
// which contains the line index, then returns the hunk considering the offset.
// e.g. if the offset is 1 it will return the next hunk.
func (p *PatchParser) GetHunkContainingLine(lineIndex int, offset int) *PatchHunk {
if len(p.PatchHunks) == 0 {
return nil
}
for index, hunk := range p.PatchHunks {
if lineIndex >= hunk.FirstLineIdx && lineIndex <= hunk.LastLineIdx() {
resultIndex := index + offset
if resultIndex < 0 {
resultIndex = 0
} else if resultIndex > len(p.PatchHunks)-1 {
resultIndex = len(p.PatchHunks) - 1
}
return p.PatchHunks[resultIndex]
}
}
// if your cursor is past the last hunk, select the last hunk
if lineIndex > p.PatchHunks[len(p.PatchHunks)-1].LastLineIdx() {
return p.PatchHunks[len(p.PatchHunks)-1]
}
// otherwise select the first
return p.PatchHunks[0]
}
// selected means you've got it highlighted with your cursor
// included means the line has been included in the patch (only applicable when
// building a patch)
func (l *PatchLine) render(selected bool, included bool) string {
content := l.Content
if len(content) == 0 {
content = " " // using the space so that we can still highlight if necessary
}
// for hunk headers we need to start off cyan and then use white for the message
if l.Kind == HUNK_HEADER {
re := regexp.MustCompile("(@@.*?@@)(.*)")
match := re.FindStringSubmatch(content)
return coloredString(style.FgCyan, match[1], selected, included) + coloredString(theme.DefaultTextColor, match[2], selected, false)
}
var textStyle style.TextStyle
switch l.Kind {
case PATCH_HEADER:
textStyle = textStyle.SetBold()
case ADDITION:
textStyle = style.FgGreen
case DELETION:
textStyle = style.FgRed
case COMMIT_SHA:
textStyle = style.FgYellow
default:
textStyle = theme.DefaultTextColor
}
return coloredString(textStyle, content, selected, included)
}
func coloredString(textStyle style.TextStyle, str string, selected bool, included bool) string {
if selected {
textStyle = textStyle.MergeStyle(theme.SelectedRangeBgColor)
}
firstCharStyle := textStyle
if included {
firstCharStyle = firstCharStyle.MergeStyle(style.BgGreen)
}
if len(str) < 2 {
return firstCharStyle.Sprint(str)
}
return firstCharStyle.Sprint(str[:1]) + textStyle.Sprint(str[1:])
}
func parsePatch(patch string) ([]int, []int, []*PatchLine) {
lines := strings.Split(patch, "\n")
hunkStarts := []int{}
stageableLines := []int{}
pastFirstHunkHeader := false
pastCommitDescription := true
patchLines := make([]*PatchLine, len(lines))
var lineKind PatchLineKind
var firstChar string
for index, line := range lines {
firstChar = " "
if len(line) > 0 {
firstChar = line[:1]
}
if index == 0 && strings.HasPrefix(line, "commit") {
lineKind = COMMIT_SHA
pastCommitDescription = false
} else if !pastCommitDescription {
if strings.HasPrefix(line, "diff") || strings.HasPrefix(line, "---") {
pastCommitDescription = true
lineKind = PATCH_HEADER
} else {
lineKind = COMMIT_DESCRIPTION
}
} else if firstChar == "@" {
pastFirstHunkHeader = true
hunkStarts = append(hunkStarts, index)
lineKind = HUNK_HEADER
} else if pastFirstHunkHeader {
switch firstChar {
case "-":
lineKind = DELETION
stageableLines = append(stageableLines, index)
case "+":
lineKind = ADDITION
stageableLines = append(stageableLines, index)
case "\\":
lineKind = NEWLINE_MESSAGE
case " ":
lineKind = CONTEXT
}
} else {
lineKind = PATCH_HEADER
}
patchLines[index] = &PatchLine{Kind: lineKind, Content: line}
}
return hunkStarts, stageableLines, patchLines
}
// Render returns the coloured string of the diff with any selected lines highlighted
func (p *PatchParser) Render(isFocused bool, firstLineIndex int, lastLineIndex int, incLineIndices []int) string {
contentToDisplay := slices.Some(p.PatchLines, func(line *PatchLine) bool {
return line.Content != ""
})
if !contentToDisplay {
return ""
}
renderedLines := slices.MapWithIndex(p.PatchLines, func(patchLine *PatchLine, index int) string {
selected := isFocused && index >= firstLineIndex && index <= lastLineIndex
included := lo.Contains(incLineIndices, index)
return patchLine.render(selected, included)
})
result := strings.Join(renderedLines, "\n")
return result
}
// RenderLinesPlain returns the non-coloured string of diff part from firstLineIndex to
// lastLineIndex
func (p *PatchParser) RenderLinesPlain(firstLineIndex, lastLineIndex int) string {
return renderLinesPlain(p.PatchLines[firstLineIndex : lastLineIndex+1])
}
func renderLinesPlain(lines []*PatchLine) string {
renderedLines := slices.Map(lines, func(line *PatchLine) string {
return line.Content + "\n"
})
return strings.Join(renderedLines, "")
}
// GetNextStageableLineIndex takes a line index and returns the line index of the next stageable line
// note this will actually include the current index if it is stageable
func (p *PatchParser) GetNextStageableLineIndex(currentIndex int) int {
for _, lineIndex := range p.StageableLines {
if lineIndex >= currentIndex {
return lineIndex
}
}
return p.StageableLines[len(p.StageableLines)-1]
}
-156
View File
@@ -1,156 +0,0 @@
package patch
import "github.com/samber/lo"
type patchTransformer struct {
patch *Patch
opts TransformOpts
}
type TransformOpts struct {
// Create a patch that will applied in reverse with `git apply --reverse`.
// This affects how unselected lines are treated when only parts of a hunk
// are selected: usually, for unselected lines we change '-' lines to
// context lines and remove '+' lines, but when Reverse is true we need to
// turn '+' lines into context lines and remove '-' lines.
Reverse bool
// If set, we will replace the original header with one referring to this file name.
// For staging/unstaging lines we don't want the original header because
// it makes git confused e.g. when dealing with deleted/added files
// but with building and applying patches the original header gives git
// information it needs to cleanly apply patches
FileNameOverride string
// The indices of lines that should be included in the patch.
IncludedLineIndices []int
}
func transform(patch *Patch, opts TransformOpts) *Patch {
transformer := &patchTransformer{
patch: patch,
opts: opts,
}
return transformer.transform()
}
// helper function that takes a start and end index and returns a slice of all
// indexes inbetween (inclusive)
func ExpandRange(start int, end int) []int {
expanded := []int{}
for i := start; i <= end; i++ {
expanded = append(expanded, i)
}
return expanded
}
func (self *patchTransformer) transform() *Patch {
header := self.transformHeader()
hunks := self.transformHunks()
return &Patch{
header: header,
hunks: hunks,
}
}
func (self *patchTransformer) transformHeader() []string {
if self.opts.FileNameOverride != "" {
return []string{
"--- a/" + self.opts.FileNameOverride,
"+++ b/" + self.opts.FileNameOverride,
}
} else {
return self.patch.header
}
}
func (self *patchTransformer) transformHunks() []*Hunk {
newHunks := make([]*Hunk, 0, len(self.patch.hunks))
startOffset := 0
var formattedHunk *Hunk
for i, hunk := range self.patch.hunks {
startOffset, formattedHunk = self.transformHunk(
hunk,
startOffset,
self.patch.HunkStartIdx(i),
)
if formattedHunk.containsChanges() {
newHunks = append(newHunks, formattedHunk)
}
}
return newHunks
}
func (self *patchTransformer) transformHunk(hunk *Hunk, startOffset int, firstLineIdx int) (int, *Hunk) {
newLines := self.transformHunkLines(hunk, firstLineIdx)
newNewStart, newStartOffset := self.transformHunkHeader(newLines, hunk.oldStart, startOffset)
newHunk := &Hunk{
bodyLines: newLines,
oldStart: hunk.oldStart,
newStart: newNewStart,
headerContext: hunk.headerContext,
}
return newStartOffset, newHunk
}
func (self *patchTransformer) transformHunkLines(hunk *Hunk, firstLineIdx int) []*PatchLine {
skippedNewlineMessageIndex := -1
newLines := []*PatchLine{}
for i, line := range hunk.bodyLines {
lineIdx := i + firstLineIdx + 1 // plus one for header line
if line.Content == "" {
break
}
isLineSelected := lo.Contains(self.opts.IncludedLineIndices, lineIdx)
if isLineSelected || (line.Kind == NEWLINE_MESSAGE && skippedNewlineMessageIndex != lineIdx) || line.Kind == CONTEXT {
newLines = append(newLines, line)
continue
}
if (line.Kind == DELETION && !self.opts.Reverse) || (line.Kind == ADDITION && self.opts.Reverse) {
content := " " + line.Content[1:]
newLines = append(newLines, &PatchLine{
Kind: CONTEXT,
Content: content,
})
continue
}
if line.Kind == ADDITION {
// we don't want to include the 'newline at end of file' line if it involves an addition we're not including
skippedNewlineMessageIndex = lineIdx + 1
}
}
return newLines
}
func (self *patchTransformer) transformHunkHeader(newBodyLines []*PatchLine, oldStart int, startOffset int) (int, int) {
oldLength := nLinesWithKind(newBodyLines, []PatchLineKind{CONTEXT, DELETION})
newLength := nLinesWithKind(newBodyLines, []PatchLineKind{CONTEXT, ADDITION})
var newStartOffset int
// if the hunk went from zero to positive length, we need to increment the starting point by one
// if the hunk went from positive to zero length, we need to decrement the starting point by one
if oldLength == 0 {
newStartOffset = 1
} else if newLength == 0 {
newStartOffset = -1
} else {
newStartOffset = 0
}
newStart := oldStart + startOffset + newStartOffset
newStartOffset = startOffset + newLength - oldLength
return newStart, newStartOffset
}
+4 -2
View File
@@ -6,7 +6,9 @@ package config
// GetPlatformDefaultConfig gets the defaults for the platform
func GetPlatformDefaultConfig() OSConfig {
return OSConfig{
Open: "open -- {{filename}}",
OpenLink: "open {{link}}",
EditCommand: ``,
EditCommandTemplate: "",
OpenCommand: "open -- {{filename}}",
OpenLinkCommand: "open {{link}}",
}
}
+9 -20
View File
@@ -2,7 +2,6 @@ package config
import (
"io/ioutil"
"os"
"strings"
)
@@ -11,31 +10,21 @@ func isWSL() bool {
return err == nil && strings.Contains(string(data), "microsoft")
}
func isContainer() bool {
data, err := ioutil.ReadFile("/proc/1/cgroup")
if strings.Contains(string(data), "docker") ||
strings.Contains(string(data), "/lxc/") ||
[]string{string(data)}[0] != "systemd" &&
[]string{string(data)}[0] != "init" ||
os.Getenv("container") != "" {
return err == nil && true
}
return err == nil && false
}
// GetPlatformDefaultConfig gets the defaults for the platform
func GetPlatformDefaultConfig() OSConfig {
if isWSL() && !isContainer() {
if isWSL() {
return OSConfig{
Open: `powershell.exe start explorer.exe {{filename}} >/dev/null`,
OpenLink: `powershell.exe start {{link}} >/dev/null`,
EditCommand: ``,
EditCommandTemplate: "",
OpenCommand: `powershell.exe start explorer.exe {{filename}} >/dev/null`,
OpenLinkCommand: `powershell.exe start {{link}} >/dev/null`,
}
}
return OSConfig{
Open: `xdg-open {{filename}} >/dev/null`,
OpenLink: `xdg-open {{link}} >/dev/null`,
EditCommand: ``,
EditCommandTemplate: "",
OpenCommand: `xdg-open {{filename}} >/dev/null`,
OpenLinkCommand: `xdg-open {{link}} >/dev/null`,
}
}
+4 -2
View File
@@ -3,7 +3,9 @@ package config
// GetPlatformDefaultConfig gets the defaults for the platform
func GetPlatformDefaultConfig() OSConfig {
return OSConfig{
Open: `start "" {{filename}}`,
OpenLink: `start "" {{link}}`,
EditCommand: ``,
EditCommandTemplate: "",
OpenCommand: `start "" {{filename}}`,
OpenLinkCommand: `start "" {{link}}`,
}
}
-111
View File
@@ -1,111 +0,0 @@
package config
func GetEditTemplate(osConfig *OSConfig, guessDefaultEditor func() string) (string, bool) {
preset := getPreset(osConfig, guessDefaultEditor)
template := osConfig.Edit
if template == "" {
template = preset.editTemplate
}
return template, getEditInTerminal(osConfig, preset)
}
func GetEditAtLineTemplate(osConfig *OSConfig, guessDefaultEditor func() string) (string, bool) {
preset := getPreset(osConfig, guessDefaultEditor)
template := osConfig.EditAtLine
if template == "" {
template = preset.editAtLineTemplate
}
return template, getEditInTerminal(osConfig, preset)
}
func GetEditAtLineAndWaitTemplate(osConfig *OSConfig, guessDefaultEditor func() string) string {
preset := getPreset(osConfig, guessDefaultEditor)
template := osConfig.EditAtLineAndWait
if template == "" {
template = preset.editAtLineAndWaitTemplate
}
return template
}
type editPreset struct {
editTemplate string
editAtLineTemplate string
editAtLineAndWaitTemplate string
editInTerminal bool
}
func getPreset(osConfig *OSConfig, guessDefaultEditor func() string) *editPreset {
presets := map[string]*editPreset{
"vi": standardTerminalEditorPreset("vi"),
"vim": standardTerminalEditorPreset("vim"),
"nvim": standardTerminalEditorPreset("nvim"),
"emacs": standardTerminalEditorPreset("emacs"),
"nano": standardTerminalEditorPreset("nano"),
"kakoune": standardTerminalEditorPreset("kakoune"),
"vscode": {
editTemplate: "code --reuse-window -- {{filename}}",
editAtLineTemplate: "code --reuse-window --goto -- {{filename}}:{{line}}",
editAtLineAndWaitTemplate: "code --reuse-window --goto --wait -- {{filename}}:{{line}}",
editInTerminal: false,
},
"sublime": {
editTemplate: "subl -- {{filename}}",
editAtLineTemplate: "subl -- {{filename}}:{{line}}",
editAtLineAndWaitTemplate: "subl --wait -- {{filename}}:{{line}}",
editInTerminal: false,
},
"bbedit": {
editTemplate: "bbedit -- {{filename}}",
editAtLineTemplate: "bbedit +{{line}} -- {{filename}}",
editAtLineAndWaitTemplate: "bbedit +{{line}} --wait -- {{filename}}",
editInTerminal: false,
},
"xcode": {
editTemplate: "xed -- {{filename}}",
editAtLineTemplate: "xed --line {{line}} -- {{filename}}",
editAtLineAndWaitTemplate: "xed --line {{line}} --wait -- {{filename}}",
editInTerminal: false,
},
}
// Some of our presets have a different name than the editor they are using.
editorToPreset := map[string]string{
"kak": "kakoune",
"code": "vscode",
"subl": "sublime",
"xed": "xcode",
}
presetName := osConfig.EditPreset
if presetName == "" {
defaultEditor := guessDefaultEditor()
if presets[defaultEditor] != nil {
presetName = defaultEditor
} else if p := editorToPreset[defaultEditor]; p != "" {
presetName = p
}
}
if presetName == "" || presets[presetName] == nil {
presetName = "vim"
}
return presets[presetName]
}
func standardTerminalEditorPreset(editor string) *editPreset {
return &editPreset{
editTemplate: editor + " -- {{filename}}",
editAtLineTemplate: editor + " +{{line}} -- {{filename}}",
editAtLineAndWaitTemplate: editor + " +{{line}} -- {{filename}}",
editInTerminal: true,
}
}
func getEditInTerminal(osConfig *OSConfig, preset *editPreset) bool {
if osConfig.EditInTerminal != nil {
return *osConfig.EditInTerminal
}
return preset.editInTerminal
}
-126
View File
@@ -1,126 +0,0 @@
package config
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetEditTemplate(t *testing.T) {
trueVal := true
scenarios := []struct {
name string
osConfig *OSConfig
guessDefaultEditor func() string
expectedEditTemplate string
expectedEditAtLineTemplate string
expectedEditAtLineAndWaitTemplate string
expectedEditInTerminal bool
}{
{
"Default template is vim",
&OSConfig{},
func() string { return "" },
"vim -- {{filename}}",
"vim +{{line}} -- {{filename}}",
"vim +{{line}} -- {{filename}}",
true,
},
{
"Setting a preset",
&OSConfig{
EditPreset: "vscode",
},
func() string { return "" },
"code --reuse-window -- {{filename}}",
"code --reuse-window --goto -- {{filename}}:{{line}}",
"code --reuse-window --goto --wait -- {{filename}}:{{line}}",
false,
},
{
"Setting a preset wins over guessed editor",
&OSConfig{
EditPreset: "vscode",
},
func() string { return "nano" },
"code --reuse-window -- {{filename}}",
"code --reuse-window --goto -- {{filename}}:{{line}}",
"code --reuse-window --goto --wait -- {{filename}}:{{line}}",
false,
},
{
"Overriding a preset with explicit config (edit)",
&OSConfig{
EditPreset: "vscode",
Edit: "myeditor {{filename}}",
EditInTerminal: &trueVal,
},
func() string { return "" },
"myeditor {{filename}}",
"code --reuse-window --goto -- {{filename}}:{{line}}",
"code --reuse-window --goto --wait -- {{filename}}:{{line}}",
true,
},
{
"Overriding a preset with explicit config (edit at line)",
&OSConfig{
EditPreset: "vscode",
EditAtLine: "myeditor --line={{line}} {{filename}}",
EditInTerminal: &trueVal,
},
func() string { return "" },
"code --reuse-window -- {{filename}}",
"myeditor --line={{line}} {{filename}}",
"code --reuse-window --goto --wait -- {{filename}}:{{line}}",
true,
},
{
"Overriding a preset with explicit config (edit at line and wait)",
&OSConfig{
EditPreset: "vscode",
EditAtLineAndWait: "myeditor --line={{line}} -w {{filename}}",
EditInTerminal: &trueVal,
},
func() string { return "" },
"code --reuse-window -- {{filename}}",
"code --reuse-window --goto -- {{filename}}:{{line}}",
"myeditor --line={{line}} -w {{filename}}",
true,
},
{
"Unknown preset name",
&OSConfig{
EditPreset: "thisPresetDoesNotExist",
},
func() string { return "" },
"vim -- {{filename}}",
"vim +{{line}} -- {{filename}}",
"vim +{{line}} -- {{filename}}",
true,
},
{
"Guessing a preset from guessed editor",
&OSConfig{},
func() string { return "emacs" },
"emacs -- {{filename}}",
"emacs +{{line}} -- {{filename}}",
"emacs +{{line}} -- {{filename}}",
true,
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
template, editInTerminal := GetEditTemplate(s.osConfig, s.guessDefaultEditor)
assert.Equal(t, s.expectedEditTemplate, template)
assert.Equal(t, s.expectedEditInTerminal, editInTerminal)
template, editInTerminal = GetEditAtLineTemplate(s.osConfig, s.guessDefaultEditor)
assert.Equal(t, s.expectedEditAtLineTemplate, template)
assert.Equal(t, s.expectedEditInTerminal, editInTerminal)
template = GetEditAtLineAndWaitTemplate(s.osConfig, s.guessDefaultEditor)
assert.Equal(t, s.expectedEditAtLineAndWaitTemplate, template)
})
}
}
+52 -92
View File
@@ -27,33 +27,31 @@ type RefresherConfig struct {
}
type GuiConfig struct {
AuthorColors map[string]string `yaml:"authorColors"`
BranchColors map[string]string `yaml:"branchColors"`
ScrollHeight int `yaml:"scrollHeight"`
ScrollPastBottom bool `yaml:"scrollPastBottom"`
MouseEvents bool `yaml:"mouseEvents"`
SkipUnstageLineWarning bool `yaml:"skipUnstageLineWarning"`
SkipStashWarning bool `yaml:"skipStashWarning"`
SidePanelWidth float64 `yaml:"sidePanelWidth"`
ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
MainPanelSplitMode string `yaml:"mainPanelSplitMode"`
Language string `yaml:"language"`
TimeFormat string `yaml:"timeFormat"`
Theme ThemeConfig `yaml:"theme"`
CommitLength CommitLengthConfig `yaml:"commitLength"`
SkipNoStagedFilesWarning bool `yaml:"skipNoStagedFilesWarning"`
ShowListFooter bool `yaml:"showListFooter"`
ShowFileTree bool `yaml:"showFileTree"`
ShowRandomTip bool `yaml:"showRandomTip"`
ShowCommandLog bool `yaml:"showCommandLog"`
ShowBottomLine bool `yaml:"showBottomLine"`
ShowIcons bool `yaml:"showIcons"`
ExperimentalShowBranchHeads bool `yaml:"experimentalShowBranchHeads"`
CommandLogSize int `yaml:"commandLogSize"`
SplitDiff string `yaml:"splitDiff"`
SkipRewordInEditorWarning bool `yaml:"skipRewordInEditorWarning"`
WindowSize string `yaml:"windowSize"`
Border string `yaml:"border"`
AuthorColors map[string]string `yaml:"authorColors"`
BranchColors map[string]string `yaml:"branchColors"`
ScrollHeight int `yaml:"scrollHeight"`
ScrollPastBottom bool `yaml:"scrollPastBottom"`
MouseEvents bool `yaml:"mouseEvents"`
SkipUnstageLineWarning bool `yaml:"skipUnstageLineWarning"`
SkipStashWarning bool `yaml:"skipStashWarning"`
SidePanelWidth float64 `yaml:"sidePanelWidth"`
ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
MainPanelSplitMode string `yaml:"mainPanelSplitMode"`
Language string `yaml:"language"`
TimeFormat string `yaml:"timeFormat"`
Theme ThemeConfig `yaml:"theme"`
CommitLength CommitLengthConfig `yaml:"commitLength"`
SkipNoStagedFilesWarning bool `yaml:"skipNoStagedFilesWarning"`
ShowListFooter bool `yaml:"showListFooter"`
ShowFileTree bool `yaml:"showFileTree"`
ShowRandomTip bool `yaml:"showRandomTip"`
ShowCommandLog bool `yaml:"showCommandLog"`
ShowBottomLine bool `yaml:"showBottomLine"`
ShowIcons bool `yaml:"showIcons"`
CommandLogSize int `yaml:"commandLogSize"`
SplitDiff string `yaml:"splitDiff"`
SkipRewordInEditorWarning bool `yaml:"skipRewordInEditorWarning"`
WindowSize string `yaml:"windowSize"`
}
type ThemeConfig struct {
@@ -139,6 +137,7 @@ type KeybindingUniversalConfig struct {
Quit string `yaml:"quit"`
QuitAlt1 string `yaml:"quit-alt1"`
Return string `yaml:"return"`
ReturnAlt1 string `yaml:"return-alt1"`
QuitWithoutChangingDirectory string `yaml:"quitWithoutChangingDirectory"`
TogglePanel string `yaml:"togglePanel"`
PrevItem string `yaml:"prevItem"`
@@ -166,7 +165,7 @@ type KeybindingUniversalConfig struct {
Select string `yaml:"select"`
GoInto string `yaml:"goInto"`
Confirm string `yaml:"confirm"`
ConfirmInEditor string `yaml:"confirmInEditor"`
ConfirmAlt1 string `yaml:"confirm-alt1"`
Remove string `yaml:"remove"`
New string `yaml:"new"`
Edit string `yaml:"edit"`
@@ -179,8 +178,8 @@ type KeybindingUniversalConfig struct {
ScrollDownMainAlt2 string `yaml:"scrollDownMain-alt2"`
ExecuteCustomCommand string `yaml:"executeCustomCommand"`
CreateRebaseOptionsMenu string `yaml:"createRebaseOptionsMenu"`
Push string `yaml:"pushFiles"` // 'Files' appended for legacy reasons
Pull string `yaml:"pullFiles"` // 'Files' appended for legacy reasons
PushFiles string `yaml:"pushFiles"`
PullFiles string `yaml:"pullFiles"`
Refresh string `yaml:"refresh"`
CreatePatchOptionsMenu string `yaml:"createPatchOptionsMenu"`
NextTab string `yaml:"nextTab"`
@@ -195,6 +194,7 @@ type KeybindingUniversalConfig struct {
CopyToClipboard string `yaml:"copyToClipboard"`
OpenRecentRepos string `yaml:"openRecentRepos"`
SubmitEditorText string `yaml:"submitEditorText"`
AppendNewline string `yaml:"appendNewline"`
ExtrasMenu string `yaml:"extrasMenu"`
ToggleWhitespaceInDiffView string `yaml:"toggleWhitespaceInDiffView"`
IncreaseContextInDiffView string `yaml:"increaseContextInDiffView"`
@@ -235,7 +235,6 @@ type KeybindingBranchesConfig struct {
MergeIntoCurrentBranch string `yaml:"mergeIntoCurrentBranch"`
ViewGitFlowOptions string `yaml:"viewGitFlowOptions"`
FastForward string `yaml:"fastForward"`
CreateTag string `yaml:"createTag"`
PushTag string `yaml:"pushTag"`
SetUpstream string `yaml:"setUpstream"`
FetchRemote string `yaml:"fetchRemote"`
@@ -258,7 +257,7 @@ type KeybindingCommitsConfig struct {
CherryPickCopy string `yaml:"cherryPickCopy"`
CherryPickCopyRange string `yaml:"cherryPickCopyRange"`
PasteCommits string `yaml:"pasteCommits"`
CreateTag string `yaml:"tagCommit"`
TagCommit string `yaml:"tagCommit"`
CheckoutCommit string `yaml:"checkoutCommit"`
ResetCherryPick string `yaml:"resetCherryPick"`
CopyCommitAttributeToClipboard string `yaml:"copyCommitAttributeToClipboard"`
@@ -292,54 +291,16 @@ type KeybindingSubmodulesConfig struct {
// OSConfig contains config on the level of the os
type OSConfig struct {
// Command for editing a file. Should contain "{{filename}}".
Edit string `yaml:"edit,omitempty"`
// Command for editing a file at a given line number. Should contain
// "{{filename}}", and may optionally contain "{{line}}".
EditAtLine string `yaml:"editAtLine,omitempty"`
// Same as EditAtLine, except that the command needs to wait until the
// window is closed.
EditAtLineAndWait string `yaml:"editAtLineAndWait,omitempty"`
// Whether the given edit commands use the terminal. Used to decide whether
// lazygit needs to suspend to the background before calling the editor.
// Pointer to bool so that we can distinguish unset (nil) from false.
EditInTerminal *bool `yaml:"editInTerminal,omitempty"`
// A built-in preset that sets all of the above settings. Supported presets
// are defined in the getPreset function in editor_presets.go.
EditPreset string `yaml:"editPreset,omitempty"`
// Command for opening a file, as if the file is double-clicked. Should
// contain "{{filename}}", but doesn't support "{{line}}".
Open string `yaml:"open,omitempty"`
// Command for opening a link. Should contain "{{link}}".
OpenLink string `yaml:"openLink,omitempty"`
// --------
// The following configs are all deprecated and kept for backward
// compatibility. They will be removed in the future.
// 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 is the command for editing a file
EditCommand string `yaml:"editCommand,omitempty"`
// EditCommandTemplate is the command template for editing a file
// Deprecated: use EditAtLine instead.
EditCommandTemplate string `yaml:"editCommandTemplate,omitempty"`
// OpenCommand is the command for opening a file
// Deprecated: use Open instead.
OpenCommand string `yaml:"openCommand,omitempty"`
// OpenLinkCommand is the command for opening a link
// Deprecated: use OpenLink instead.
// OpenCommand is the command for opening a link
OpenLinkCommand string `yaml:"openLinkCommand,omitempty"`
}
@@ -409,19 +370,17 @@ func GetDefaultConfig() *UserConfig {
UnstagedChangesColor: []string{"red"},
DefaultFgColor: []string{"default"},
},
CommitLength: CommitLengthConfig{Show: true},
SkipNoStagedFilesWarning: false,
ShowListFooter: true,
ShowCommandLog: true,
ShowBottomLine: true,
ShowFileTree: true,
ShowRandomTip: true,
ShowIcons: false,
ExperimentalShowBranchHeads: false,
CommandLogSize: 8,
SplitDiff: "auto",
SkipRewordInEditorWarning: false,
Border: "single",
CommitLength: CommitLengthConfig{Show: true},
SkipNoStagedFilesWarning: false,
ShowListFooter: true,
ShowCommandLog: true,
ShowBottomLine: true,
ShowFileTree: true,
ShowRandomTip: true,
ShowIcons: false,
CommandLogSize: 8,
SplitDiff: "auto",
SkipRewordInEditorWarning: false,
},
Git: GitConfig{
Paging: PagingConfig{
@@ -467,6 +426,7 @@ func GetDefaultConfig() *UserConfig {
Quit: "q",
QuitAlt1: "<c-c>",
Return: "<esc>",
ReturnAlt1: "",
QuitWithoutChangingDirectory: "Q",
TogglePanel: "<tab>",
PrevItem: "<up>",
@@ -489,12 +449,12 @@ func GetDefaultConfig() *UserConfig {
NextMatch: "n",
PrevMatch: "N",
StartSearch: "/",
OptionMenu: "",
OptionMenu: "x",
OptionMenuAlt1: "?",
Select: "<space>",
GoInto: "<enter>",
Confirm: "<enter>",
ConfirmInEditor: "<a-enter>",
ConfirmAlt1: "y",
Remove: "d",
New: "n",
Edit: "e",
@@ -508,8 +468,8 @@ func GetDefaultConfig() *UserConfig {
ScrollDownMainAlt2: "<c-d>",
ExecuteCustomCommand: ":",
CreateRebaseOptionsMenu: "m",
Push: "P",
Pull: "p",
PushFiles: "P",
PullFiles: "p",
Refresh: "R",
CreatePatchOptionsMenu: "<c-p>",
NextTab: "]",
@@ -523,6 +483,7 @@ func GetDefaultConfig() *UserConfig {
DiffingMenuAlt: "<c-e>",
CopyToClipboard: "<c-o>",
SubmitEditorText: "<enter>",
AppendNewline: "<a-enter>",
ExtrasMenu: "@",
ToggleWhitespaceInDiffView: "<c-w>",
IncreaseContextInDiffView: "}",
@@ -560,7 +521,6 @@ func GetDefaultConfig() *UserConfig {
MergeIntoCurrentBranch: "M",
ViewGitFlowOptions: "i",
FastForward: "f",
CreateTag: "T",
PushTag: "P",
SetUpstream: "u",
FetchRemote: "f",
@@ -582,7 +542,7 @@ func GetDefaultConfig() *UserConfig {
CherryPickCopy: "c",
CherryPickCopyRange: "C",
PasteCommits: "v",
CreateTag: "T",
TagCommit: "T",
CheckoutCommit: "<space>",
ResetCherryPick: "<c-R>",
CopyCommitAttributeToClipboard: "y",
@@ -610,7 +570,7 @@ func GetDefaultConfig() *UserConfig {
BulkMenu: "b",
},
},
OS: OSConfig{},
OS: GetPlatformDefaultConfig(),
DisableStartupPopups: false,
CustomCommands: []CustomCommand(nil),
Services: map[string]string(nil),
+3 -3
View File
@@ -79,7 +79,7 @@ func (gui *Gui) getRandomTip() string {
// keybindings and lazygit-specific advice
fmt.Sprintf(
"To force push, press '%s' and then if the push is rejected you will be asked if you want to force push",
formattedKey(config.Universal.Push),
formattedKey(config.Universal.PushFiles),
),
fmt.Sprintf(
"To filter commits by path, press '%s'",
@@ -191,7 +191,7 @@ func (gui *Gui) getRandomTip() string {
),
}
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
randomIndex := rnd.Intn(len(tips))
rand.Seed(time.Now().UnixNano())
randomIndex := rand.Intn(len(tips))
return tips[randomIndex]
}
+1 -2
View File
@@ -15,8 +15,7 @@ func (gui *Gui) commitFilesRenderToMain() error {
to := ref.RefName()
from, reverse := gui.State.Modes.Diffing.GetFromAndReverseArgsForDiff(ref.ParentRefName())
cmdObj := gui.git.WorkingTree.ShowFileDiffCmdObj(from, to, reverse, node.GetPath(), false,
gui.IgnoreWhitespaceInDiffView)
cmdObj := gui.git.WorkingTree.ShowFileDiffCmdObj(from, to, reverse, node.GetPath(), false)
task := types.NewRunPtyTask(cmdObj.GetCmd())
pair := gui.c.MainViewPairs().Normal
+1
View File
@@ -15,6 +15,7 @@ func (gui *Gui) handleCommitMessageFocused() error {
map[string]string{
"keyBindClose": keybindings.Label(gui.c.UserConfig.Keybinding.Universal.Return),
"keyBindConfirm": keybindings.Label(gui.c.UserConfig.Keybinding.Universal.Confirm),
"keyBindNewLine": keybindings.Label(gui.c.UserConfig.Keybinding.Universal.AppendNewline),
},
)
+3 -12
View File
@@ -1,7 +1,6 @@
package gui
import (
"github.com/fsmiamoto/git-todo-parser/todo"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
@@ -35,16 +34,8 @@ func (gui *Gui) branchCommitsRenderToMain() error {
commit := gui.State.Contexts.LocalCommits.GetSelected()
if commit == nil {
task = types.NewRenderStringTask(gui.c.Tr.NoCommitsThisBranch)
} else if commit.Action == todo.UpdateRef {
task = types.NewRenderStringTask(
utils.ResolvePlaceholderString(
gui.c.Tr.UpdateRefHere,
map[string]string{
"ref": commit.Name,
}))
} else {
cmdObj := gui.git.Commit.ShowCmdObj(commit.Sha, gui.State.Modes.Filtering.GetPath(),
gui.IgnoreWhitespaceInDiffView)
cmdObj := gui.git.Commit.ShowCmdObj(commit.Sha, gui.State.Modes.Filtering.GetPath())
task = types.NewRunPtyTask(cmdObj.GetCmd())
}
@@ -59,8 +50,8 @@ func (gui *Gui) branchCommitsRenderToMain() error {
}
func (gui *Gui) secondaryPatchPanelUpdateOpts() *types.ViewUpdateOpts {
if gui.git.Patch.PatchBuilder.Active() {
patch := gui.git.Patch.PatchBuilder.RenderAggregatedPatch(false)
if gui.git.Patch.PatchManager.Active() {
patch := gui.git.Patch.PatchManager.RenderAggregatedPatchColored(false)
return &types.ViewUpdateOpts{
Task: types.NewRenderStringWithoutScrollTask(patch),
+22 -4
View File
@@ -78,11 +78,17 @@ func (gui *Gui) getMessageHeight(wrap bool, message string, width int) int {
return lineCount
}
func (gui *Gui) getPopupPanelDimensionsForContentHeight(panelWidth, contentHeight int) (int, int, int, int) {
return gui.getPopupPanelDimensionsAux(panelWidth, contentHeight)
func (gui *Gui) getConfirmationPanelDimensions(wrap bool, prompt string) (int, int, int, int) {
panelWidth := gui.getConfirmationPanelWidth()
panelHeight := gui.getMessageHeight(wrap, prompt, panelWidth)
return gui.getConfirmationPanelDimensionsAux(panelWidth, panelHeight)
}
func (gui *Gui) getPopupPanelDimensionsAux(panelWidth int, panelHeight int) (int, int, int, int) {
func (gui *Gui) getConfirmationPanelDimensionsForContentHeight(panelWidth, contentHeight int) (int, int, int, int) {
return gui.getConfirmationPanelDimensionsAux(panelWidth, contentHeight)
}
func (gui *Gui) getConfirmationPanelDimensionsAux(panelWidth int, panelHeight int) (int, int, int, int) {
width, height := gui.g.Size()
if panelHeight > height*3/4 {
panelHeight = height * 3 / 4
@@ -180,7 +186,7 @@ func (gui *Gui) createPopupPanel(ctx context.Context, opts types.CreatePopupPane
}
confirmationView := gui.Views.Confirmation
confirmationView.Editable = opts.Editable
confirmationView.Editor = gocui.EditorFunc(gui.promptEditor)
confirmationView.Editor = gocui.EditorFunc(gui.defaultEditor)
if opts.Editable {
textArea := confirmationView.TextArea
@@ -235,6 +241,11 @@ func (gui *Gui) setKeyBindings(cancel context.CancelFunc, opts types.CreatePopup
Key: keybindings.GetKey(keybindingConfig.Universal.Confirm),
Handler: onConfirm,
},
{
ViewName: "confirmation",
Key: keybindings.GetKey(keybindingConfig.Universal.ConfirmAlt1),
Handler: onConfirm,
},
{
ViewName: "confirmation",
Key: keybindings.GetKey(keybindingConfig.Universal.Return),
@@ -255,6 +266,11 @@ func (gui *Gui) setKeyBindings(cancel context.CancelFunc, opts types.CreatePopup
Key: keybindings.GetKey(keybindingConfig.Universal.Confirm),
Handler: onSuggestionConfirm,
},
{
ViewName: "suggestions",
Key: keybindings.GetKey(keybindingConfig.Universal.ConfirmAlt1),
Handler: onSuggestionConfirm,
},
{
ViewName: "suggestions",
Key: keybindings.GetKey(keybindingConfig.Universal.Return),
@@ -279,8 +295,10 @@ func (gui *Gui) setKeyBindings(cancel context.CancelFunc, opts types.CreatePopup
func (gui *Gui) clearConfirmationViewKeyBindings() {
keybindingConfig := gui.c.UserConfig.Keybinding
_ = gui.g.DeleteKeybinding("confirmation", keybindings.GetKey(keybindingConfig.Universal.Confirm), gocui.ModNone)
_ = gui.g.DeleteKeybinding("confirmation", keybindings.GetKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone)
_ = gui.g.DeleteKeybinding("confirmation", keybindings.GetKey(keybindingConfig.Universal.Return), gocui.ModNone)
_ = gui.g.DeleteKeybinding("suggestions", keybindings.GetKey(keybindingConfig.Universal.Confirm), gocui.ModNone)
_ = gui.g.DeleteKeybinding("suggestions", keybindings.GetKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone)
_ = gui.g.DeleteKeybinding("suggestions", keybindings.GetKey(keybindingConfig.Universal.Return), gocui.ModNone)
}
+6 -52
View File
@@ -9,7 +9,6 @@ import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/samber/lo"
)
// This file is for the management of contexts. There is a context stack such that
@@ -53,7 +52,7 @@ func (gui *Gui) pushContext(c types.Context, opts types.OnFocusOpts) error {
return nil
}
contextsToDeactivate, contextToActivate := gui.pushToContextStack(c)
contextsToDeactivate := gui.pushToContextStack(c)
for _, contextToDeactivate := range contextsToDeactivate {
if err := gui.deactivateContext(contextToDeactivate, types.OnFocusLostOpts{NewContextKey: c.GetKey()}); err != nil {
@@ -61,28 +60,16 @@ func (gui *Gui) pushContext(c types.Context, opts types.OnFocusOpts) error {
}
}
if contextToActivate == nil {
return nil
}
return gui.activateContext(contextToActivate, opts)
return gui.activateContext(c, opts)
}
// Adjusts the context stack based on the context that's being pushed and
// returns (contexts to deactivate, context to activate)
func (gui *Gui) pushToContextStack(c types.Context) ([]types.Context, types.Context) {
// Adjusts the context stack based on the context that's being pushed and returns contexts to deactivate
func (gui *Gui) pushToContextStack(c types.Context) []types.Context {
contextsToDeactivate := []types.Context{}
gui.State.ContextManager.Lock()
defer gui.State.ContextManager.Unlock()
if len(gui.State.ContextManager.ContextStack) > 0 &&
c == gui.State.ContextManager.ContextStack[len(gui.State.ContextManager.ContextStack)-1] {
// Context being pushed is already on top of the stack: nothing to
// deactivate or activate
return contextsToDeactivate, nil
}
if len(gui.State.ContextManager.ContextStack) == 0 {
gui.State.ContextManager.ContextStack = append(gui.State.ContextManager.ContextStack, c)
} else if c.GetKind() == types.SIDE_CONTEXT {
@@ -91,15 +78,12 @@ func (gui *Gui) pushToContextStack(c types.Context) ([]types.Context, types.Cont
gui.State.ContextManager.ContextStack = []types.Context{c}
} else if c.GetKind() == types.MAIN_CONTEXT {
// if we're switching to a main context, remove all other main contexts in the stack
contextsToKeep := []types.Context{}
for _, stackContext := range gui.State.ContextManager.ContextStack {
if stackContext.GetKind() == types.MAIN_CONTEXT {
contextsToDeactivate = append(contextsToDeactivate, stackContext)
} else {
contextsToKeep = append(contextsToKeep, stackContext)
}
}
gui.State.ContextManager.ContextStack = append(contextsToKeep, c)
gui.State.ContextManager.ContextStack = []types.Context{c}
} else {
topContext := gui.currentContextWithoutLock()
@@ -121,7 +105,7 @@ func (gui *Gui) pushToContextStack(c types.Context) ([]types.Context, types.Cont
}
}
return contextsToDeactivate, c
return contextsToDeactivate
}
func (gui *Gui) popContext() error {
@@ -147,36 +131,6 @@ func (gui *Gui) popContext() error {
return gui.activateContext(newContext, types.OnFocusOpts{})
}
func (gui *Gui) removeContexts(contextsToRemove []types.Context) error {
gui.State.ContextManager.Lock()
if len(gui.State.ContextManager.ContextStack) == 1 {
gui.State.ContextManager.Unlock()
return nil
}
rest := lo.Filter(gui.State.ContextManager.ContextStack, func(context types.Context, _ int) bool {
for _, contextToRemove := range contextsToRemove {
if context.GetKey() == contextToRemove.GetKey() {
return false
}
}
return true
})
gui.State.ContextManager.ContextStack = rest
contextToActivate := rest[len(rest)-1]
gui.State.ContextManager.Unlock()
for _, context := range contextsToRemove {
if err := gui.deactivateContext(context, types.OnFocusLostOpts{NewContextKey: contextToActivate.GetKey()}); err != nil {
return err
}
}
// activate the item at the top of the stack
return gui.activateContext(contextToActivate, types.OnFocusOpts{})
}
func (gui *Gui) deactivateContext(c types.Context, opts types.OnFocusLostOpts) error {
view, _ := gui.g.View(c.GetViewName())
-3
View File
@@ -20,7 +20,6 @@ type BaseContext struct {
focusable bool
transient bool
hasControlledBounds bool
highlightOnFocus bool
*ParentContextMgr
}
@@ -35,7 +34,6 @@ type NewBaseContextOpts struct {
Focusable bool
Transient bool
HasUncontrolledBounds bool // negating for the sake of making false the default
HighlightOnFocus bool
OnGetOptionsMap func() map[string]string
}
@@ -54,7 +52,6 @@ func NewBaseContext(opts NewBaseContextOpts) *BaseContext {
focusable: opts.Focusable,
transient: opts.Transient,
hasControlledBounds: hasControlledBounds,
highlightOnFocus: opts.HighlightOnFocus,
ParentContextMgr: &ParentContextMgr{},
viewTrait: viewTrait,
}
-95
View File
@@ -1,95 +0,0 @@
package context
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type CommitMessageContext struct {
types.Context
viewModel *CommitMessageViewModel
}
// when selectedIndex (see below) is set to this value, it means that we're not
// currently viewing a commit message of an existing commit: instead we're making our own
// new commit message
const NoCommitIndex = -1
type CommitMessageViewModel struct {
// index of the commit message, where -1 is 'no commit', 0 is the HEAD commit, 1
// is the prior commit, and so on
selectedindex int
// if true, then upon escaping from the commit message panel, we will preserve
// the message so that it's still shown next time we open the panel
preserveMessage bool
// the full preserved message (combined summary and description)
preservedMessage string
// invoked when pressing enter in the commit message panel
onConfirm func(string) error
// The message typed in before cycling through history
// We store this separately to 'preservedMessage' because 'preservedMessage'
// is specifically for committing staged files and we don't want this affected
// by cycling through history in the context of rewording an old commit.
historyMessage string
}
func NewCommitMessageContext(
view *gocui.View,
opts ContextCallbackOpts,
) *CommitMessageContext {
viewModel := &CommitMessageViewModel{}
return &CommitMessageContext{
viewModel: viewModel,
Context: NewSimpleContext(
NewBaseContext(NewBaseContextOpts{
Kind: types.PERSISTENT_POPUP,
View: view,
WindowName: "commitMessage",
Key: COMMIT_MESSAGE_CONTEXT_KEY,
Focusable: true,
HasUncontrolledBounds: true,
}),
opts,
),
}
}
func (self *CommitMessageContext) SetSelectedIndex(value int) {
self.viewModel.selectedindex = value
}
func (self *CommitMessageContext) GetSelectedIndex() int {
return self.viewModel.selectedindex
}
func (self *CommitMessageContext) GetPreserveMessage() bool {
return self.viewModel.preserveMessage
}
func (self *CommitMessageContext) GetPreservedMessage() string {
return self.viewModel.preservedMessage
}
func (self *CommitMessageContext) SetPreservedMessage(message string) {
self.viewModel.preservedMessage = message
}
func (self *CommitMessageContext) GetHistoryMessage() string {
return self.viewModel.historyMessage
}
func (self *CommitMessageContext) SetHistoryMessage(message string) {
self.viewModel.historyMessage = message
}
func (self *CommitMessageContext) OnConfirm(message string) error {
return self.viewModel.onConfirm(message)
}
func (self *CommitMessageContext) SetPanelState(index int, title string, preserveMessage bool, onConfirm func(string) error) {
self.viewModel.selectedindex = index
self.viewModel.preserveMessage = preserveMessage
self.viewModel.onConfirm = onConfirm
self.GetView().Title = title
}
+8 -11
View File
@@ -33,14 +33,13 @@ const (
INFORMATION_CONTEXT_KEY types.ContextKey = "information"
LIMIT_CONTEXT_KEY types.ContextKey = "limit"
MENU_CONTEXT_KEY types.ContextKey = "menu"
CONFIRMATION_CONTEXT_KEY types.ContextKey = "confirmation"
SEARCH_CONTEXT_KEY types.ContextKey = "search"
COMMIT_MESSAGE_CONTEXT_KEY types.ContextKey = "commitMessage"
COMMIT_DESCRIPTION_CONTEXT_KEY types.ContextKey = "commitDescription"
SUBMODULES_CONTEXT_KEY types.ContextKey = "submodules"
SUGGESTIONS_CONTEXT_KEY types.ContextKey = "suggestions"
COMMAND_LOG_CONTEXT_KEY types.ContextKey = "cmdLog"
MENU_CONTEXT_KEY types.ContextKey = "menu"
CONFIRMATION_CONTEXT_KEY types.ContextKey = "confirmation"
SEARCH_CONTEXT_KEY types.ContextKey = "search"
COMMIT_MESSAGE_CONTEXT_KEY types.ContextKey = "commitMessage"
SUBMODULES_CONTEXT_KEY types.ContextKey = "submodules"
SUGGESTIONS_CONTEXT_KEY types.ContextKey = "suggestions"
COMMAND_LOG_CONTEXT_KEY types.ContextKey = "cmdLog"
)
var AllContextKeys = []types.ContextKey{
@@ -98,8 +97,7 @@ type ContextTree struct {
CustomPatchBuilderSecondary types.Context
MergeConflicts *MergeConflictsContext
Confirmation types.Context
CommitMessage *CommitMessageContext
CommitDescription types.Context
CommitMessage types.Context
CommandLog types.Context
// display contexts
@@ -131,7 +129,6 @@ func (self *ContextTree) Flatten() []types.Context {
self.Menu,
self.Confirmation,
self.CommitMessage,
self.CommitDescription,
self.MergeConflicts,
self.StagingSecondary,
+9 -10
View File
@@ -42,13 +42,12 @@ func NewMergeConflictsContext(
mutex: &deadlock.Mutex{},
Context: NewSimpleContext(
NewBaseContext(NewBaseContextOpts{
Kind: types.MAIN_CONTEXT,
View: view,
WindowName: "main",
Key: MERGE_CONFLICTS_CONTEXT_KEY,
OnGetOptionsMap: getOptionsMap,
Focusable: true,
HighlightOnFocus: true,
Kind: types.MAIN_CONTEXT,
View: view,
WindowName: "main",
Key: MERGE_CONFLICTS_CONTEXT_KEY,
OnGetOptionsMap: getOptionsMap,
Focusable: true,
}),
opts,
),
@@ -78,7 +77,7 @@ func (self *MergeConflictsContext) IsUserScrolling() bool {
func (self *MergeConflictsContext) RenderAndFocus(isFocused bool) error {
self.setContent(isFocused)
self.FocusSelection()
self.focusSelection()
self.c.Render()
@@ -105,9 +104,9 @@ func (self *MergeConflictsContext) setContent(isFocused bool) {
self.GetView().SetContent(self.GetContentToRender(isFocused))
}
func (self *MergeConflictsContext) FocusSelection() {
func (self *MergeConflictsContext) focusSelection() {
if !self.IsUserScrolling() {
_ = self.GetView().SetOriginY(self.GetOriginY())
_ = self.GetView().SetOrigin(self.GetView().OriginX(), self.GetOriginY())
}
}
+12 -12
View File
@@ -37,12 +37,11 @@ func NewPatchExplorerContext(
mutex: &deadlock.Mutex{},
getIncludedLineIndices: getIncludedLineIndices,
SimpleContext: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
View: view,
WindowName: windowName,
Key: key,
Kind: types.MAIN_CONTEXT,
Focusable: true,
HighlightOnFocus: true,
View: view,
WindowName: windowName,
Key: key,
Kind: types.MAIN_CONTEXT,
Focusable: true,
}), ContextCallbackOpts{
OnFocus: onFocus,
OnFocusLost: onFocusLost,
@@ -69,7 +68,7 @@ func (self *PatchExplorerContext) GetIncludedLineIndices() []int {
func (self *PatchExplorerContext) RenderAndFocus(isFocused bool) error {
self.setContent(isFocused)
self.FocusSelection()
self.focusSelection()
self.c.Render()
return nil
@@ -84,7 +83,7 @@ func (self *PatchExplorerContext) Render(isFocused bool) error {
}
func (self *PatchExplorerContext) Focus() error {
self.FocusSelection()
self.focusSelection()
self.c.Render()
return nil
@@ -94,18 +93,19 @@ func (self *PatchExplorerContext) setContent(isFocused bool) {
self.GetView().SetContent(self.GetContentToRender(isFocused))
}
func (self *PatchExplorerContext) FocusSelection() {
func (self *PatchExplorerContext) focusSelection() {
view := self.GetView()
state := self.GetState()
_, viewHeight := view.Size()
bufferHeight := viewHeight - 1
_, origin := view.Origin()
newOriginY := state.CalculateOrigin(origin, bufferHeight)
selectedLineIdx := state.GetSelectedLineIdx()
_ = view.SetOriginY(newOriginY)
newOrigin := state.CalculateOrigin(origin, bufferHeight)
view.SetCursorY(state.GetSelectedLineIdx() - newOriginY)
_ = view.SetOriginY(newOrigin)
_ = view.SetCursor(0, selectedLineIdx-newOrigin)
}
func (self *PatchExplorerContext) GetContentToRender(isFocused bool) string {
-4
View File
@@ -50,10 +50,6 @@ func NewDisplayContext(key types.ContextKey, view *gocui.View, windowName string
}
func (self *SimpleContext) HandleFocus(opts types.OnFocusOpts) error {
if self.highlightOnFocus {
self.GetViewTrait().SetHighlight(true)
}
if self.OnFocus != nil {
if err := self.OnFocus(opts); err != nil {
return err
+5 -20
View File
@@ -30,8 +30,7 @@ func NewSubCommitsContext(
) *SubCommitsContext {
viewModel := &SubCommitsViewModel{
BasicViewModel: NewBasicViewModel(getModel),
ref: nil,
limitCommits: true,
refName: "",
}
return &SubCommitsContext{
@@ -61,18 +60,12 @@ func NewSubCommitsContext(
type SubCommitsViewModel struct {
// name of the ref that the sub-commits are shown for
ref types.Ref
refName string
*BasicViewModel[*models.Commit]
limitCommits bool
}
func (self *SubCommitsViewModel) SetRef(ref types.Ref) {
self.ref = ref
}
func (self *SubCommitsViewModel) GetRef() types.Ref {
return self.ref
func (self *SubCommitsViewModel) SetRefName(refName string) {
self.refName = refName
}
func (self *SubCommitsContext) GetSelectedItemId() string {
@@ -101,13 +94,5 @@ func (self *SubCommitsContext) GetCommits() []*models.Commit {
}
func (self *SubCommitsContext) Title() string {
return fmt.Sprintf(self.c.Tr.SubCommitsDynamicTitle, utils.TruncateWithEllipsis(self.ref.RefName(), 50))
}
func (self *SubCommitsContext) SetLimitCommits(value bool) {
self.limitCommits = value
}
func (self *SubCommitsContext) GetLimitCommits() bool {
return self.limitCommits
return fmt.Sprintf(self.c.Tr.SubCommitsDynamicTitle, utils.TruncateWithEllipsis(self.refName, 50))
}
+8 -20
View File
@@ -150,15 +150,15 @@ func (gui *Gui) contextTree() *context.ContextTree {
func(opts types.OnFocusLostOpts) error {
gui.Views.PatchBuilding.Wrap = true
if gui.git.Patch.PatchBuilder.IsEmpty() {
gui.git.Patch.PatchBuilder.Reset()
if gui.git.Patch.PatchManager.IsEmpty() {
gui.git.Patch.PatchManager.Reset()
}
return nil
},
func() []int {
filename := gui.State.Contexts.CommitFiles.GetSelectedPath()
includedLineIndices, err := gui.git.Patch.PatchBuilder.GetFileIncLineIndices(filename)
includedLineIndices, err := gui.git.Patch.PatchManager.GetFileIncLineIndices(filename)
if err != nil {
gui.Log.Error(err)
return nil
@@ -217,29 +217,17 @@ func (gui *Gui) contextTree() *context.ContextTree {
},
},
),
CommitMessage: context.NewCommitMessageContext(
gui.Views.CommitMessage,
context.ContextCallbackOpts{
OnFocus: OnFocusWrapper(gui.handleCommitMessageFocused),
},
),
CommitDescription: context.NewSimpleContext(
CommitMessage: context.NewSimpleContext(
context.NewBaseContext(context.NewBaseContextOpts{
Kind: types.PERSISTENT_POPUP,
View: gui.Views.CommitDescription,
WindowName: "commitDescription",
Key: context.COMMIT_DESCRIPTION_CONTEXT_KEY,
View: gui.Views.CommitMessage,
WindowName: "commitMessage",
Key: context.COMMIT_MESSAGE_CONTEXT_KEY,
Focusable: true,
HasUncontrolledBounds: true,
}),
context.ContextCallbackOpts{
OnFocus: func(opts types.OnFocusOpts) error {
_, err := gui.g.SetViewBeneath("commitDescription", "commitMessage", 10)
if err != nil {
return err
}
return nil
},
OnFocus: OnFocusWrapper(gui.handleCommitMessageFocused),
},
),
Search: context.NewSimpleContext(
+25 -38
View File
@@ -25,25 +25,10 @@ func (gui *Gui) resetControllers() {
rebaseHelper := helpers.NewMergeAndRebaseHelper(helperCommon, gui.State.Contexts, gui.git, refsHelper)
suggestionsHelper := helpers.NewSuggestionsHelper(helperCommon, model, gui.refreshSuggestions)
setCommitSummary := gui.getCommitMessageSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitMessage })
setCommitDescription := gui.getCommitMessageSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitDescription })
getCommitSummary := func() string {
return strings.TrimSpace(gui.Views.CommitMessage.TextArea.GetContent())
setCommitMessage := gui.getSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitMessage })
getSavedCommitMessage := func() string {
return gui.State.savedCommitMessage
}
getCommitDescription := func() string {
return strings.TrimSpace(gui.Views.CommitDescription.TextArea.GetContent())
}
commitsHelper := helpers.NewCommitsHelper(helperCommon,
gui.State.Model,
gui.State.Contexts,
getCommitSummary,
setCommitSummary,
getCommitDescription,
setCommitDescription,
gui.RenderCommitLength,
)
gpgHelper := helpers.NewGpgHelper(helperCommon, gui.os, gui.git)
gui.helpers = &helpers.Helpers{
Refs: refsHelper,
Host: helpers.NewHostHelper(helperCommon, gui.git),
@@ -51,9 +36,9 @@ func (gui *Gui) resetControllers() {
Bisect: helpers.NewBisectHelper(helperCommon, gui.git),
Suggestions: suggestionsHelper,
Files: helpers.NewFilesHelper(helperCommon, gui.git, osCommand),
WorkingTree: helpers.NewWorkingTreeHelper(helperCommon, gui.git, gui.State.Contexts, refsHelper, model, setCommitSummary, commitsHelper, gpgHelper),
WorkingTree: helpers.NewWorkingTreeHelper(helperCommon, gui.git, gui.State.Contexts, refsHelper, model, setCommitMessage, getSavedCommitMessage),
Tags: helpers.NewTagsHelper(helperCommon, gui.git),
GPG: gpgHelper,
GPG: helpers.NewGpgHelper(helperCommon, gui.os, gui.git),
MergeAndRebase: rebaseHelper,
MergeConflicts: helpers.NewMergeConflictsHelper(helperCommon, gui.State.Contexts, gui.git),
CherryPick: helpers.NewCherryPickHelper(
@@ -63,9 +48,7 @@ func (gui *Gui) resetControllers() {
func() *cherrypicking.CherryPicking { return gui.State.Modes.CherryPicking },
rebaseHelper,
),
Upstream: helpers.NewUpstreamHelper(helperCommon, model, suggestionsHelper.GetRemoteBranchesSuggestionsFunc),
AmendHelper: helpers.NewAmendHelper(helperCommon, gui.git, gpgHelper),
Commits: commitsHelper,
Upstream: helpers.NewUpstreamHelper(helperCommon, model, suggestionsHelper.GetRemoteBranchesSuggestionsFunc),
}
gui.CustomCommandsClient = custom_commands.NewClient(
@@ -98,12 +81,24 @@ func (gui *Gui) resetControllers() {
bisectController := controllers.NewBisectController(common)
getCommitMessage := func() string {
return strings.TrimSpace(gui.Views.CommitMessage.TextArea.GetContent())
}
onCommitAttempt := func(message string) {
gui.State.savedCommitMessage = message
gui.Views.CommitMessage.ClearTextArea()
}
onCommitSuccess := func() {
gui.State.savedCommitMessage = ""
}
commitMessageController := controllers.NewCommitMessageController(
common,
)
commitDescriptionController := controllers.NewCommitDescriptionController(
common,
getCommitMessage,
onCommitAttempt,
onCommitSuccess,
)
remoteBranchesController := controllers.NewRemoteBranchesController(common)
@@ -114,7 +109,8 @@ func (gui *Gui) resetControllers() {
filesController := controllers.NewFilesController(
common,
gui.enterSubmodule,
setCommitSummary,
setCommitMessage,
getSavedCommitMessage,
)
mergeConflictsController := controllers.NewMergeConflictsController(common)
remotesController := controllers.NewRemotesController(
@@ -137,12 +133,7 @@ func (gui *Gui) resetControllers() {
patchBuildingController := controllers.NewPatchBuildingController(common)
snakeController := controllers.NewSnakeController(common, func() *snake.Game { return gui.snakeGame })
setSubCommits := func(commits []*models.Commit) {
gui.Mutexes.SubCommitsMutex.Lock()
defer gui.Mutexes.SubCommitsMutex.Unlock()
gui.State.Model.SubCommits = commits
}
setSubCommits := func(commits []*models.Commit) { gui.State.Model.SubCommits = commits }
for _, context := range []controllers.CanSwitchToSubCommits{
gui.State.Contexts.Branches,
@@ -248,10 +239,6 @@ func (gui *Gui) resetControllers() {
commitMessageController,
)
controllers.AttachControllers(gui.State.Contexts.CommitDescription,
commitDescriptionController,
)
controllers.AttachControllers(gui.State.Contexts.RemoteBranches,
remoteBranchesController,
)
@@ -86,11 +86,6 @@ func (self *BranchesController) GetKeybindings(opts types.KeybindingsOpts) []*ty
Handler: self.checkSelectedAndReal(self.fastForward),
Description: self.c.Tr.FastForward,
},
{
Key: opts.GetKey(opts.Config.Branches.CreateTag),
Handler: self.checkSelected(self.createTag),
Description: self.c.Tr.LcCreateTag,
},
{
Key: opts.GetKey(opts.Config.Commits.ViewResetOptions),
Handler: self.checkSelected(self.createResetMenu),
@@ -368,10 +363,6 @@ func (self *BranchesController) fastForward(branch *models.Branch) error {
})
}
func (self *BranchesController) createTag(branch *models.Branch) error {
return self.helpers.Tags.CreateTagMenu(branch.FullRefName(), func() {})
}
func (self *BranchesController) createResetMenu(selectedBranch *models.Branch) error {
return self.helpers.Refs.CreateGitResetMenu(selectedBranch.Name)
}
@@ -1,60 +0,0 @@
package controllers
import (
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type CommitDescriptionController struct {
baseController
*controllerCommon
}
var _ types.IController = &CommitMessageController{}
func NewCommitDescriptionController(
common *controllerCommon,
) *CommitDescriptionController {
return &CommitDescriptionController{
baseController: baseController{},
controllerCommon: common,
}
}
func (self *CommitDescriptionController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
bindings := []*types.Binding{
{
Key: opts.GetKey(opts.Config.Universal.TogglePanel),
Handler: self.switchToCommitMessage,
},
{
Key: opts.GetKey(opts.Config.Universal.Return),
Handler: self.close,
},
{
Key: opts.GetKey(opts.Config.Universal.ConfirmInEditor),
Handler: self.confirm,
},
}
return bindings
}
func (self *CommitDescriptionController) Context() types.Context {
return self.context()
}
func (self *CommitDescriptionController) context() types.Context {
return self.contexts.CommitMessage
}
func (self *CommitDescriptionController) switchToCommitMessage() error {
return self.c.PushContext(self.contexts.CommitMessage)
}
func (self *CommitDescriptionController) close() error {
return self.helpers.Commits.CloseCommitMessagePanel()
}
func (self *CommitDescriptionController) confirm() error {
return self.helpers.Commits.HandleCommitConfirm()
}
@@ -1,24 +1,33 @@
package controllers
import (
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type CommitMessageController struct {
baseController
*controllerCommon
getCommitMessage func() string
onCommitAttempt func(message string)
onCommitSuccess func()
}
var _ types.IController = &CommitMessageController{}
func NewCommitMessageController(
common *controllerCommon,
getCommitMessage func() string,
onCommitAttempt func(message string),
onCommitSuccess func(),
) *CommitMessageController {
return &CommitMessageController{
baseController: baseController{},
controllerCommon: common,
getCommitMessage: getCommitMessage,
onCommitAttempt: onCommitAttempt,
onCommitSuccess: onCommitSuccess,
}
}
@@ -32,18 +41,6 @@ func (self *CommitMessageController) GetKeybindings(opts types.KeybindingsOpts)
Key: opts.GetKey(opts.Config.Universal.Return),
Handler: self.close,
},
{
Key: opts.GetKey(opts.Config.Universal.PrevItem),
Handler: self.handlePreviousCommit,
},
{
Key: opts.GetKey(opts.Config.Universal.NextItem),
Handler: self.handleNextCommit,
},
{
Key: opts.GetKey(opts.Config.Universal.TogglePanel),
Handler: self.switchToCommitDescription,
},
}
return bindings
@@ -53,63 +50,30 @@ func (self *CommitMessageController) Context() types.Context {
return self.context()
}
func (self *CommitMessageController) context() *context.CommitMessageContext {
// this method is pointless in this context but I'm keeping it consistent
// with other contexts so that when generics arrive it's easier to refactor
func (self *CommitMessageController) context() types.Context {
return self.contexts.CommitMessage
}
func (self *CommitMessageController) handlePreviousCommit() error {
return self.handleCommitIndexChange(1)
}
func (self *CommitMessageController) handleNextCommit() error {
if self.context().GetSelectedIndex() == context.NoCommitIndex {
return nil
}
return self.handleCommitIndexChange(-1)
}
func (self *CommitMessageController) switchToCommitDescription() error {
if err := self.c.PushContext(self.contexts.CommitDescription); err != nil {
return err
}
return nil
}
func (self *CommitMessageController) handleCommitIndexChange(value int) error {
currentIndex := self.context().GetSelectedIndex()
newIndex := currentIndex + value
if newIndex == context.NoCommitIndex {
self.context().SetSelectedIndex(newIndex)
self.helpers.Commits.SetMessageAndDescriptionInView(self.context().GetHistoryMessage())
return nil
} else if currentIndex == context.NoCommitIndex {
self.context().SetHistoryMessage(self.helpers.Commits.JoinCommitMessageAndDescription())
}
validCommit, err := self.setCommitMessageAtIndex(newIndex)
if validCommit {
self.context().SetSelectedIndex(newIndex)
}
return err
}
// returns true if the given index is for a valid commit
func (self *CommitMessageController) setCommitMessageAtIndex(index int) (bool, error) {
commitMessage, err := self.git.Commit.GetCommitMessageFromHistory(index)
if err != nil {
if err == git_commands.ErrInvalidCommitIndex {
return false, nil
}
return false, self.c.ErrorMsg(self.c.Tr.CommitWithoutMessageErr)
}
self.helpers.Commits.UpdateCommitPanelView(commitMessage)
return true, nil
}
func (self *CommitMessageController) confirm() error {
return self.helpers.Commits.HandleCommitConfirm()
message := self.getCommitMessage()
self.onCommitAttempt(message)
if message == "" {
return self.c.ErrorMsg(self.c.Tr.CommitWithoutMessageErr)
}
cmdObj := self.git.Commit.CommitCmdObj(message)
self.c.LogAction(self.c.Tr.Actions.Commit)
_ = self.c.PopContext()
return self.helpers.GPG.WithGpgHandling(cmdObj, self.c.Tr.CommittingStatus, func() error {
self.onCommitSuccess()
return nil
})
}
func (self *CommitMessageController) close() error {
return self.helpers.Commits.CloseCommitMessagePanel()
return self.c.PopContext()
}
+15 -15
View File
@@ -157,8 +157,8 @@ func (self *CommitFilesController) edit(node *filetree.CommitFileNode) error {
func (self *CommitFilesController) toggleForPatch(node *filetree.CommitFileNode) error {
toggle := func() error {
return self.c.WithWaitingStatus(self.c.Tr.LcUpdatingPatch, func() error {
if !self.git.Patch.PatchBuilder.Active() {
if err := self.startPatchBuilder(); err != nil {
if !self.git.Patch.PatchManager.Active() {
if err := self.startPatchManager(); err != nil {
return err
}
}
@@ -166,34 +166,34 @@ func (self *CommitFilesController) toggleForPatch(node *filetree.CommitFileNode)
// if there is any file that hasn't been fully added we'll fully add everything,
// otherwise we'll remove everything
adding := node.SomeFile(func(file *models.CommitFile) bool {
return self.git.Patch.PatchBuilder.GetFileStatus(file.Name, self.context().GetRef().RefName()) != patch.WHOLE
return self.git.Patch.PatchManager.GetFileStatus(file.Name, self.context().GetRef().RefName()) != patch.WHOLE
})
err := node.ForEachFile(func(file *models.CommitFile) error {
if adding {
return self.git.Patch.PatchBuilder.AddFileWhole(file.Name)
return self.git.Patch.PatchManager.AddFileWhole(file.Name)
} else {
return self.git.Patch.PatchBuilder.RemoveFile(file.Name)
return self.git.Patch.PatchManager.RemoveFile(file.Name)
}
})
if err != nil {
return self.c.Error(err)
}
if self.git.Patch.PatchBuilder.IsEmpty() {
self.git.Patch.PatchBuilder.Reset()
if self.git.Patch.PatchManager.IsEmpty() {
self.git.Patch.PatchManager.Reset()
}
return self.c.PostRefreshUpdate(self.context())
})
}
if self.git.Patch.PatchBuilder.Active() && self.git.Patch.PatchBuilder.To != self.context().GetRef().RefName() {
if self.git.Patch.PatchManager.Active() && self.git.Patch.PatchManager.To != self.context().GetRef().RefName() {
return self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.DiscardPatch,
Prompt: self.c.Tr.DiscardPatchConfirm,
HandleConfirm: func() error {
self.git.Patch.PatchBuilder.Reset()
self.git.Patch.PatchManager.Reset()
return toggle()
},
})
@@ -207,7 +207,7 @@ func (self *CommitFilesController) toggleAllForPatch(_ *filetree.CommitFileNode)
return self.toggleForPatch(root)
}
func (self *CommitFilesController) startPatchBuilder() error {
func (self *CommitFilesController) startPatchManager() error {
commitFilesContext := self.context()
canRebase := commitFilesContext.GetCanRebase()
@@ -215,7 +215,7 @@ func (self *CommitFilesController) startPatchBuilder() error {
to := ref.RefName()
from, reverse := self.modes.Diffing.GetFromAndReverseArgsForDiff(ref.ParentRefName())
self.git.Patch.PatchBuilder.Start(from, to, reverse, canRebase)
self.git.Patch.PatchManager.Start(from, to, reverse, canRebase)
return nil
}
@@ -229,8 +229,8 @@ func (self *CommitFilesController) enterCommitFile(node *filetree.CommitFileNode
}
enterTheFile := func() error {
if !self.git.Patch.PatchBuilder.Active() {
if err := self.startPatchBuilder(); err != nil {
if !self.git.Patch.PatchManager.Active() {
if err := self.startPatchManager(); err != nil {
return err
}
}
@@ -238,12 +238,12 @@ func (self *CommitFilesController) enterCommitFile(node *filetree.CommitFileNode
return self.c.PushContext(self.contexts.CustomPatchBuilder, opts)
}
if self.git.Patch.PatchBuilder.Active() && self.git.Patch.PatchBuilder.To != self.context().GetRef().RefName() {
if self.git.Patch.PatchManager.Active() && self.git.Patch.PatchManager.To != self.context().GetRef().RefName() {
return self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.DiscardPatch,
Prompt: self.c.Tr.DiscardPatchConfirm,
HandleConfirm: func() error {
self.git.Patch.PatchBuilder.Reset()
self.git.Patch.PatchManager.Reset()
return enterTheFile()
},
})
@@ -101,7 +101,7 @@ func (self *ContextLinesController) applyChange() error {
}
func (self *ContextLinesController) checkCanChangeContext() error {
if self.git.Patch.PatchBuilder.Active() {
if self.git.Patch.PatchManager.Active() {
return errors.New(self.c.Tr.CantChangeContextSizeError)
}
+18 -7
View File
@@ -12,11 +12,12 @@ import (
)
type FilesController struct {
baseController // nolint: unused
baseController
*controllerCommon
enterSubmodule func(submodule *models.SubmoduleConfig) error
setCommitMessage func(message string)
enterSubmodule func(submodule *models.SubmoduleConfig) error
setCommitMessage func(message string)
getSavedCommitMessage func() string
}
var _ types.IController = &FilesController{}
@@ -25,11 +26,13 @@ func NewFilesController(
common *controllerCommon,
enterSubmodule func(submodule *models.SubmoduleConfig) error,
setCommitMessage func(message string),
getSavedCommitMessage func() string,
) *FilesController {
return &FilesController{
controllerCommon: common,
enterSubmodule: enterSubmodule,
setCommitMessage: setCommitMessage,
controllerCommon: common,
enterSubmodule: enterSubmodule,
setCommitMessage: setCommitMessage,
getSavedCommitMessage: getSavedCommitMessage,
}
}
@@ -564,7 +567,15 @@ func (self *FilesController) handleAmendCommitPress() error {
return self.c.ErrorMsg(self.c.Tr.NoCommitToAmend)
}
return self.helpers.AmendHelper.AmendHead()
return self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.AmendLastCommitTitle,
Prompt: self.c.Tr.SureToAmend,
HandleConfirm: func() error {
cmdObj := self.git.Commit.AmendHeadCmdObj()
self.c.LogAction(self.c.Tr.Actions.AmendCommit)
return self.helpers.GPG.WithGpgHandling(cmdObj, self.c.Tr.AmendingStatus, nil)
},
})
}
func (self *FilesController) handleStatusFilterPressed() error {
+2 -27
View File
@@ -5,7 +5,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// splitting this action out into its own file because it's self-contained
@@ -52,13 +51,7 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error {
}
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
},
Key: 'x',
Tooltip: utils.ResolvePlaceholderString(
self.c.Tr.DiscardAllTooltip,
map[string]string{
"path": node.GetPath(),
},
),
Key: 'd',
},
}
@@ -74,12 +67,6 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error {
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
},
Key: 'u',
Tooltip: utils.ResolvePlaceholderString(
self.c.Tr.DiscardUnstagedTooltip,
map[string]string{
"path": node.GetPath(),
},
),
})
}
} else {
@@ -108,13 +95,7 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error {
}
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
},
Key: 'x',
Tooltip: utils.ResolvePlaceholderString(
self.c.Tr.DiscardAllTooltip,
map[string]string{
"path": node.GetPath(),
},
),
Key: 'd',
},
}
@@ -130,12 +111,6 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error {
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
},
Key: 'u',
Tooltip: utils.ResolvePlaceholderString(
self.c.Tr.DiscardUnstagedTooltip,
map[string]string{
"path": node.GetPath(),
},
),
})
}
}
+4 -14
View File
@@ -1,8 +1,6 @@
package controllers
import (
"strings"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
"github.com/jesseduffield/lazygit/pkg/gui/types"
@@ -39,12 +37,10 @@ func (self *GlobalController) customCommand() error {
Title: self.c.Tr.CustomCommand,
FindSuggestionsFunc: self.GetCustomCommandsHistorySuggestionsFunc(),
HandleConfirm: func(command string) error {
if self.shouldSaveCommand(command) {
self.c.GetAppState().CustomCommandsHistory = utils.Limit(
lo.Uniq(append(self.c.GetAppState().CustomCommandsHistory, command)),
1000,
)
}
self.c.GetAppState().CustomCommandsHistory = utils.Limit(
lo.Uniq(append(self.c.GetAppState().CustomCommandsHistory, command)),
1000,
)
err := self.c.SaveAppState()
if err != nil {
@@ -59,12 +55,6 @@ func (self *GlobalController) customCommand() error {
})
}
// this mimics the shell functionality `ignorespace`
// which doesn't save a command to history if it starts with a space
func (self *GlobalController) shouldSaveCommand(command string) bool {
return !strings.HasPrefix(command, " ")
}
func (self *GlobalController) GetCustomCommandsHistorySuggestionsFunc() func(string) []*types.Suggestion {
// reversing so that we display the latest command first
history := slices.Reverse(self.c.GetAppState().CustomCommandsHistory)
@@ -1,36 +0,0 @@
package helpers
import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type AmendHelper struct {
c *types.HelperCommon
git *commands.GitCommand
gpg *GpgHelper
}
func NewAmendHelper(
c *types.HelperCommon,
git *commands.GitCommand,
gpg *GpgHelper,
) *AmendHelper {
return &AmendHelper{
c: c,
git: git,
gpg: gpg,
}
}
func (self *AmendHelper) AmendHead() error {
return self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.AmendLastCommitTitle,
Prompt: self.c.Tr.SureToAmend,
HandleConfirm: func() error {
cmdObj := self.git.Commit.AmendHeadCmdObj()
self.c.LogAction(self.c.Tr.Actions.AmendCommit)
return self.gpg.WithGpgHandling(cmdObj, self.c.Tr.AmendingStatus, nil)
},
})
}
@@ -1,166 +0,0 @@
package helpers
import (
"strings"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type ICommitsHelper interface {
UpdateCommitPanelView(message string)
}
type CommitsHelper struct {
c *types.HelperCommon
model *types.Model
contexts *context.ContextTree
getCommitSummary func() string
setCommitSummary func(string)
getCommitDescription func() string
setCommitDescription func(string)
renderCommitLength func()
}
var _ ICommitsHelper = &CommitsHelper{}
func NewCommitsHelper(
c *types.HelperCommon,
model *types.Model,
contexts *context.ContextTree,
getCommitSummary func() string,
setCommitSummary func(string),
getCommitDescription func() string,
setCommitDescription func(string),
renderCommitLength func(),
) *CommitsHelper {
return &CommitsHelper{
c: c,
model: model,
contexts: contexts,
getCommitSummary: getCommitSummary,
setCommitSummary: setCommitSummary,
getCommitDescription: getCommitDescription,
setCommitDescription: setCommitDescription,
renderCommitLength: renderCommitLength,
}
}
func (self *CommitsHelper) SplitCommitMessageAndDescription(message string) (string, string) {
for _, separator := range []string{"\n\n", "\n\r\n\r", "\n", "\n\r"} {
msg, description, found := strings.Cut(message, separator)
if found {
return msg, description
}
}
return message, ""
}
func (self *CommitsHelper) SetMessageAndDescriptionInView(message string) {
summary, description := self.SplitCommitMessageAndDescription(message)
self.setCommitSummary(summary)
self.setCommitDescription(description)
self.renderCommitLength()
}
func (self *CommitsHelper) JoinCommitMessageAndDescription() string {
if len(self.getCommitDescription()) == 0 {
return self.getCommitSummary()
}
return self.getCommitSummary() + "\n" + self.getCommitDescription()
}
func (self *CommitsHelper) UpdateCommitPanelView(message string) {
// first try the passed in message, if not fallback to context -> view in that order
if message != "" {
self.SetMessageAndDescriptionInView(message)
return
}
message = self.contexts.CommitMessage.GetPreservedMessage()
if message != "" {
self.SetMessageAndDescriptionInView(message)
} else {
self.SetMessageAndDescriptionInView(self.getCommitSummary())
}
}
type OpenCommitMessagePanelOpts struct {
CommitIndex int
Title string
PreserveMessage bool
OnConfirm func(string) error
InitialMessage string
}
func (self *CommitsHelper) OpenCommitMessagePanel(opts *OpenCommitMessagePanelOpts) error {
self.contexts.CommitMessage.SetPanelState(
opts.CommitIndex,
opts.Title,
opts.PreserveMessage,
opts.OnConfirm,
)
self.UpdateCommitPanelView(opts.InitialMessage)
return self.pushCommitMessageContexts()
}
func (self *CommitsHelper) OnCommitSuccess() {
// if we have a preserved message we want to clear it on success
if self.contexts.CommitMessage.GetPreserveMessage() {
self.contexts.CommitMessage.SetPreservedMessage("")
}
self.SetMessageAndDescriptionInView("")
}
func (self *CommitsHelper) HandleCommitConfirm() error {
fullMessage := self.JoinCommitMessageAndDescription()
if fullMessage == "" {
return self.c.ErrorMsg(self.c.Tr.CommitWithoutMessageErr)
}
err := self.contexts.CommitMessage.OnConfirm(fullMessage)
if err != nil {
return err
}
return nil
}
func (self *CommitsHelper) CloseCommitMessagePanel() error {
if self.contexts.CommitMessage.GetPreserveMessage() {
message := self.JoinCommitMessageAndDescription()
self.contexts.CommitMessage.SetPreservedMessage(message)
} else {
self.SetMessageAndDescriptionInView("")
}
self.contexts.CommitMessage.SetHistoryMessage("")
return self.PopCommitMessageContexts()
}
func (self *CommitsHelper) PopCommitMessageContexts() error {
return self.c.RemoveContexts(self.commitMessageContexts())
}
func (self *CommitsHelper) pushCommitMessageContexts() error {
for _, context := range self.commitMessageContexts() {
if err := self.c.PushContext(context); err != nil {
return err
}
}
return nil
}
func (self *CommitsHelper) commitMessageContexts() []types.Context {
return []types.Context{
self.contexts.CommitDescription,
self.contexts.CommitMessage,
}
}
+28 -22
View File
@@ -10,6 +10,7 @@ type IFilesHelper interface {
EditFile(filename string) error
EditFileAtLine(filename string, lineNumber int) error
OpenFile(filename string) error
OpenFileAtLine(filename string, lineNumber int) error
}
type FilesHelper struct {
@@ -33,37 +34,42 @@ func NewFilesHelper(
var _ IFilesHelper = &FilesHelper{}
func (self *FilesHelper) EditFile(filename string) error {
cmdStr, editInTerminal := self.git.File.GetEditCmdStr(filename)
return self.callEditor(cmdStr, editInTerminal)
editor, err := self.git.File.GetEditor()
if err != nil {
return self.c.Error(err)
}
editCmd := ""
switch editor {
case "code":
editCmd = editor + " -r --goto -- " + filename
default:
editCmd = editor + " -- " + filename
}
self.c.LogAction(self.c.Tr.Actions.EditFile)
return self.c.RunSubprocessAndRefresh(
self.os.Cmd.NewShell(editCmd),
)
}
func (self *FilesHelper) EditFileAtLine(filename string, lineNumber int) error {
cmdStr, editInTerminal := self.git.File.GetEditAtLineCmdStr(filename, lineNumber)
return self.callEditor(cmdStr, editInTerminal)
}
func (self *FilesHelper) EditFileAtLineAndWait(filename string, lineNumber int) error {
cmdStr := self.git.File.GetEditAtLineAndWaitCmdStr(filename, lineNumber)
// Always suspend, regardless of the value of the editInTerminal config,
// since we want to prevent interacting with the UI until the editor
// returns, even if the editor doesn't use the terminal
return self.callEditor(cmdStr, true)
}
func (self *FilesHelper) callEditor(cmdStr string, editInTerminal bool) error {
if editInTerminal {
return self.c.RunSubprocessAndRefresh(
self.os.Cmd.NewShell(cmdStr),
)
cmdStr, err := self.git.File.GetEditCmdStr(filename, lineNumber)
if err != nil {
return self.c.Error(err)
}
return self.os.Cmd.NewShell(cmdStr).Run()
self.c.LogAction(self.c.Tr.Actions.EditFile)
return self.c.RunSubprocessAndRefresh(
self.os.Cmd.NewShell(cmdStr),
)
}
func (self *FilesHelper) OpenFile(filename string) error {
return self.OpenFileAtLine(filename, 1)
}
func (self *FilesHelper) OpenFileAtLine(filename string, lineNumber int) error {
self.c.LogAction(self.c.Tr.Actions.OpenFile)
if err := self.os.OpenFile(filename); err != nil {
if err := self.os.OpenFileAtLine(filename, lineNumber); err != nil {
return self.c.Error(err)
}
return nil
-4
View File
@@ -14,8 +14,6 @@ type Helpers struct {
PatchBuilding *PatchBuildingHelper
GPG *GpgHelper
Upstream *UpstreamHelper
AmendHelper *AmendHelper
Commits *CommitsHelper
}
func NewStubHelpers() *Helpers {
@@ -33,7 +31,5 @@ func NewStubHelpers() *Helpers {
PatchBuilding: &PatchBuildingHelper{},
GPG: &GpgHelper{},
Upstream: &UpstreamHelper{},
AmendHelper: &AmendHelper{},
Commits: &CommitsHelper{},
}
}
@@ -119,7 +119,6 @@ var conflictStrings = []string{
"When you have resolved this problem",
"fix conflicts",
"Resolve all conflicts manually",
"Merge conflict in file",
}
func isMergeConflictErr(errStr string) bool {
@@ -201,42 +200,22 @@ func (self *MergeAndRebaseHelper) RebaseOntoRef(ref string) error {
if ref == checkedOutBranch {
return self.c.ErrorMsg(self.c.Tr.CantRebaseOntoSelf)
}
menuItems := []*types.MenuItem{
{
Label: self.c.Tr.SimpleRebase,
Key: 's',
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.RebaseBranch)
err := self.git.Rebase.RebaseBranch(ref)
return self.CheckMergeOrRebase(err)
},
},
{
Label: self.c.Tr.InteractiveRebase,
Key: 'i',
Tooltip: self.c.Tr.InteractiveRebaseTooltip,
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.RebaseBranch)
err := self.git.Rebase.EditRebase(ref)
if err = self.CheckMergeOrRebase(err); err != nil {
return err
}
return self.c.PushContext(self.contexts.LocalCommits)
},
},
}
title := utils.ResolvePlaceholderString(
self.c.Tr.RebasingTitle,
prompt := utils.ResolvePlaceholderString(
self.c.Tr.ConfirmRebase,
map[string]string{
"checkedOutBranch": checkedOutBranch,
"ref": ref,
"selectedBranch": ref,
},
)
return self.c.Menu(types.CreateMenuOptions{
Title: title,
Items: menuItems,
return self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.RebasingTitle,
Prompt: prompt,
HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.RebaseBranch)
err := self.git.Rebase.RebaseBranch(ref)
return self.CheckMergeOrRebase(err)
},
})
}
@@ -38,12 +38,12 @@ func (self *PatchBuildingHelper) ValidateNormalWorkingTreeState() (bool, error)
// takes us from the patch building panel back to the commit files panel
func (self *PatchBuildingHelper) Escape() error {
return self.c.PopContext()
return self.c.PushContext(self.contexts.CommitFiles)
}
// kills the custom patch and returns us back to the commit files panel if needed
func (self *PatchBuildingHelper) Reset() error {
self.git.Patch.PatchBuilder.Reset()
self.git.Patch.PatchManager.Reset()
if self.c.CurrentStaticContext().GetKind() != types.SIDE_CONTEXT {
if err := self.Escape(); err != nil {
+7 -7
View File
@@ -21,20 +21,20 @@ func NewTagsHelper(c *types.HelperCommon, git *commands.GitCommand) *TagsHelper
}
}
func (self *TagsHelper) CreateTagMenu(ref string, onCreate func()) error {
func (self *TagsHelper) CreateTagMenu(commitSha string, onCreate func()) error {
return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.TagMenuTitle,
Items: []*types.MenuItem{
{
Label: self.c.Tr.LcLightweightTag,
OnPress: func() error {
return self.handleCreateLightweightTag(ref, onCreate)
return self.handleCreateLightweightTag(commitSha, onCreate)
},
},
{
Label: self.c.Tr.LcAnnotatedTag,
OnPress: func() error {
return self.handleCreateAnnotatedTag(ref, onCreate)
return self.handleCreateAnnotatedTag(commitSha, onCreate)
},
},
},
@@ -48,7 +48,7 @@ func (self *TagsHelper) afterTagCreate(onCreate func()) error {
})
}
func (self *TagsHelper) handleCreateAnnotatedTag(ref string, onCreate func()) error {
func (self *TagsHelper) handleCreateAnnotatedTag(commitSha string, onCreate func()) error {
return self.c.Prompt(types.PromptOpts{
Title: self.c.Tr.TagNameTitle,
HandleConfirm: func(tagName string) error {
@@ -56,7 +56,7 @@ func (self *TagsHelper) handleCreateAnnotatedTag(ref string, onCreate func()) er
Title: self.c.Tr.TagMessageTitle,
HandleConfirm: func(msg string) error {
self.c.LogAction(self.c.Tr.Actions.CreateAnnotatedTag)
if err := self.git.Tag.CreateAnnotated(tagName, ref, msg); err != nil {
if err := self.git.Tag.CreateAnnotated(tagName, commitSha, msg); err != nil {
return self.c.Error(err)
}
return self.afterTagCreate(onCreate)
@@ -66,12 +66,12 @@ func (self *TagsHelper) handleCreateAnnotatedTag(ref string, onCreate func()) er
})
}
func (self *TagsHelper) handleCreateLightweightTag(ref string, onCreate func()) error {
func (self *TagsHelper) handleCreateLightweightTag(commitSha string, onCreate func()) error {
return self.c.Prompt(types.PromptOpts{
Title: self.c.Tr.TagNameTitle,
HandleConfirm: func(tagName string) error {
self.c.LogAction(self.c.Tr.Actions.CreateLightweightTag)
if err := self.git.Tag.CreateLightweight(tagName, ref); err != nil {
if err := self.git.Tag.CreateLightweight(tagName, commitSha); err != nil {
return self.c.Error(err)
}
return self.afterTagCreate(onCreate)
@@ -20,14 +20,13 @@ type IWorkingTreeHelper interface {
}
type WorkingTreeHelper struct {
c *types.HelperCommon
git *commands.GitCommand
contexts *context.ContextTree
refHelper *RefsHelper
model *types.Model
setCommitMessage func(message string)
commitsHelper *CommitsHelper
gpgHelper *GpgHelper
c *types.HelperCommon
git *commands.GitCommand
contexts *context.ContextTree
refHelper *RefsHelper
model *types.Model
setCommitMessage func(message string)
getSavedCommitMessage func() string
}
func NewWorkingTreeHelper(
@@ -37,18 +36,16 @@ func NewWorkingTreeHelper(
refHelper *RefsHelper,
model *types.Model,
setCommitMessage func(message string),
commitsHelper *CommitsHelper,
gpgHelper *GpgHelper,
getSavedCommitMessage func() string,
) *WorkingTreeHelper {
return &WorkingTreeHelper{
c: c,
git: git,
contexts: contexts,
refHelper: refHelper,
model: model,
setCommitMessage: setCommitMessage,
commitsHelper: commitsHelper,
gpgHelper: gpgHelper,
c: c,
git: git,
contexts: contexts,
refHelper: refHelper,
model: model,
setCommitMessage: setCommitMessage,
getSavedCommitMessage: getSavedCommitMessage,
}
}
@@ -97,7 +94,7 @@ func (self *WorkingTreeHelper) OpenMergeTool() error {
})
}
func (self *WorkingTreeHelper) HandleCommitPressWithMessage(initialMessage string) error {
func (self *WorkingTreeHelper) HandleCommitPress() error {
if err := self.prepareFilesForCommit(); err != nil {
return self.c.Error(err)
}
@@ -110,25 +107,28 @@ func (self *WorkingTreeHelper) HandleCommitPressWithMessage(initialMessage strin
return self.PromptToStageAllAndRetry(self.HandleCommitPress)
}
return self.commitsHelper.OpenCommitMessagePanel(
&OpenCommitMessagePanelOpts{
CommitIndex: context.NoCommitIndex,
InitialMessage: initialMessage,
Title: self.c.Tr.CommitSummary,
PreserveMessage: true,
OnConfirm: self.handleCommit,
},
)
}
savedCommitMessage := self.getSavedCommitMessage()
if len(savedCommitMessage) > 0 {
self.setCommitMessage(savedCommitMessage)
} else {
commitPrefixConfig := self.commitPrefixConfigForRepo()
if commitPrefixConfig != nil {
prefixPattern := commitPrefixConfig.Pattern
prefixReplace := commitPrefixConfig.Replace
rgx, err := regexp.Compile(prefixPattern)
if err != nil {
return self.c.ErrorMsg(fmt.Sprintf("%s: %s", self.c.Tr.LcCommitPrefixPatternError, err.Error()))
}
prefix := rgx.ReplaceAllString(self.refHelper.GetCheckedOutRef().Name, prefixReplace)
self.setCommitMessage(prefix)
}
}
func (self *WorkingTreeHelper) handleCommit(message string) error {
cmdObj := self.git.Commit.CommitCmdObj(message)
self.c.LogAction(self.c.Tr.Actions.Commit)
_ = self.commitsHelper.PopCommitMessageContexts()
return self.gpgHelper.WithGpgHandling(cmdObj, self.c.Tr.CommittingStatus, func() error {
self.commitsHelper.OnCommitSuccess()
return nil
})
if err := self.c.PushContext(self.contexts.CommitMessage); err != nil {
return err
}
return nil
}
// HandleCommitEditorPress - handle when the user wants to commit changes via
@@ -154,27 +154,9 @@ func (self *WorkingTreeHelper) HandleWIPCommitPress() error {
return self.c.ErrorMsg(self.c.Tr.SkipHookPrefixNotConfigured)
}
return self.HandleCommitPressWithMessage(skipHookPrefix)
}
self.setCommitMessage(skipHookPrefix)
func (self *WorkingTreeHelper) HandleCommitPress() error {
message := self.contexts.CommitMessage.GetPreservedMessage()
if message != "" {
commitPrefixConfig := self.commitPrefixConfigForRepo()
if commitPrefixConfig != nil {
prefixPattern := commitPrefixConfig.Pattern
prefixReplace := commitPrefixConfig.Replace
rgx, err := regexp.Compile(prefixPattern)
if err != nil {
return self.c.ErrorMsg(fmt.Sprintf("%s: %s", self.c.Tr.LcCommitPrefixPatternError, err.Error()))
}
prefix := rgx.ReplaceAllString(self.refHelper.GetCheckedOutRef().Name, prefixReplace)
message = prefix
}
}
return self.HandleCommitPressWithMessage(message)
return self.HandleCommitPress()
}
func (self *WorkingTreeHelper) PromptToStageAllAndRetry(retry func() error) error {
+47 -113
View File
@@ -3,14 +3,10 @@ package controllers
import (
"fmt"
"github.com/fsmiamoto/git-todo-parser/todo"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
type (
@@ -20,6 +16,7 @@ type (
type LocalCommitsController struct {
baseController
*controllerCommon
pullFiles PullFilesFn
}
@@ -135,7 +132,7 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
Description: self.c.Tr.LcRevertCommit,
},
{
Key: opts.GetKey(opts.Config.Commits.CreateTag),
Key: opts.GetKey(opts.Config.Commits.TagCommit),
Handler: self.checkSelected(self.createTag),
Description: self.c.Tr.LcTagCommit,
},
@@ -151,11 +148,11 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
}
func (self *LocalCommitsController) squashDown(commit *models.Commit) error {
if self.context().GetSelectedLineIdx() >= len(self.model.Commits)-1 {
return self.c.ErrorMsg(self.c.Tr.CannotSquashOrFixupFirstCommit)
if len(self.model.Commits) <= 1 {
return self.c.ErrorMsg(self.c.Tr.YouNoCommitsToSquash)
}
applied, err := self.handleMidRebaseCommand(todo.Squash, commit)
applied, err := self.handleMidRebaseCommand("squash", commit)
if err != nil {
return err
}
@@ -169,18 +166,18 @@ func (self *LocalCommitsController) squashDown(commit *models.Commit) error {
HandleConfirm: func() error {
return self.c.WithWaitingStatus(self.c.Tr.SquashingStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.SquashCommitDown)
return self.interactiveRebase(todo.Squash)
return self.interactiveRebase("squash")
})
},
})
}
func (self *LocalCommitsController) fixup(commit *models.Commit) error {
if self.context().GetSelectedLineIdx() >= len(self.model.Commits)-1 {
return self.c.ErrorMsg(self.c.Tr.CannotSquashOrFixupFirstCommit)
if len(self.model.Commits) <= 1 {
return self.c.ErrorMsg(self.c.Tr.YouNoCommitsToSquash)
}
applied, err := self.handleMidRebaseCommand(todo.Fixup, commit)
applied, err := self.handleMidRebaseCommand("fixup", commit)
if err != nil {
return err
}
@@ -194,14 +191,14 @@ func (self *LocalCommitsController) fixup(commit *models.Commit) error {
HandleConfirm: func() error {
return self.c.WithWaitingStatus(self.c.Tr.FixingStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.FixupCommit)
return self.interactiveRebase(todo.Fixup)
return self.interactiveRebase("fixup")
})
},
})
}
func (self *LocalCommitsController) reword(commit *models.Commit) error {
applied, err := self.handleMidRebaseCommand(todo.Reword, commit)
applied, err := self.handleMidRebaseCommand("reword", commit)
if err != nil {
return err
}
@@ -209,36 +206,30 @@ func (self *LocalCommitsController) reword(commit *models.Commit) error {
return nil
}
commitMessage, err := self.git.Commit.GetCommitMessage(commit.Sha)
message, err := self.git.Commit.GetCommitMessage(commit.Sha)
if err != nil {
return self.c.Error(err)
}
return self.helpers.Commits.OpenCommitMessagePanel(
&helpers.OpenCommitMessagePanelOpts{
CommitIndex: self.context().GetSelectedLineIdx(),
InitialMessage: commitMessage,
Title: self.c.Tr.Actions.RewordCommit,
PreserveMessage: false,
OnConfirm: self.handleReword,
// TODO: use the commit message panel here
return self.c.Prompt(types.PromptOpts{
Title: self.c.Tr.LcRewordCommit,
InitialContent: message,
HandleConfirm: func(response string) error {
self.c.LogAction(self.c.Tr.Actions.RewordCommit)
if err := self.git.Rebase.RewordCommit(self.model.Commits, self.context().GetSelectedLineIdx(), response); err != nil {
return self.c.Error(err)
}
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
},
)
}
func (self *LocalCommitsController) handleReword(message string) error {
err := self.git.Rebase.RewordCommit(self.model.Commits, self.contexts.LocalCommits.GetSelectedLineIdx(), message)
if err != nil {
return self.c.Error(err)
}
self.helpers.Commits.OnCommitSuccess()
_ = self.helpers.Commits.PopCommitMessageContexts()
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
})
}
func (self *LocalCommitsController) doRewordEditor() error {
self.c.LogAction(self.c.Tr.Actions.RewordCommit)
if self.isHeadCommit() {
if self.context().GetSelectedLineIdx() == 0 {
return self.c.RunSubprocessAndRefresh(self.os.Cmd.New("git commit --allow-empty --amend --only"))
}
@@ -256,7 +247,7 @@ func (self *LocalCommitsController) doRewordEditor() error {
}
func (self *LocalCommitsController) rewordEditor(commit *models.Commit) error {
midRebase, err := self.handleMidRebaseCommand(todo.Reword, commit)
midRebase, err := self.handleMidRebaseCommand("reword", commit)
if err != nil {
return err
}
@@ -276,7 +267,7 @@ func (self *LocalCommitsController) rewordEditor(commit *models.Commit) error {
}
func (self *LocalCommitsController) drop(commit *models.Commit) error {
applied, err := self.handleMidRebaseCommand(todo.Drop, commit)
applied, err := self.handleMidRebaseCommand("drop", commit)
if err != nil {
return err
}
@@ -290,14 +281,14 @@ func (self *LocalCommitsController) drop(commit *models.Commit) error {
HandleConfirm: func() error {
return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.DropCommit)
return self.interactiveRebase(todo.Drop)
return self.interactiveRebase("drop")
})
},
})
}
func (self *LocalCommitsController) edit(commit *models.Commit) error {
applied, err := self.handleMidRebaseCommand(todo.Edit, commit)
applied, err := self.handleMidRebaseCommand("edit", commit)
if err != nil {
return err
}
@@ -307,13 +298,12 @@ func (self *LocalCommitsController) edit(commit *models.Commit) error {
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.EditCommit)
err := self.git.Rebase.EditRebase(commit.Sha)
return self.helpers.MergeAndRebase.CheckMergeOrRebase(err)
return self.interactiveRebase("edit")
})
}
func (self *LocalCommitsController) pick(commit *models.Commit) error {
applied, err := self.handleMidRebaseCommand(todo.Pick, commit)
applied, err := self.handleMidRebaseCommand("pick", commit)
if err != nil {
return err
}
@@ -326,7 +316,7 @@ func (self *LocalCommitsController) pick(commit *models.Commit) error {
return self.pullFiles()
}
func (self *LocalCommitsController) interactiveRebase(action todo.TodoCommand) error {
func (self *LocalCommitsController) interactiveRebase(action string) error {
err := self.git.Rebase.InteractiveRebase(self.model.Commits, self.context().GetSelectedLineIdx(), action)
return self.helpers.MergeAndRebase.CheckMergeOrRebase(err)
}
@@ -334,16 +324,8 @@ func (self *LocalCommitsController) interactiveRebase(action todo.TodoCommand) e
// handleMidRebaseCommand sees if the selected commit is in fact a rebasing
// commit meaning you are trying to edit the todo file rather than actually
// begin a rebase. It then updates the todo file with that action
func (self *LocalCommitsController) handleMidRebaseCommand(action todo.TodoCommand, commit *models.Commit) (bool, error) {
if !commit.IsTODO() {
if self.git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE {
// If we are in a rebase, the only action that is allowed for
// non-todo commits is rewording the current head commit
if !(action == todo.Reword && self.isHeadCommit()) {
return true, self.c.ErrorMsg(self.c.Tr.AlreadyRebasing)
}
}
func (self *LocalCommitsController) handleMidRebaseCommand(action string, commit *models.Commit) (bool, error) {
if commit.Status != "rebasing" {
return false, nil
}
@@ -351,21 +333,19 @@ func (self *LocalCommitsController) handleMidRebaseCommand(action todo.TodoComma
// and that means we either unconditionally wait around for the subprocess to ask for
// our input or we set a lazygit client as the EDITOR env variable and have it
// request us to edit the commit message when prompted.
if action == todo.Reword {
if action == "reword" {
return true, self.c.ErrorMsg(self.c.Tr.LcRewordNotSupported)
}
if allowed := isChangeOfRebaseTodoAllowed(action); !allowed {
return true, self.c.ErrorMsg(self.c.Tr.LcChangingThisActionIsNotAllowed)
}
self.c.LogAction("Update rebase TODO")
self.c.LogCommand(
fmt.Sprintf("Updating rebase action of commit %s to '%s'", commit.ShortSha(), action.String()),
fmt.Sprintf("Updating rebase action of commit %s to '%s'", commit.ShortSha(), action),
false,
)
if err := self.git.Rebase.EditRebaseTodo(commit, action); err != nil {
if err := self.git.Rebase.EditRebaseTodo(
self.context().GetSelectedLineIdx(), action,
); err != nil {
return false, self.c.Error(err)
}
@@ -377,14 +357,8 @@ func (self *LocalCommitsController) handleMidRebaseCommand(action todo.TodoComma
func (self *LocalCommitsController) moveDown(commit *models.Commit) error {
index := self.context().GetSelectedLineIdx()
commits := self.model.Commits
// can't move past the initial commit
if index >= len(commits)-1 {
return nil
}
if commit.IsTODO() {
if !commits[index+1].IsTODO() {
if commit.Status == "rebasing" {
if commits[index+1].Status != "rebasing" {
return nil
}
@@ -393,7 +367,7 @@ func (self *LocalCommitsController) moveDown(commit *models.Commit) error {
self.c.LogAction(self.c.Tr.Actions.MoveCommitDown)
self.c.LogCommand(fmt.Sprintf("Moving commit %s down", commit.ShortSha()), false)
if err := self.git.Rebase.MoveTodoDown(commit); err != nil {
if err := self.git.Rebase.MoveTodoDown(index); err != nil {
return self.c.Error(err)
}
self.context().MoveSelectedLine(1)
@@ -402,10 +376,6 @@ func (self *LocalCommitsController) moveDown(commit *models.Commit) error {
})
}
if self.git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE {
return self.c.ErrorMsg(self.c.Tr.AlreadyRebasing)
}
return self.c.WithWaitingStatus(self.c.Tr.MovingStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.MoveCommitDown)
err := self.git.Rebase.MoveCommitDown(self.model.Commits, index)
@@ -422,7 +392,7 @@ func (self *LocalCommitsController) moveUp(commit *models.Commit) error {
return nil
}
if commit.IsTODO() {
if commit.Status == "rebasing" {
// logging directly here because MoveTodoDown doesn't have enough information
// to provide a useful log
self.c.LogAction(self.c.Tr.Actions.MoveCommitUp)
@@ -431,7 +401,7 @@ func (self *LocalCommitsController) moveUp(commit *models.Commit) error {
false,
)
if err := self.git.Rebase.MoveTodoUp(self.model.Commits[index]); err != nil {
if err := self.git.Rebase.MoveTodoDown(index - 1); err != nil {
return self.c.Error(err)
}
self.context().MoveSelectedLine(-1)
@@ -440,13 +410,9 @@ func (self *LocalCommitsController) moveUp(commit *models.Commit) error {
})
}
if self.git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE {
return self.c.ErrorMsg(self.c.Tr.AlreadyRebasing)
}
return self.c.WithWaitingStatus(self.c.Tr.MovingStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.MoveCommitUp)
err := self.git.Rebase.MoveCommitUp(self.model.Commits, index)
err := self.git.Rebase.MoveCommitDown(self.model.Commits, index-1)
if err == nil {
self.context().MoveSelectedLine(-1)
}
@@ -455,24 +421,13 @@ func (self *LocalCommitsController) moveUp(commit *models.Commit) error {
}
func (self *LocalCommitsController) amendTo(commit *models.Commit) error {
if self.isHeadCommit() {
if err := self.helpers.AmendHelper.AmendHead(); err != nil {
return err
}
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
}
if self.git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE {
return self.c.ErrorMsg(self.c.Tr.AlreadyRebasing)
}
return self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.AmendCommitTitle,
Prompt: self.c.Tr.AmendCommitPrompt,
HandleConfirm: func() error {
return self.c.WithWaitingStatus(self.c.Tr.AmendingStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.AmendCommit)
err := self.git.Rebase.AmendTo(self.model.Commits, self.context().GetView().SelectedLineIdx())
err := self.git.Rebase.AmendTo(commit.Sha)
return self.helpers.MergeAndRebase.CheckMergeOrRebase(err)
})
},
@@ -480,10 +435,6 @@ func (self *LocalCommitsController) amendTo(commit *models.Commit) error {
}
func (self *LocalCommitsController) amendAttribute(commit *models.Commit) error {
if self.git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE && !self.isHeadCommit() {
return self.c.ErrorMsg(self.c.Tr.AlreadyRebasing)
}
return self.c.Menu(types.CreateMenuOptions{
Title: "Amend commit attribute",
Items: []*types.MenuItem{
@@ -619,7 +570,7 @@ func (self *LocalCommitsController) squashAllAboveFixupCommits(commit *models.Co
HandleConfirm: func() error {
return self.c.WithWaitingStatus(self.c.Tr.SquashingStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.SquashAllAboveFixupCommits)
err := self.git.Rebase.SquashAllAboveFixupCommits(commit)
err := self.git.Rebase.SquashAllAboveFixupCommits(commit.Sha)
return self.helpers.MergeAndRebase.CheckMergeOrRebase(err)
})
},
@@ -770,20 +721,3 @@ func (self *LocalCommitsController) context() *context.LocalCommitsContext {
func (self *LocalCommitsController) paste() error {
return self.helpers.CherryPick.Paste()
}
func (self *LocalCommitsController) isHeadCommit() bool {
return models.IsHeadCommit(self.model.Commits, self.context().GetSelectedLineIdx())
}
func isChangeOfRebaseTodoAllowed(action todo.TodoCommand) bool {
allowedActions := []todo.TodoCommand{
todo.Pick,
todo.Drop,
todo.Edit,
todo.Fixup,
todo.Squash,
todo.Reword,
}
return lo.Contains(allowedActions, action)
}

Some files were not shown because too many files have changed in this diff Show More