Compare commits

..
Author SHA1 Message Date
Jesse Duffield 79a72fe7a6 refactor 2022-09-22 09:03:11 -07:00
Yuki Osaki ac8e76bee0 Merge branch 'master' into gh-integration 2022-09-07 11:49:45 +09:00
Yuki Osaki c06974b879 Merge branch 'master' into gh-integration 2022-08-22 11:50:50 +09:00
Yuki Osaki b5578b3816 fix reload issue 2022-08-19 13:11:43 +09:00
Yuki Osaki 93bbea1221 fix layout 2022-08-19 12:18:35 +09:00
Yuki Osaki f277ea2350 fix conflict 2022-08-16 22:07:42 +09:00
Yuki Osaki 4a31f3fd50 Merge branch 'master' into gh-integration 2022-08-14 20:48:42 +09:00
Yuki Osaki c3d2722cb8 fix conflicts 2022-02-25 11:50:59 +09:00
Yuki Osaki ed4e7ac960 Merge branch 'master' into gh-integration 2022-02-25 09:01:03 +09:00
Yuki Osaki bf355bc0b5 fix based on the reviews 2021-11-10 00:19:39 +09:00
Yuki OsakiandJesse Duffield 27a12fe2ea Update pkg/commands/github.go
Co-authored-by: Jesse Duffield <jessedduffield@gmail.com>
2021-11-09 21:21:59 +09:00
Yuki Osaki 37aa558d34 Merge branch 'master' into gh-integration 2021-11-06 12:49:36 +09:00
Yuki Osaki 588bd95118 handle when repo is not set 2021-11-03 00:14:25 +09:00
Yuki Osaki 2b8c959cb0 add Gh version check 2021-11-02 22:04:22 +09:00
Yuki Osaki 4fead59416 Merge branch 'master' into gh-integration 2021-11-02 21:11:35 +09:00
Yuki Osaki 2d1c29fa76 Add enableGhCommand flag 2021-11-01 00:00:39 +09:00
Yuki Osaki 66f3978ab5 use surfaceError 2021-10-31 23:48:52 +09:00
Yuki Osaki c8de14aa9d generate github state struct 2021-10-31 23:39:01 +09:00
Yuki Osaki d2e75a6359 remove BranchesWithGithubPullRequests check 2021-10-31 23:31:05 +09:00
Yuki Osaki b36f6db521 refactor logic 2021-10-31 23:16:04 +09:00
Yuki Osaki 769a6cd069 add local 2021-10-31 23:10:05 +09:00
Yuki Osaki 1547b4155e remove refreshStatus 2021-10-31 22:59:32 +09:00
Yuki Osaki 153970fb08 split logic into functions 2021-10-31 22:55:11 +09:00
Yuki Osaki aed7c29509 move pr color logic to own method 2021-10-31 22:40:39 +09:00
Yuki Osaki 4a1fa4889d Merge branch 'master' into gh-integration 2021-10-31 22:29:37 +09:00
Yuki Osaki 515e2d2fa9 refactor the code 2021-10-31 22:28:03 +09:00
Yuki Osaki c1f3fd6b3f Update GithubMostRecentPRs method 2021-10-31 15:40:18 +09:00
Yuki Osaki 9d774c2450 Use c.GetRemotes() 2021-10-31 14:34:27 +09:00
Yuki Osaki 04edfc2c63 remove unnecessary check 2021-10-31 14:03:45 +09:00
Yuki Osaki 1bf9d2e30c Merge branch 'master' into gh-integration 2021-10-31 13:59:22 +09:00
Yuki Osaki 502c23dff7 update the logic 2021-10-31 13:58:07 +09:00
Yuki Osaki 61d08dc4af better error handle 2021-10-30 15:58:37 +09:00
Yuki Osaki a71db3355a update it not to inject the PRs into our branches 2021-10-30 00:37:38 +09:00
Yuki Osaki 7fbf4fa7f7 say 'create / open' instead of 'create / show' 2021-10-29 21:09:31 +09:00
Yuki Osaki 3ed1611d94 Move it to guiState 2021-10-29 21:03:04 +09:00
Yuki Osaki ada3b528d3 fix conflict 2021-10-29 20:29:41 +09:00
Yuki Osaki 3df068b1fc Merge branch 'master' into gh-integration 2021-10-29 19:19:43 +09:00
mjarkk c295deaa81 show github pr number in branches list 2021-07-29 12:14:22 +02:00
5143 changed files with 28878 additions and 49729 deletions
-13
View File
@@ -1,13 +0,0 @@
# adapted from https://github.com/devcontainers/images/blob/main/src/go/.devcontainer/Dockerfile
# [Choice] Go version (use -bullseye variants on local arm64/Apple Silicon): 1, 1.19, 1.18, 1-bullseye, 1.19-bullseye, 1.18-bullseye, 1-buster, 1.19-buster, 1.18-buster
ARG VARIANT=1-bullseye
FROM golang:${VARIANT}
RUN go install mvdan.cc/gofumpt@latest
RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.50.0
RUN golangci-lint --version
# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>
-69
View File
@@ -1,69 +0,0 @@
// adapted from https://github.com/devcontainers/images/blob/main/src/go/.devcontainer/devcontainer.json
{
"build": {
"dockerfile": "./Dockerfile",
"context": "."
},
"features": {
"ghcr.io/devcontainers/features/common-utils:1": {
"installZsh": "true",
"username": "vscode",
"uid": "1000",
"gid": "1000",
"upgradePackages": "true"
},
"ghcr.io/devcontainers/features/go:1": {
"version": "none"
},
"ghcr.io/devcontainers/features/git:1": {
"version": "latest",
"ppa": "false"
}
},
"overrideFeatureInstallOrder": [
"ghcr.io/devcontainers/features/common-utils"
],
// not sure if we actually need these
"runArgs": [
"--cap-add=SYS_PTRACE",
"--security-opt",
"seccomp=unconfined"
],
// Configure tool-specific properties.
"customizations": {
// Configure properties specific to VS Code.
"vscode": {
// Set *default* container specific settings.json values on container create.
"settings": {
"go.toolsManagement.checkForUpdates": "local",
"go.useLanguageServer": true,
"go.gopath": "/go",
"[go]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true
}
},
"go.lintTool": "golangci-lint",
"gopls": {
"formatting.gofumpt": true,
"usePlaceholders": false // add parameter placeholders when completing a function
},
"files.eol": "\n"
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"golang.Go"
]
}
},
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "go version",
// See https://www.kenmuse.com/blog/avoiding-dubious-ownership-in-dev-containers/ for the safe.directory part
// The defaultBranch part is required for our deprecated integration tests.
"postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder} && git config --global init.defaultBranch master",
// Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "vscode"
}
+30 -9
View File
@@ -46,6 +46,35 @@ 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
run: |
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 +97,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 +163,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:
-81
View File
@@ -1,81 +0,0 @@
name: Build PR Image
on:
pull_request:
types: [opened, synchronize, reopened, closed]
jobs:
build-application:
name: Build and Push `lazygit`
runs-on: ubuntu-latest
if: ${{ github.event_name != 'pull_request' || github.event.action != 'closed' }}
outputs:
tags: ${{ steps.meta.outputs.tags }}
steps:
- name: Checkout git repo
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Generate UUID image name
id: uuid
run: echo "UUID_APP_TAG=$(uuidgen)" >> $GITHUB_ENV
- name: Docker metadata
id: meta
uses: docker/metadata-action@v4
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
with:
push: true
context: ./
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
file: ./uffizzi/DockerfileTtyd
cache-from: type=gha
cache-to: type=gha,mode=max
render-compose-file:
name: Render Docker Compose File
# Pass output of this workflow to another triggered by `workflow_run` event.
runs-on: ubuntu-latest
needs:
- build-application
outputs:
compose-file-cache-key: ${{ steps.hash.outputs.hash }}
steps:
- name: Checkout git repo
uses: actions/checkout@v3
- name: Render Compose File
run: |
APP_IMAGE=$(echo ${{ needs.build-application.outputs.tags }})
export APP_IMAGE
# Render simple template from environment variables.
envsubst < ./uffizzi/docker-compose.uffizzi.yml > docker-compose.rendered.yml
cat docker-compose.rendered.yml
- name: Upload Rendered Compose File as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: docker-compose.rendered.yml
retention-days: 2
- name: Upload PR Event as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: ${{ github.event_path }}
retention-days: 2
delete-preview:
name: Call for Preview Deletion
runs-on: ubuntu-latest
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: Upload PR Event as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: ${{ github.event_path }}
retention-days: 2
-90
View File
@@ -1,90 +0,0 @@
name: Deploy Uffizzi Preview
on:
workflow_run:
workflows:
- "Build PR Image"
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 }}
steps:
- name: 'Download artifacts'
# Fetch output (zip archive) from the workflow run that triggered this workflow.
uses: actions/github-script@v6
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
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,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
let fs = require('fs');
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
- 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
with:
path: docker-compose.rendered.yml
key: ${{ env.COMPOSE_FILE_HASH }}
- name: Read PR Number From Event Object
id: pr
run: echo "PR_NUMBER=${{ fromJSON(env.EVENT_JSON).number }}" >> $GITHUB_ENV
- name: DEBUG - Print Job Outputs
if: ${{ runner.debug }}
run: |
echo "PR number: ${{ env.PR_NUMBER }}"
echo "Compose file hash: ${{ env.COMPOSE_FILE_HASH }}"
cat event.json
deploy-uffizzi-preview:
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
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.
compose-file-cache-key: ${{ needs.cache-compose-file.outputs.compose-file-cache-key }}
compose-file-cache-path: docker-compose.rendered.yml
server: https://app.uffizzi.com
pr-number: ${{ needs.cache-compose-file.outputs.pr-number }}
permissions:
contents: read
pull-requests: write
id-token: write
+12 -2
View File
@@ -27,7 +27,6 @@ lazygit.exe
!.circleci/
!.github/
!.vscode/
!.devcontainer/
# these are for our integration tests
!.git_keep
@@ -35,7 +34,18 @@ 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/**/actual/
test/integration_new/**/used_config/
# these sample hooks waste too much space
test/integration_new/**/expected/**/hooks/
test/integration_new/**/expected_remote/**/hooks/
oryxBuildBinary
__debug_bin
+5 -1
View File
@@ -32,8 +32,12 @@ checksum:
snapshot:
name_template: '{{ .Tag }}-next'
changelog:
use: github-native
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
- '^bump'
brews:
-
# Repository to push the tap to.
+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
-86
View File
@@ -1,86 +0,0 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "Generate cheatsheet",
"type": "process",
"command": "go run scripts/cheatsheet/main.go ",
"problemMatcher": [],
},
{
"label": "Bump gocui",
"type": "shell",
"command": "./scripts/bump_gocui.sh",
"problemMatcher": [],
},
{
"label": "Bump lazycore",
"type": "shell",
"command": "./scripts/bump_lazycore.sh",
"problemMatcher": [],
},
{
"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}",
"problemMatcher": [],
"group": {
"kind": "test",
"isDefault": true
},
"presentation": {
"focus": true
}
},
{
"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}",
"problemMatcher": [],
"group": {
"kind": "test",
},
"presentation": {
"focus": true
}
},
{
"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}",
"problemMatcher": [],
"group": {
"kind": "test",
},
"presentation": {
"focus": true
}
},
{
"label": "Open deprecated test TUI",
"type": "shell",
"command": "go run pkg/integration/deprecated/cmd/tui/main.go",
"problemMatcher": [],
"group": {
"kind": "test",
},
"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 -68
View File
@@ -23,28 +23,6 @@ welcome your pull requests:
If you've never written Go in your life, then join the club! Lazygit was the maintainer's first Go program, and most contributors have never used Go before. Go is widely considered an easy-to-learn language, so if you're looking for an open source project to gain dev experience, you've come to the right place.
## Running in a VSCode dev container
If you want to spare yourself the hassle of setting up your dev environment yourself (i.e. installing Go, extensions, and extra tools), you can run the Lazygit code in a VSCode dev container like so:
![image](https://user-images.githubusercontent.com/8456633/201500508-0d55f99f-5035-4a6f-a0f8-eaea5c003e5d.png)
This requires that:
* you have docker installed
* you have the dev containers extension installed in VSCode
See [here](https://code.visualstudio.com/docs/devcontainers/containers) for more info about dev containers.
## Running in a Github Codespace
If you want to start contributing to Lazygit with the click of a button, you can open the lazygit codebase in a Codespace. First fork the repo, then click to create a codespace:
![image](https://user-images.githubusercontent.com/8456633/201500566-ffe9105d-6030-4cc7-a525-6570b0b413a2.png)
To run lazygit from within the integrated terminal just go `go run main.go`
This allows you to contribute to Lazygit without needing to install anything on your local machine. The Codespace has all the necessary tools and extensions pre-installed.
## Code of conduct
Please note by participating in this project, you agree to abide by the [code of conduct].
@@ -154,34 +132,6 @@ If you want to trigger a debug session from VSCode, you can use the following sn
}
```
## Profiling
If you want to investigate what's contributing to CPU usage you can add the following to the top of the `main()` function in `main.go`
```go
import "runtime/pprof"
func main() {
f, err := os.Create("cpu.prof")
if err != nil {
log.Fatal("could not create CPU profile: ", err)
}
defer f.Close()
if err := pprof.StartCPUProfile(f); err != nil {
log.Fatal("could not start CPU profile: ", err)
}
defer pprof.StopCPUProfile()
...
```
Then run lazygit, and afterwards, from your terminal, run:
```sh
go tool pprof --web cpu.prof
```
That should open an application which allows you to view the breakdown of CPU usage.
## Testing
Lazygit has two kinds of tests: unit tests and integration tests. Unit tests go in files that end in `_test.go`, and are written in Go. For integration tests, see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
@@ -190,7 +140,7 @@ Lazygit has two kinds of tests: unit tests and integration tests. Unit tests go
Sometimes you will need to make a change in the gocui fork (https://github.com/jesseduffield/gocui). Gocui is the package responsible for rendering windows and handling user input. Here's the typical process to follow:
1. Make the changes in gocui inside lazygit's vendor directory so it's easy to test against lazygit
1. Make the changes in gocui inside the vendor directory so it's easy to test against lazygit
2. Copy the changes over to the actual gocui repo (clone it if you haven't already, and use the `awesome` branch, not `master`)
3. Raise a PR on the gocui repo with your changes
4. After that PR is merged, make a PR in lazygit bumping the gocui version. You can bump the version by running the following at the lazygit repo root:
@@ -201,23 +151,6 @@ Sometimes you will need to make a change in the gocui fork (https://github.com/j
5. Raise a PR in lazygit with those changes
## Updating Lazycore
[Lazycore](https://github.com/jesseduffield/lazycore) is a repo containing shared functionality between lazygit and lazydocker. Sometimes you will need to make a change to that repo and import the changes into lazygit. Similar to updating Gocui, here's what you do:
1. Make the changes in lazycore inside lazygit's vendor directory so it's easy to test against lazygit
2. Copy the changes over to the actual lazycore repo (clone it if you haven't already, and use the `master` branch)
3. Raise a PR on the lazycore repo with your changes
4. After that PR is merged, make a PR in lazygit bumping the lazycore version. You can bump the version by running the following at the lazygit repo root:
```sh
./scripts/bump_lazycore.sh
```
Or if you're using VSCode, there is a bump lazycore task you can find by going `cmd+shift+p` and typing 'Run task'
5. Raise a PR in lazygit with those changes
## Improvements
If you can think of any way to improve these docs let us know.
+11 -6
View File
File diff suppressed because one or more lines are too long
+10 -12
View File
@@ -13,10 +13,15 @@ Usage:
See https://github.com/jesseduffield/lazygit/tree/master/pkg/integration/README.md
CLI mode:
> go run cmd/integration_test/main.go cli [--slow] [--sandbox] <test1> <test2> ...
> go run cmd/integration_test/main.go cli [--slow] <test1> <test2> ...
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
@@ -37,19 +42,12 @@ func main() {
case "cli":
testNames := os.Args[2:]
slow := false
sandbox := false
// get the next arg if it's --slow
if len(os.Args) > 2 {
if os.Args[2] == "--slow" || os.Args[2] == "-slow" {
testNames = os.Args[3:]
slow = true
} else if os.Args[2] == "--sandbox" || os.Args[2] == "-sandbox" {
testNames = os.Args[3:]
sandbox = true
}
if len(os.Args) > 2 && (os.Args[2] == "--slow" || os.Args[2] == "-slow") {
testNames = os.Args[3:]
slow = true
}
clients.RunCLI(testNames, slow, sandbox)
clients.RunCLI(testNames, slow)
case "tui":
clients.RunTUI()
default:
+62 -58
View File
@@ -9,19 +9,14 @@ 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:
- MacOS: `export XDG_CONFIG_HOME="$HOME/.config"`
## Default
```yaml
gui:
# stuff relating to the UI
windowSize: 'normal' # one of 'normal' | 'half' | 'full' default is 'normal'
scrollHeight: 2 # how many lines you scroll by
scrollPastBottom: true # enable scrolling past the bottom
sidePanelWidth: 0.3333 # number from 0 to 1
@@ -30,6 +25,7 @@ gui:
language: 'auto' # one of 'auto' | 'en' | 'zh' | 'pl' | 'nl' | 'ja' | 'ko'
timeFormat: '02 Jan 06 15:04 MST' # https://pkg.go.dev/time#Time.Format
theme:
lightTheme: false # For terminals with a light background
activeBorderColor:
- green
- bold
@@ -47,8 +43,6 @@ gui:
- blue
unstagedChangesColor:
- red
defaultFgColor:
- default
commitLength:
show: true
mouseEvents: true
@@ -57,28 +51,24 @@ 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
useConfig: false
commit:
signOff: false
verbose: default # one of 'default' | 'always' | 'never'
merging:
# only applicable to unix users
manualCommit: false
# extra args passed to `git merge`, e.g. --no-ff
args: ''
log:
# one of date-order, author-date-order, topo-order or default.
# one of date-order, author-date-order, topo-order.
# topo-order makes it easier to read the git log graph, but commits may not
# appear chronologically. See https://git-scm.com/docs/git-log#_commit_ordering
order: 'topo-order'
@@ -95,20 +85,19 @@ git:
overrideGpg: false # prevents lazygit from spawning a separate process when using GPG
disableForcePushing: false
parseEmoji: false
enableGhCommand: 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'.
update:
method: prompt # can be: prompt | background | never
days: 14 # how often an update is checked for
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
confirmOnQuit: false
# determines whether hitting 'esc' will quit the application when there is nothing to cancel/close
quitOnTopLevelReturn: false
@@ -139,12 +128,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 +162,7 @@ keybinding:
diffingMenu-alt: '<c-e>' # deprecated
copyToClipboard: '<c-o>'
submitEditorText: '<enter>'
appendNewline: '<a-enter>'
extrasMenu: '@'
toggleWhitespaceInDiffView: '<c-w>'
increaseContextInDiffView: '}'
@@ -192,8 +183,6 @@ keybinding:
viewResetOptions: 'D'
fetch: 'f'
toggleTreeView: '`'
openMergeTool: 'M'
openStatusFilter: '<c-b>'
branches:
createPullRequest: 'o'
viewPullRequestOptions: 'O'
@@ -204,7 +193,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'
@@ -232,7 +220,6 @@ keybinding:
viewBisectOptions: 'b'
stash:
popStash: 'g'
renameStash: 'r'
commitFiles:
checkoutCommitFile: 'c'
main:
@@ -252,60 +239,59 @@ keybinding:
```yaml
os:
open: 'start "" {{filename}}'
openCommand: 'start "" {{filename}}'
```
### Linux
```yaml
os:
open: 'xdg-open {{filename}} >/dev/null'
openCommand: 'xdg-open {{filename}} >/dev/null'
```
### OSX
```yaml
os:
open: 'open {{filename}}'
openCommand: 'open {{filename}}'
```
### 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 +307,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)
@@ -345,6 +340,23 @@ The available attributes are:
- reverse # useful for high-contrast
- underline
## Light terminal theme
If you have issues with a light terminal theme where you can't read / see the text add these settings
```yaml
gui:
theme:
lightTheme: true
activeBorderColor:
- black
- bold
inactiveBorderColor:
- black
selectedLineBgColor:
- default
```
## Highlighting the selected line
If you don't like the default behaviour of highlighting the selected line with a blue background, you can use the `selectedLineBgColor` and `selectedRangeBgColor` keys to customise the behaviour. If you just want to embolden the selected line (this was the original default), you can do the following:
@@ -420,14 +432,6 @@ gui:
For all possible keybinding options, check [Custom_Keybindings.md](https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md)
You can disable certain key bindings by specifying `null`.
```yaml
keybinding:
universal:
edit: null # disable 'edit file'
```
### Example Keybindings For Colemak Users
```yaml
@@ -474,7 +478,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
+1 -1
View File
@@ -8,7 +8,7 @@ customCommands:
command: 'hub browse -- "commit/{{.SelectedLocalCommit.Sha}}"'
context: 'commits'
- key: 'a'
command: "git {{if .SelectedFile.HasUnstagedChanges}} add {{else}} reset {{end}} {{.SelectedFile.Name | quote}}"
command: "git {{if .SelectedFile.HasUnstagedChanges}} add {{else}} reset {{end}} {{.SelectedFile.Name}}"
context: 'files'
description: 'toggle file staged'
- key: 'C'
+8 -14
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
@@ -192,9 +191,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>space</kbd>: toggle line staged / unstaged
<kbd>d</kbd>: delete change (git reset)
<kbd>E</kbd>: edit hunk
<kbd>c</kbd>: commit changes
<kbd>w</kbd>: commit changes without pre-commit hook
<kbd>C</kbd>: commit changes using git editor
</pre>
## Reflog
@@ -215,7 +211,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
@@ -243,7 +238,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>g</kbd>: pop
<kbd>d</kbd>: drop
<kbd>n</kbd>: new branch
<kbd>r</kbd>: rename stash
<kbd>enter</kbd>: view selected item's files
</pre>
+8 -14
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>: カスタムコマンドを実行
@@ -49,7 +48,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>g</kbd>: pop
<kbd>d</kbd>: drop
<kbd>n</kbd>: 新しいブランチを作成
<kbd>r</kbd>: Stashを変更
<kbd>enter</kbd>: view selected item's files
</pre>
@@ -153,6 +151,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 +190,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 +201,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 +222,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 +237,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選択を切り替え
@@ -252,9 +250,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>space</kbd>: 選択行をステージ/アンステージ
<kbd>d</kbd>: 変更を削除 (git reset)
<kbd>E</kbd>: edit hunk
<kbd>c</kbd>: 変更をコミット
<kbd>w</kbd>: pre-commitフックを実行せずに変更をコミット
<kbd>C</kbd>: gitエディタを使用して変更をコミット
</pre>
## リモート
@@ -269,7 +264,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 -14
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
@@ -64,7 +63,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>g</kbd>: pop
<kbd>d</kbd>: drop
<kbd>n</kbd>: 새 브랜치 생성
<kbd>r</kbd>: rename stash
<kbd>enter</kbd>: view selected item's files
</pre>
@@ -88,8 +86,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 +107,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 +122,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
@@ -137,9 +135,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>space</kbd>: 선택한 행을 staged / unstaged
<kbd>d</kbd>: 변경을 삭제 (git reset)
<kbd>E</kbd>: edit hunk
<kbd>c</kbd>: 커밋 변경내용
<kbd>w</kbd>: commit changes without pre-commit hook
<kbd>C</kbd>: Git 편집기를 사용하여 변경 내용을 커밋합니다.
</pre>
## 브랜치
@@ -158,7 +153,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 +194,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 +267,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 -14
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
@@ -231,9 +229,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>space</kbd>: toggle lijnen staged / unstaged
<kbd>d</kbd>: verwijdert change (git reset)
<kbd>E</kbd>: edit hunk
<kbd>c</kbd>: commit veranderingen
<kbd>w</kbd>: commit veranderingen zonder pre-commit hook
<kbd>C</kbd>: commit veranderingen met de git editor
</pre>
## Stash
@@ -243,7 +238,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>g</kbd>: pop
<kbd>d</kbd>: laten vallen
<kbd>n</kbd>: nieuwe branch
<kbd>r</kbd>: rename stash
<kbd>enter</kbd>: bekijk gecommite bestanden
</pre>
+8 -14
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
@@ -169,9 +168,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>space</kbd>: toggle line staged / unstaged
<kbd>d</kbd>: delete change (git reset)
<kbd>E</kbd>: edit hunk
<kbd>c</kbd>: Zatwierdź zmiany
<kbd>w</kbd>: zatwierdź zmiany bez skryptu pre-commit
<kbd>C</kbd>: Zatwierdź zmiany używając edytora
</pre>
## Reflog
@@ -192,7 +188,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 +213,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
@@ -236,7 +231,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>g</kbd>: wyciągnij
<kbd>d</kbd>: porzuć
<kbd>n</kbd>: nowa gałąź
<kbd>r</kbd>: rename stash
<kbd>enter</kbd>: przeglądaj pliki commita
</pre>
+8 -14
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>: 切换选择区块
@@ -239,9 +238,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>space</kbd>: 切换行暂存状态
<kbd>d</kbd>: 取消变更 (git reset)
<kbd>E</kbd>: edit hunk
<kbd>c</kbd>: 提交更改
<kbd>w</kbd>: 提交更改而无需预先提交钩子
<kbd>C</kbd>: 提交更改(使用编辑器编辑提交信息)
</pre>
## 正常
@@ -268,14 +264,12 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>g</kbd>: 应用并删除
<kbd>d</kbd>: 删除
<kbd>n</kbd>: 新分支
<kbd>r</kbd>: rename stash
<kbd>enter</kbd>: 查看提交的文件
</pre>
## 远程分支
<pre>
<kbd>ctrl+o</kbd>: 将分支名称复制到剪贴板
<kbd>space</kbd>: 检出
<kbd>n</kbd>: 新分支
<kbd>M</kbd>: 合并到当前检出的分支
+15 -16
View File
@@ -4,41 +4,40 @@ 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.2
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/go-git/v5 v5.1.2-0.20201006095850-341962be15a4
github.com/jesseduffield/gocui v0.3.1-0.20220815095708-156fda5e0419
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
github.com/jesseduffield/yaml v2.1.0+incompatible
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/kyokomi/emoji/v2 v2.2.8
github.com/lucasb-eyer/go-colorful v1.2.0
github.com/mattn/go-runewidth v0.0.14
github.com/mattn/go-runewidth v0.0.13
github.com/mgutz/str v1.2.0
github.com/pmezard/go-difflib v1.0.0
github.com/sahilm/fuzzy v0.1.0
github.com/samber/lo v1.31.0
github.com/samber/lo v1.10.1
github.com/sanity-io/litter v1.5.2
github.com/sasha-s/go-deadlock v0.3.1
github.com/sirupsen/logrus v1.4.2
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
github.com/stretchr/testify v1.8.0
github.com/stretchr/testify v1.7.0
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0
gopkg.in/yaml.v3 v3.0.1
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
)
require (
@@ -62,14 +61,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.3.4 // 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/crypto v0.0.0-20201002170205-7f63de1d35b0 // 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/net v0.0.0-20201002202402-0a1ea396d57c // indirect
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 // indirect
golang.org/x/text v0.3.7 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)
+30 -54
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.2 h1:tKzG29kO9p2V++3oBY2W9zUjYu7IK1MENFeY/BzJSVY=
github.com/gdamore/tcell/v2 v2.5.2/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo=
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=
@@ -72,14 +70,12 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 h1:EQP2Tv8TIcC6Y4RI+1ZbJDOHfGJ570tPeYVCqo7/tws=
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/go-git/v5 v5.1.2-0.20201006095850-341962be15a4 h1:GOQrmaE8i+KEdB8NzAegKYd4tPn/inM0I1uo0NXFerg=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
github.com/jesseduffield/gocui v0.3.1-0.20220815095708-156fda5e0419 h1:p3Ix7RUcy4X16Lk5jTSfTxecJT7ryqYHclfRbo/Svzs=
github.com/jesseduffield/gocui v0.3.1-0.20220815095708-156fda5e0419/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=
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5/go.mod h1:qxN4mHOAyeIDLP7IK7defgPClM/z1Kze8VVQiaEjzsQ=
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e h1:uw/oo+kg7t/oeMs6sqlAwr85ND/9cpO3up3VxphxY0U=
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e/go.mod h1:u60qdFGXRd36jyEXxetz0vQceQIxzI13lIo3EFUDf4I=
github.com/jesseduffield/yaml v2.1.0+incompatible h1:HWQJ1gIv2zHKbDYNp0Jwjlj24K8aqpFHnMCynY1EpmE=
@@ -116,8 +112,8 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mgutz/str v1.2.0 h1:4IzWSdIz9qPQWLfKZ0rJcV0jcUDpxvP4JVZ4GXQyvSw=
github.com/mgutz/str v1.2.0/go.mod h1:w1v0ofgLaJdoD0HpQ3fycxKD1WtxpjSo151pK/31q6w=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
@@ -138,13 +134,12 @@ 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/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/rivo/uniseg v0.3.4 h1:3Z3Eu6FGHZWSfNKJTOUiPatWwfc7DzJRU04jFUqJODw=
github.com/rivo/uniseg v0.3.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=
github.com/samber/lo v1.31.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8=
github.com/samber/lo v1.10.1 h1:0D3h7i0U3hRAbaCeQ82DLe67n0A7Bbl0/cEoWqFGp+U=
github.com/samber/lo v1.10.1/go.mod h1:2I7tgIv8Q1SG2xEIkRq0F2i2zgxVpnyPOP0d3Gj2r+A=
github.com/sanity-io/litter v1.5.2 h1:AnC8s9BMORWH5a4atZ4D6FPVvKGzHcnc5/IVTa87myw=
github.com/sanity-io/litter v1.5.2/go.mod h1:5Z71SvaYy5kcGtyglXOC9rrUi3c1E8CamFWjQsazTh0=
github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0=
@@ -156,42 +151,34 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
github.com/urfave/cli v1.20.1-0.20180226030253-8e01ec4cd3e2/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20220318154914-8dddf5d87bd8 h1:s/+U+w0teGzcoH2mdIlFQ6KfVKGaYpgyGdUefZrn9TU=
golang.org/x/exp v0.0.0-20220318154914-8dddf5d87bd8/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c h1:dk0ukUIHmGHqASjP0iue2261isepFCC6XRCSd1nHgDw=
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c/go.mod h1:iQL9McJNjoIa5mjH6nYTCTZXUN6RP+XW3eib7Ya3XcI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20170407050850-f3918c30c5c2/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -201,33 +188,22 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
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 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
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/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=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -246,5 +222,5 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+80 -14
View File
@@ -7,6 +7,8 @@ import (
"log"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/go-errors/errors"
@@ -15,6 +17,7 @@ import (
appTypes "github.com/jesseduffield/lazygit/pkg/app/types"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/config"
@@ -98,36 +101,95 @@ func NewApp(config config.AppConfigurer, common *common.Common) (*App, error) {
return app, err
}
gitVersion, err := app.validateGitVersion()
if err != nil {
return app, err
}
showRecentRepos, err := app.setupRepo()
if err != nil {
return app, err
}
app.Gui, err = gui.NewGui(common, config, gitVersion, app.Updater, showRecentRepos, dirName)
gitConfig := git_config.NewStdCachedGitConfig(app.Log)
app.Gui, err = gui.NewGui(common, config, gitConfig, app.Updater, showRecentRepos, dirName)
if err != nil {
return app, err
}
if app.Gui.Config.GetUserConfig().Git.EnableGhCommand {
if err := app.validateGhVersion(); err != nil {
return nil, err
}
}
return app, nil
}
func (app *App) validateGitVersion() (*git_commands.GitVersion, error) {
version, err := git_commands.GetGitVersion(app.OSCommand)
// if we get an error anywhere here we'll show the same status
minVersionError := errors.New(app.Tr.MinGitVersionError)
func (app *App) validateGhVersion() error {
output, err := app.OSCommand.Cmd.New("gh --version").RunWithOutput()
if err != nil {
return nil, minVersionError
return fmt.Errorf(app.Tr.FailedToObtainGhVersionError, err.Error())
}
if version.IsOlderThan(2, 20, 0) {
return nil, minVersionError
if !isGhVersionValid(output) {
return errors.New(app.Tr.MinGhVersionError)
}
return version, nil
return nil
}
func isGhVersionValid(versionStr string) bool {
// output should be something like:
// gh version 2.0.0 (2021-08-23)
// https://github.com/cli/cli/releases/tag/v2.0.0
re := regexp.MustCompile(`[^\d]+([\d\.]+)`)
matches := re.FindStringSubmatch(versionStr)
if len(matches) == 0 {
return false
}
ghVersion := matches[1]
majorVersion, err := strconv.Atoi(ghVersion[0:1])
if err != nil {
return false
}
if majorVersion < 2 {
return false
}
return true
}
func (app *App) validateGitVersion() error {
output, err := app.OSCommand.Cmd.New("git --version").RunWithOutput()
if err != nil {
return fmt.Errorf(app.Tr.FailedToObtainGitVersionError, err.Error())
}
if !isGitVersionValid(output) {
return errors.New(app.Tr.MinGitVersionError)
}
return nil
}
func isGitVersionValid(versionStr string) bool {
// output should be something like: 'git version 2.23.0 (blah)'
re := regexp.MustCompile(`[^\d]+([\d\.]+)`)
matches := re.FindStringSubmatch(versionStr)
if len(matches) == 0 {
return false
}
gitVersion := matches[1]
majorVersion, err := strconv.Atoi(gitVersion[0:1])
if err != nil {
return false
}
if majorVersion < 2 {
return false
}
return true
}
func isDirectoryAGitRepository(dir string) (bool, error) {
@@ -148,6 +210,10 @@ func openRecentRepo(app *App) bool {
}
func (app *App) setupRepo() (bool, error) {
if err := app.validateGitVersion(); err != nil {
return false, err
}
if env.GetGitDirEnv() != "" {
// we've been given the git dir directly. We'll verify this dir when initializing our Git object
return false, nil
+85
View File
@@ -0,0 +1,85 @@
package app
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsGitVersionValid(t *testing.T) {
type scenario struct {
versionStr string
expectedResult bool
}
scenarios := []scenario{
{
"",
false,
},
{
"git version 1.9.0",
false,
},
{
"git version 1.9.0 (Apple Git-128)",
false,
},
{
"git version 2.4.0",
true,
},
{
"git version 2.24.3 (Apple Git-128)",
true,
},
}
for _, s := range scenarios {
s := s
t.Run(s.versionStr, func(t *testing.T) {
result := isGitVersionValid(s.versionStr)
assert.Equal(t, result, s.expectedResult)
})
}
}
func TestIsValidGhVersion(t *testing.T) {
type scenario struct {
versionStr string
expectedResult bool
}
scenarios := []scenario{
{
"",
false,
},
{
`gh version 1.0.0 (2020-08-23)
https://github.com/cli/cli/releases/tag/v1.0.0`,
false,
},
{
`gh version 2.0.0 (2021-08-23)
https://github.com/cli/cli/releases/tag/v2.0.0`,
true,
},
{
`gh version 1.1.0 (2021-10-14)
https://github.com/cli/cli/releases/tag/v1.1.0
A new release of gh is available: 1.1.0 → v2.2.0
To upgrade, run: brew update && brew upgrade gh
https://github.com/cli/cli/releases/tag/v2.2.0`,
false,
},
}
for _, s := range scenarios {
t.Run(s.versionStr, func(t *testing.T) {
result := isGhVersionValid(s.versionStr)
assert.Equal(t, result, s.expectedResult)
})
}
}
+50 -249
View File
@@ -1,17 +1,14 @@
package daemon
import (
"encoding/json"
"fmt"
"io/ioutil"
"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 +16,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 +50,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 := ioutil.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
}
+1 -15
View File
@@ -17,7 +17,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/env"
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
"github.com/jesseduffield/lazygit/pkg/logs"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
"gopkg.in/yaml.v3"
@@ -58,11 +57,6 @@ func Start(buildInfo *BuildInfo, integrationTest integrationTypes.IntegrationTes
if err != nil {
log.Fatal(err)
}
if isRepo, err := isDirectoryAGitRepository(absRepoPath); err != nil || !isRepo {
log.Fatal(absRepoPath + " is not a valid git repository.")
}
cliArgs.WorkTree = absRepoPath
cliArgs.GitDir = filepath.Join(absRepoPath, ".git")
}
@@ -84,8 +78,7 @@ func Start(buildInfo *BuildInfo, integrationTest integrationTypes.IntegrationTes
}
if cliArgs.PrintVersionInfo {
gitVersion := getGitVersionInfo()
fmt.Printf("commit=%s, build date=%s, build source=%s, version=%s, os=%s, arch=%s, git version=%s\n", buildInfo.Commit, buildInfo.Date, buildInfo.BuildSource, buildInfo.Version, runtime.GOOS, runtime.GOARCH, gitVersion)
fmt.Printf("commit=%s, build date=%s, build source=%s, version=%s, os=%s, arch=%s\n", buildInfo.Commit, buildInfo.Date, buildInfo.BuildSource, buildInfo.Version, runtime.GOOS, runtime.GOARCH)
os.Exit(0)
}
@@ -270,10 +263,3 @@ func mergeBuildInfo(buildInfo *BuildInfo) {
buildInfo.Date = time.Value
}
}
func getGitVersionInfo() string {
cmd := secureexec.Command("git", "--version")
stdout, _ := cmd.Output()
gitVersion := strings.Trim(strings.TrimPrefix(string(stdout), "git version "), " \r\n")
return gitVersion
}
+2 -2
View File
@@ -1,7 +1,7 @@
package app
import (
"io"
"io/ioutil"
"log"
"os"
@@ -26,7 +26,7 @@ func newLogger(config config.AppConfigurer) *logrus.Entry {
func newProductionLogger() *logrus.Logger {
log := logrus.New()
log.Out = io.Discard
log.Out = ioutil.Discard
log.SetLevel(logrus.ErrorLevel)
return log
}
+3 -2
View File
@@ -3,6 +3,7 @@ package cheatsheet
import (
"fmt"
"io/fs"
"io/ioutil"
"log"
"os"
"path/filepath"
@@ -12,7 +13,7 @@ import (
)
func Check() {
dir := GetKeybindingsDir()
dir := GetDir()
tmpDir := filepath.Join(os.TempDir(), "lazygit_cheatsheet")
err := os.RemoveAll(tmpDir)
if err != nil {
@@ -59,7 +60,7 @@ func obtainContent(dir string) string {
content := ""
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if re.MatchString(path) {
bytes, err := os.ReadFile(path)
bytes, err := ioutil.ReadFile(path)
if err != nil {
log.Fatalf("Error occurred while checking if cheatsheets are up to date: %v", err)
}
+32 -33
View File
@@ -15,12 +15,12 @@ import (
"github.com/jesseduffield/generics/maps"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazycore/pkg/utils"
"github.com/jesseduffield/lazygit/pkg/app"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
@@ -44,8 +44,8 @@ func CommandToRun() string {
return "go run scripts/cheatsheet/main.go generate"
}
func GetKeybindingsDir() string {
return utils.GetLazyRootDirectory() + "/docs/keybindings"
func GetDir() string {
return utils.GetLazygitRootDirectory() + "/docs/keybindings"
}
func generateAtDir(cheatsheetDir string) {
@@ -75,7 +75,7 @@ func generateAtDir(cheatsheetDir string) {
}
func Generate() {
generateAtDir(GetKeybindingsDir())
generateAtDir(GetDir())
}
func writeString(file *os.File, str string) {
@@ -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]
@@ -132,7 +131,7 @@ func getBindingSections(bindings []*types.Binding, tr *i18n.TranslationSet) []*b
return false
}
return (binding.Description != "" || binding.Alternative != "") && binding.Key != nil
return (binding.Description != "" || binding.Alternative != "")
})
bindingsByHeader := lo.GroupBy(bindingsToDisplay, func(binding *types.Binding) header {
-29
View File
@@ -27,7 +27,6 @@ func TestGetBindingSections(t *testing.T) {
{
ViewName: "files",
Description: "stage file",
Key: 'a',
},
},
expected: []*bindingSection{
@@ -37,7 +36,6 @@ func TestGetBindingSections(t *testing.T) {
{
ViewName: "files",
Description: "stage file",
Key: 'a',
},
},
},
@@ -49,7 +47,6 @@ func TestGetBindingSections(t *testing.T) {
{
ViewName: "",
Description: "quit",
Key: 'a',
},
},
expected: []*bindingSection{
@@ -59,7 +56,6 @@ func TestGetBindingSections(t *testing.T) {
{
ViewName: "",
Description: "quit",
Key: 'a',
},
},
},
@@ -71,17 +67,14 @@ func TestGetBindingSections(t *testing.T) {
{
ViewName: "files",
Description: "stage file",
Key: 'a',
},
{
ViewName: "files",
Description: "unstage file",
Key: 'a',
},
{
ViewName: "submodules",
Description: "drop submodule",
Key: 'a',
},
},
expected: []*bindingSection{
@@ -91,12 +84,10 @@ func TestGetBindingSections(t *testing.T) {
{
ViewName: "files",
Description: "stage file",
Key: 'a',
},
{
ViewName: "files",
Description: "unstage file",
Key: 'a',
},
},
},
@@ -106,7 +97,6 @@ func TestGetBindingSections(t *testing.T) {
{
ViewName: "submodules",
Description: "drop submodule",
Key: 'a',
},
},
},
@@ -118,23 +108,19 @@ func TestGetBindingSections(t *testing.T) {
{
ViewName: "files",
Description: "stage file",
Key: 'a',
},
{
ViewName: "files",
Description: "unstage file",
Key: 'a',
},
{
ViewName: "files",
Description: "scroll",
Key: 'a',
Tag: "navigation",
},
{
ViewName: "commits",
Description: "revert commit",
Key: 'a',
},
},
expected: []*bindingSection{
@@ -144,7 +130,6 @@ func TestGetBindingSections(t *testing.T) {
{
ViewName: "files",
Description: "scroll",
Key: 'a',
Tag: "navigation",
},
},
@@ -155,7 +140,6 @@ func TestGetBindingSections(t *testing.T) {
{
ViewName: "commits",
Description: "revert commit",
Key: 'a',
},
},
},
@@ -165,12 +149,10 @@ func TestGetBindingSections(t *testing.T) {
{
ViewName: "files",
Description: "stage file",
Key: 'a',
},
{
ViewName: "files",
Description: "unstage file",
Key: 'a',
},
},
},
@@ -182,34 +164,28 @@ func TestGetBindingSections(t *testing.T) {
{
ViewName: "files",
Description: "stage file",
Key: 'a',
},
{
ViewName: "files",
Description: "unstage file",
Key: 'a',
},
{
ViewName: "files",
Description: "scroll",
Key: 'a',
Tag: "navigation",
},
{
ViewName: "commits",
Description: "revert commit",
Key: 'a',
},
{
ViewName: "commits",
Description: "scroll",
Key: 'a',
Tag: "navigation",
},
{
ViewName: "commits",
Description: "page up",
Key: 'a',
Tag: "navigation",
},
},
@@ -220,13 +196,11 @@ func TestGetBindingSections(t *testing.T) {
{
ViewName: "files",
Description: "scroll",
Key: 'a',
Tag: "navigation",
},
{
ViewName: "commits",
Description: "page up",
Key: 'a',
Tag: "navigation",
},
},
@@ -237,7 +211,6 @@ func TestGetBindingSections(t *testing.T) {
{
ViewName: "commits",
Description: "revert commit",
Key: 'a',
},
},
},
@@ -247,12 +220,10 @@ func TestGetBindingSections(t *testing.T) {
{
ViewName: "files",
Description: "stage file",
Key: 'a',
},
{
ViewName: "files",
Description: "unstage file",
Key: 'a',
},
},
},
+62 -71
View File
@@ -1,6 +1,7 @@
package commands
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
@@ -11,6 +12,7 @@ import (
gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/common"
@@ -20,40 +22,41 @@ import (
// GitCommand is our main git interface
type GitCommand struct {
Branch *git_commands.BranchCommands
Commit *git_commands.CommitCommands
Config *git_commands.ConfigCommands
Custom *git_commands.CustomCommands
File *git_commands.FileCommands
Flow *git_commands.FlowCommands
Patch *git_commands.PatchCommands
Rebase *git_commands.RebaseCommands
Remote *git_commands.RemoteCommands
Stash *git_commands.StashCommands
Status *git_commands.StatusCommands
Submodule *git_commands.SubmoduleCommands
Sync *git_commands.SyncCommands
Tag *git_commands.TagCommands
WorkingTree *git_commands.WorkingTreeCommands
Bisect *git_commands.BisectCommands
Branch *git_commands.BranchCommands
Commit *git_commands.CommitCommands
Config *git_commands.ConfigCommands
Custom *git_commands.CustomCommands
File *git_commands.FileCommands
Flow *git_commands.FlowCommands
Patch *git_commands.PatchCommands
Rebase *git_commands.RebaseCommands
Remote *git_commands.RemoteCommands
Stash *git_commands.StashCommands
Status *git_commands.StatusCommands
Submodule *git_commands.SubmoduleCommands
Sync *git_commands.SyncCommands
Tag *git_commands.TagCommands
WorkingTree *git_commands.WorkingTreeCommands
Bisect *git_commands.BisectCommands
Gh *git_commands.GhCommands
HostingService *git_commands.HostingService
Loaders Loaders
}
type Loaders struct {
BranchLoader *git_commands.BranchLoader
CommitFileLoader *git_commands.CommitFileLoader
CommitLoader *git_commands.CommitLoader
FileLoader *git_commands.FileLoader
ReflogCommitLoader *git_commands.ReflogCommitLoader
RemoteLoader *git_commands.RemoteLoader
StashLoader *git_commands.StashLoader
TagLoader *git_commands.TagLoader
Branches *loaders.BranchLoader
CommitFiles *loaders.CommitFileLoader
Commits *loaders.CommitLoader
Files *loaders.FileLoader
ReflogCommits *loaders.ReflogCommitLoader
Remotes *loaders.RemoteLoader
Stash *loaders.StashLoader
Tags *loaders.TagLoader
}
func NewGitCommand(
cmn *common.Common,
version *git_commands.GitVersion,
osCommand *oscommands.OSCommand,
gitConfig git_config.IGitConfig,
syncMutex *deadlock.Mutex,
@@ -67,14 +70,13 @@ func NewGitCommand(
return nil, err
}
dotGitDir, err := findDotGitDir(os.Stat, os.ReadFile)
dotGitDir, err := findDotGitDir(os.Stat, ioutil.ReadFile)
if err != nil {
return nil, err
}
return NewGitCommandAux(
cmn,
version,
osCommand,
gitConfig,
dotGitDir,
@@ -85,7 +87,6 @@ func NewGitCommand(
func NewGitCommandAux(
cmn *common.Common,
version *git_commands.GitVersion,
osCommand *oscommands.OSCommand,
gitConfig git_config.IGitConfig,
dotGitDir string,
@@ -100,11 +101,10 @@ func NewGitCommandAux(
// on the one struct.
// common ones are: cmn, osCommand, dotGitDir, configCommands
configCommands := git_commands.NewConfigCommands(cmn, gitConfig, repo)
gitCommon := git_commands.NewGitCommon(cmn, cmd, osCommand, dotGitDir, repo, configCommands, syncMutex)
fileLoader := git_commands.NewFileLoader(cmn, cmd, configCommands)
gitCommon := git_commands.NewGitCommon(cmn, version, cmd, osCommand, dotGitDir, repo, configCommands, syncMutex)
statusCommands := git_commands.NewStatusCommands(gitCommon)
fileLoader := loaders.NewFileLoader(cmn, cmd, configCommands)
flowCommands := git_commands.NewFlowCommands(gitCommon)
remoteCommands := git_commands.NewRemoteCommands(gitCommon)
branchCommands := git_commands.NewBranchCommands(gitCommon)
@@ -117,50 +117,41 @@ 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)
commitFileLoader := git_commands.NewCommitFileLoader(cmn, cmd)
commitLoader := git_commands.NewCommitLoader(cmn, cmd, dotGitDir, branchCommands.CurrentBranchInfo, statusCommands.RebaseMode)
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)
ghCommands := git_commands.NewGhCommand(gitCommon)
hostingServiceCommands := git_commands.NewHostingServiceCommand(gitCommon)
return &GitCommand{
Branch: branchCommands,
Commit: commitCommands,
Config: configCommands,
Custom: customCommands,
File: fileCommands,
Flow: flowCommands,
Patch: patchCommands,
Rebase: rebaseCommands,
Remote: remoteCommands,
Stash: stashCommands,
Status: statusCommands,
Submodule: submoduleCommands,
Sync: syncCommands,
Tag: tagCommands,
Bisect: bisectCommands,
WorkingTree: workingTreeCommands,
Branch: branchCommands,
Commit: commitCommands,
Config: configCommands,
Custom: customCommands,
File: fileCommands,
Flow: flowCommands,
Patch: patchCommands,
Rebase: rebaseCommands,
Remote: remoteCommands,
Stash: stashCommands,
Status: statusCommands,
Submodule: submoduleCommands,
Sync: syncCommands,
Tag: tagCommands,
Bisect: bisectCommands,
WorkingTree: workingTreeCommands,
Gh: ghCommands,
HostingService: hostingServiceCommands,
Loaders: Loaders{
BranchLoader: branchLoader,
CommitFileLoader: commitFileLoader,
CommitLoader: commitLoader,
FileLoader: fileLoader,
ReflogCommitLoader: reflogCommitLoader,
RemoteLoader: remoteLoader,
StashLoader: stashLoader,
TagLoader: tagLoader,
Branches: loaders.NewBranchLoader(cmn, branchCommands.GetRawBranches, branchCommands.CurrentBranchName, configCommands),
CommitFiles: loaders.NewCommitFileLoader(cmn, cmd),
Commits: loaders.NewCommitLoader(cmn, cmd, dotGitDir, branchCommands.CurrentBranchName, statusCommands.RebaseMode),
Files: fileLoader,
ReflogCommits: loaders.NewReflogCommitLoader(cmn, cmd),
Remotes: loaders.NewRemoteLoader(cmn, cmd, repo.Remotes),
Stash: loaders.NewStashLoader(cmn, cmd),
Tags: loaders.NewTagLoader(cmn, cmd),
},
}
}
-4
View File
@@ -21,10 +21,6 @@ func (self *gitCmdObjRunner) RunWithOutput(cmdObj oscommands.ICmdObj) (string, e
return self.innerRunner.RunWithOutput(cmdObj)
}
func (self *gitCmdObjRunner) RunWithOutputs(cmdObj oscommands.ICmdObj) (string, string, error) {
return self.innerRunner.RunWithOutputs(cmdObj)
}
func (self *gitCmdObjRunner) RunAndProcessLines(cmdObj oscommands.ICmdObj, onLine func(line string) (bool, error)) error {
return self.innerRunner.RunAndProcessLines(cmdObj, onLine)
}
+21 -23
View File
@@ -2,12 +2,19 @@ package git_commands
import (
"fmt"
"regexp"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// this takes something like:
// * (HEAD detached at 264fc6f5)
// remotes
// and returns '264fc6f5' as the second match
const CurrentBranchNameRegex = `(?m)^\*.*?([^ ]*?)\)?$`
type BranchCommands struct {
*GitCommon
}
@@ -23,38 +30,29 @@ func (self *BranchCommands) New(name string, base string) error {
return self.cmd.New(fmt.Sprintf("git checkout -b %s %s", self.cmd.Quote(name), self.cmd.Quote(base))).Run()
}
// CurrentBranchInfo get the current branch information.
func (self *BranchCommands) CurrentBranchInfo() (BranchInfo, error) {
// CurrentBranchName get the current branch name and displayname.
// the first returned string is the name and the second is the displayname
// e.g. name is 123asdf and displayname is '(HEAD detached at 123asdf)'
func (self *BranchCommands) CurrentBranchName() (string, string, error) {
branchName, err := self.cmd.New("git symbolic-ref --short HEAD").DontLog().RunWithOutput()
if err == nil && branchName != "HEAD\n" {
trimmedBranchName := strings.TrimSpace(branchName)
return BranchInfo{
RefName: trimmedBranchName,
DisplayName: trimmedBranchName,
DetachedHead: false,
}, nil
return trimmedBranchName, trimmedBranchName, nil
}
output, err := self.cmd.New(`git branch --points-at=HEAD --format="%(HEAD)%00%(objectname)%00%(refname)"`).DontLog().RunWithOutput()
output, err := self.cmd.New("git branch --contains").DontLog().RunWithOutput()
if err != nil {
return BranchInfo{}, err
return "", "", err
}
for _, line := range utils.SplitLines(output) {
split := strings.Split(strings.TrimRight(line, "\r\n"), "\x00")
if len(split) == 3 && split[0] == "*" {
sha := split[1]
displayName := split[2]
return BranchInfo{
RefName: sha,
DisplayName: displayName,
DetachedHead: true,
}, nil
re := regexp.MustCompile(CurrentBranchNameRegex)
match := re.FindStringSubmatch(line)
if len(match) > 0 {
branchName = match[1]
displayBranchName := match[0][2:]
return branchName, displayBranchName, nil
}
}
return BranchInfo{
RefName: "HEAD",
DisplayName: "HEAD",
DetachedHead: true,
}, nil
return "HEAD", "HEAD", nil
}
// Delete delete branch
-210
View File
@@ -1,210 +0,0 @@
package git_commands
import (
"regexp"
"strings"
"github.com/jesseduffield/generics/set"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/go-git/v5/config"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// context:
// we want to only show 'safe' branches (ones that haven't e.g. been deleted)
// which `git branch -a` gives us, but we also want the recency data that
// git reflog gives us.
// So we get the HEAD, then append get the reflog branches that intersect with
// our safe branches, then add the remaining safe branches, ensuring uniqueness
// along the way
// if we find out we need to use one of these functions in the git.go file, we
// can just pull them out of here and put them there and then call them from in here
type BranchLoaderConfigCommands interface {
Branches() (map[string]*config.Branch, error)
}
type BranchInfo struct {
RefName string
DisplayName string // e.g. '(HEAD detached at 123asdf)'
DetachedHead bool
}
// BranchLoader returns a list of Branch objects for the current repo
type BranchLoader struct {
*common.Common
getRawBranches func() (string, error)
getCurrentBranchInfo func() (BranchInfo, error)
config BranchLoaderConfigCommands
}
func NewBranchLoader(
cmn *common.Common,
getRawBranches func() (string, error),
getCurrentBranchInfo func() (BranchInfo, error),
config BranchLoaderConfigCommands,
) *BranchLoader {
return &BranchLoader{
Common: cmn,
getRawBranches: getRawBranches,
getCurrentBranchInfo: getCurrentBranchInfo,
config: config,
}
}
// Load the list of branches for the current repo
func (self *BranchLoader) Load(reflogCommits []*models.Commit) ([]*models.Branch, error) {
branches := self.obtainBranches()
reflogBranches := self.obtainReflogBranches(reflogCommits)
// loop through reflog branches. If there is a match, merge them, then remove it from the branches and keep it in the reflog branches
branchesWithRecency := make([]*models.Branch, 0)
outer:
for _, reflogBranch := range reflogBranches {
for j, branch := range branches {
if branch.Head {
continue
}
if strings.EqualFold(reflogBranch.Name, branch.Name) {
branch.Recency = reflogBranch.Recency
branchesWithRecency = append(branchesWithRecency, branch)
branches = slices.Remove(branches, j)
continue outer
}
}
}
branches = slices.Prepend(branches, branchesWithRecency...)
foundHead := false
for i, branch := range branches {
if branch.Head {
foundHead = true
branch.Recency = " *"
branches = slices.Move(branches, i, 0)
break
}
}
if !foundHead {
info, err := self.getCurrentBranchInfo()
if err != nil {
return nil, err
}
branches = slices.Prepend(branches, &models.Branch{Name: info.RefName, DisplayName: info.DisplayName, Head: true, DetachedHead: info.DetachedHead, Recency: " *"})
}
configBranches, err := self.config.Branches()
if err != nil {
return nil, err
}
for _, branch := range branches {
match := configBranches[branch.Name]
if match != nil {
branch.UpstreamRemote = match.Remote
branch.UpstreamBranch = match.Merge.Short()
}
}
return branches, nil
}
func (self *BranchLoader) obtainBranches() []*models.Branch {
output, err := self.getRawBranches()
if err != nil {
panic(err)
}
trimmedOutput := strings.TrimSpace(output)
outputLines := strings.Split(trimmedOutput, "\n")
return slices.FilterMap(outputLines, func(line string) (*models.Branch, bool) {
if line == "" {
return nil, false
}
split := strings.Split(line, "\x00")
if len(split) != 4 {
// Ignore line if it isn't separated into 4 parts
// This is probably a warning message, for more info see:
// https://github.com/jesseduffield/lazygit/issues/1385#issuecomment-885580439
return nil, false
}
return obtainBranch(split), true
})
}
// Obtain branch information from parsed line output of getRawBranches()
// split contains the '|' separated tokens in the line of output
func obtainBranch(split []string) *models.Branch {
name := strings.TrimPrefix(split[1], "heads/")
branch := &models.Branch{
Name: name,
Pullables: "?",
Pushables: "?",
Head: split[0] == "*",
}
upstreamName := split[2]
if upstreamName == "" {
// if we're here then it means we do not have a local version of the remote.
// The branch might still be tracking a remote though, we just don't know
// how many commits ahead/behind it is
return branch
}
track := split[3]
if track == "[gone]" {
branch.UpstreamGone = true
} else {
re := regexp.MustCompile(`ahead (\d+)`)
match := re.FindStringSubmatch(track)
if len(match) > 1 {
branch.Pushables = match[1]
} else {
branch.Pushables = "0"
}
re = regexp.MustCompile(`behind (\d+)`)
match = re.FindStringSubmatch(track)
if len(match) > 1 {
branch.Pullables = match[1]
} else {
branch.Pullables = "0"
}
}
return branch
}
// TODO: only look at the new reflog commits, and otherwise store the recencies in
// int form against the branch to recalculate the time ago
func (self *BranchLoader) obtainReflogBranches(reflogCommits []*models.Commit) []*models.Branch {
foundBranches := set.New[string]()
re := regexp.MustCompile(`checkout: moving from ([\S]+) to ([\S]+)`)
reflogBranches := make([]*models.Branch, 0, len(reflogCommits))
for _, commit := range reflogCommits {
match := re.FindStringSubmatch(commit.Name)
if len(match) != 3 {
continue
}
recency := utils.UnixToTimeAgo(commit.UnixTimestamp)
for _, branchName := range match[1:] {
if !foundBranches.Includes(branchName) {
foundBranches.Add(branchName)
reflogBranches = append(reflogBranches, &models.Branch{
Recency: recency,
Name: branchName,
})
}
}
}
return reflogBranches
}
@@ -1,52 +0,0 @@
package git_commands
// "*|feat/detect-purge|origin/feat/detect-purge|[ahead 1]"
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/stretchr/testify/assert"
)
func TestObtainBanch(t *testing.T) {
type scenario struct {
testName string
input []string
expectedBranch *models.Branch
}
scenarios := []scenario{
{
testName: "TrimHeads",
input: []string{"", "heads/a_branch", "", ""},
expectedBranch: &models.Branch{Name: "a_branch", Pushables: "?", Pullables: "?", Head: false},
},
{
testName: "NoUpstream",
input: []string{"", "a_branch", "", ""},
expectedBranch: &models.Branch{Name: "a_branch", Pushables: "?", Pullables: "?", Head: false},
},
{
testName: "IsHead",
input: []string{"*", "a_branch", "", ""},
expectedBranch: &models.Branch{Name: "a_branch", Pushables: "?", Pullables: "?", Head: true},
},
{
testName: "IsBehindAndAhead",
input: []string{"", "a_branch", "a_remote/a_branch", "[behind 2, ahead 3]"},
expectedBranch: &models.Branch{Name: "a_branch", Pushables: "3", Pullables: "2", Head: false},
},
{
testName: "RemoteBranchIsGone",
input: []string{"", "a_branch", "a_remote/a_branch", "[gone]"},
expectedBranch: &models.Branch{Name: "a_branch", UpstreamGone: true, Pushables: "?", Pullables: "?", Head: false},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
branch := obtainBranch(s.input)
assert.EqualValues(t, s.expectedBranch, branch)
})
}
}
+22 -30
View File
@@ -53,10 +53,10 @@ func TestBranchGetCommitDifferences(t *testing.T) {
func TestBranchNewBranch(t *testing.T) {
runner := oscommands.NewFakeRunner(t).
Expect(`git checkout -b "test" "refs/heads/master"`, "", nil)
Expect(`git checkout -b "test" "master"`, "", nil)
instance := buildBranchCommands(commonDeps{runner: runner})
assert.NoError(t, instance.New("test", "refs/heads/master"))
assert.NoError(t, instance.New("test", "master"))
runner.CheckForMissingCalls()
}
@@ -162,62 +162,54 @@ func TestBranchGetAllBranchGraph(t *testing.T) {
assert.NoError(t, err)
}
func TestBranchCurrentBranchInfo(t *testing.T) {
func TestBranchCurrentBranchName(t *testing.T) {
type scenario struct {
testName string
runner *oscommands.FakeCmdObjRunner
test func(BranchInfo, error)
test func(string, string, error)
}
scenarios := []scenario{
{
"says we are on the master branch if we are",
oscommands.NewFakeRunner(t).Expect(`git symbolic-ref --short HEAD`, "master", nil),
func(info BranchInfo, err error) {
func(name string, displayname string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "master", info.RefName)
assert.EqualValues(t, "master", info.DisplayName)
assert.False(t, info.DetachedHead)
assert.EqualValues(t, "master", name)
assert.EqualValues(t, "master", displayname)
},
},
{
"falls back to git `git branch --points-at=HEAD` if symbolic-ref fails",
"falls back to git `git branch --contains` if symbolic-ref fails",
oscommands.NewFakeRunner(t).
Expect(`git symbolic-ref --short HEAD`, "", errors.New("error")).
Expect(`git branch --points-at=HEAD --format="%(HEAD)%00%(objectname)%00%(refname)"`, "*\x006f71c57a8d4bd6c11399c3f55f42c815527a73a4\x00(HEAD detached at 6f71c57a)\n", nil),
func(info BranchInfo, err error) {
Expect(`git branch --contains`, "* master", nil),
func(name string, displayname string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "6f71c57a8d4bd6c11399c3f55f42c815527a73a4", info.RefName)
assert.EqualValues(t, "(HEAD detached at 6f71c57a)", info.DisplayName)
assert.True(t, info.DetachedHead)
assert.EqualValues(t, "master", name)
assert.EqualValues(t, "master", displayname)
},
},
{
"handles a detached head (LANG=zh_CN.UTF-8)",
"handles a detached head",
oscommands.NewFakeRunner(t).
Expect(`git symbolic-ref --short HEAD`, "", errors.New("error")).
Expect(
`git branch --points-at=HEAD --format="%(HEAD)%00%(objectname)%00%(refname)"`,
"*\x00679b0456f3db7c505b398def84e7d023e5b55a8d\x00(头指针在 679b0456 分离)\n"+
" \x00679b0456f3db7c505b398def84e7d023e5b55a8d\x00refs/heads/master\n",
nil),
func(info BranchInfo, err error) {
Expect(`git branch --contains`, "* (HEAD detached at 123abcd)", nil),
func(name string, displayname string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "679b0456f3db7c505b398def84e7d023e5b55a8d", info.RefName)
assert.EqualValues(t, "(头指针在 679b0456 分离)", info.DisplayName)
assert.True(t, info.DetachedHead)
assert.EqualValues(t, "123abcd", name)
assert.EqualValues(t, "(HEAD detached at 123abcd)", displayname)
},
},
{
"bubbles up error if there is one",
oscommands.NewFakeRunner(t).
Expect(`git symbolic-ref --short HEAD`, "", errors.New("error")).
Expect(`git branch --points-at=HEAD --format="%(HEAD)%00%(objectname)%00%(refname)"`, "", errors.New("error")),
func(info BranchInfo, err error) {
Expect(`git branch --contains`, "", errors.New("error")),
func(name string, displayname string, err error) {
assert.Error(t, err)
assert.EqualValues(t, "", info.RefName)
assert.EqualValues(t, "", info.DisplayName)
assert.False(t, info.DetachedHead)
assert.EqualValues(t, "", name)
assert.EqualValues(t, "", displayname)
},
},
}
@@ -226,7 +218,7 @@ func TestBranchCurrentBranchInfo(t *testing.T) {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildBranchCommands(commonDeps{runner: s.runner})
s.test(instance.CurrentBranchInfo())
s.test(instance.CurrentBranchName())
s.runner.CheckForMissingCalls()
})
}
+14 -49
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,28 +57,12 @@ 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
func (self *CommitCommands) CommitEditorCmdObj() oscommands.ICmdObj {
return self.cmd.New(fmt.Sprintf("git commit%s%s", self.signoffFlag(), self.verboseFlag()))
return self.cmd.New(fmt.Sprintf("git commit%s", self.signoffFlag()))
}
func (self *CommitCommands) signoffFlag() string {
@@ -82,17 +73,6 @@ func (self *CommitCommands) signoffFlag() string {
}
}
func (self *CommitCommands) verboseFlag() string {
switch self.config.UserConfig.Git.Commit.Verbose {
case "always":
return " --verbose"
case "never":
return " --no-verbose"
default:
return ""
}
}
// Get the subject of the HEAD commit
func (self *CommitCommands) GetHeadCommitMessage() (string, error) {
message, err := self.cmd.New("git log -1 --pretty=%s").DontLog().RunWithOutput()
@@ -158,19 +138,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 +162,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)
}
@@ -1,56 +0,0 @@
package git_commands
import (
"fmt"
"strings"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/samber/lo"
)
type CommitFileLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
}
func NewCommitFileLoader(common *common.Common, cmd oscommands.ICmdObjBuilder) *CommitFileLoader {
return &CommitFileLoader{
Common: common,
cmd: cmd,
}
}
// GetFilesInDiff get the specified commit files
func (self *CommitFileLoader) GetFilesInDiff(from string, to string, reverse bool) ([]*models.CommitFile, error) {
reverseFlag := ""
if reverse {
reverseFlag = " -R "
}
filenames, err := self.cmd.New(fmt.Sprintf("git diff --submodule --no-ext-diff --name-status -z --no-renames %s %s %s", reverseFlag, from, to)).DontLog().RunWithOutput()
if err != nil {
return nil, err
}
return getCommitFilesFromFilenames(filenames), nil
}
// filenames string is something like "MM\x00file1\x00MU\x00file2\x00AA\x00file3\x00"
// so we need to split it by the null character and then map each status-name pair to a commit file
func getCommitFilesFromFilenames(filenames string) []*models.CommitFile {
lines := strings.Split(strings.TrimRight(filenames, "\x00"), "\x00")
if len(lines) == 1 {
return []*models.CommitFile{}
}
// typical result looks like 'A my_file' meaning my_file was added
return slices.Map(lo.Chunk(lines, 2), func(chunk []string) *models.CommitFile {
return &models.CommitFile{
ChangeStatus: chunk[0],
Name: chunk[1],
}
})
}
@@ -1,71 +0,0 @@
package git_commands
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/stretchr/testify/assert"
)
func TestGetCommitFilesFromFilenames(t *testing.T) {
tests := []struct {
testName string
input string
output []*models.CommitFile
}{
{
testName: "no files",
input: "",
output: []*models.CommitFile{},
},
{
testName: "one file",
input: "MM\x00Myfile\x00",
output: []*models.CommitFile{
{
Name: "Myfile",
ChangeStatus: "MM",
},
},
},
{
testName: "two files",
input: "MM\x00Myfile\x00M \x00MyOtherFile\x00",
output: []*models.CommitFile{
{
Name: "Myfile",
ChangeStatus: "MM",
},
{
Name: "MyOtherFile",
ChangeStatus: "M ",
},
},
},
{
testName: "three files",
input: "MM\x00Myfile\x00M \x00MyOtherFile\x00 M\x00YetAnother\x00",
output: []*models.CommitFile{
{
Name: "Myfile",
ChangeStatus: "MM",
},
{
Name: "MyOtherFile",
ChangeStatus: "M ",
},
{
Name: "YetAnother",
ChangeStatus: " M",
},
},
},
}
for _, test := range tests {
t.Run(test.testName, func(t *testing.T) {
result := getCommitFilesFromFilenames(test.input)
assert.Equal(t, test.output, result)
})
}
}
-454
View File
@@ -1,454 +0,0 @@
package git_commands
import (
"bytes"
"fmt"
"os"
"path/filepath"
"regexp"
"strconv"
"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/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/common"
)
// context:
// here we get the commits from git log but format them to show whether they're
// unpushed/pushed/merged into the base branch or not, or if they're yet to
// be processed as part of a rebase (these won't appear in git log but we
// grab them from the rebase-related files in the .git directory to show them
// CommitLoader returns a list of Commit objects for the current repo
type CommitLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
getCurrentBranchInfo func() (BranchInfo, error)
getRebaseMode func() (enums.RebaseMode, error)
readFile func(filename string) ([]byte, error)
walkFiles func(root string, fn filepath.WalkFunc) error
dotGitDir string
}
// making our dependencies explicit for the sake of easier testing
func NewCommitLoader(
cmn *common.Common,
cmd oscommands.ICmdObjBuilder,
dotGitDir string,
getCurrentBranchInfo func() (BranchInfo, error),
getRebaseMode func() (enums.RebaseMode, error),
) *CommitLoader {
return &CommitLoader{
Common: cmn,
cmd: cmd,
getCurrentBranchInfo: getCurrentBranchInfo,
getRebaseMode: getRebaseMode,
readFile: os.ReadFile,
walkFiles: filepath.Walk,
dotGitDir: dotGitDir,
}
}
type GetCommitsOptions struct {
Limit bool
FilterPath string
IncludeRebaseCommits bool
RefName string // e.g. "HEAD" or "my_branch"
// determines if we show the whole git graph i.e. pass the '--all' flag
All bool
}
// GetCommits obtains the commits of the current branch
func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit, error) {
commits := []*models.Commit{}
var rebasingCommits []*models.Commit
if opts.IncludeRebaseCommits && opts.FilterPath == "" {
var err error
rebasingCommits, err = self.MergeRebasingCommits(commits)
if err != nil {
return nil, err
}
commits = append(commits, rebasingCommits...)
}
passedFirstPushedCommit := false
firstPushedCommit, err := self.getFirstPushedCommit(opts.RefName)
if err != nil {
// must have no upstream branch so we'll consider everything as pushed
passedFirstPushedCommit = true
}
err = self.getLogCmd(opts).RunAndProcessLines(func(line string) (bool, error) {
commit := self.extractCommitFromLine(line)
if commit.Sha == firstPushedCommit {
passedFirstPushedCommit = true
}
commit.Status = map[bool]models.CommitStatus{true: models.StatusUnpushed, false: models.StatusPushed}[!passedFirstPushedCommit]
commits = append(commits, commit)
return false, nil
})
if err != nil {
return nil, err
}
if len(commits) == 0 {
return commits, nil
}
commits, err = self.setCommitMergedStatuses(opts.RefName, commits)
if err != nil {
return nil, err
}
return commits, nil
}
func (self *CommitLoader) MergeRebasingCommits(commits []*models.Commit) ([]*models.Commit, error) {
// 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
result = append(result, commits[i:]...)
break
}
}
rebaseMode, err := self.getRebaseMode()
if err != nil {
return nil, err
}
if rebaseMode == enums.REBASE_MODE_NONE {
// not in rebase mode so return original commits
return result, nil
}
rebasingCommits, err := self.getHydratedRebasingCommits(rebaseMode)
if err != nil {
return nil, err
}
if len(rebasingCommits) > 0 {
result = append(rebasingCommits, result...)
}
return result, nil
}
// extractCommitFromLine takes a line from a git log and extracts the sha, message, date, and tag if present
// then puts them into a commit object
// example input:
// 8ad01fe32fcc20f07bc6693f87aa4977c327f1e1|10 hours ago|Jesse Duffield| (HEAD -> master, tag: v0.15.2)|refresh commits when adding a tag
func (self *CommitLoader) extractCommitFromLine(line string) *models.Commit {
split := strings.SplitN(line, "\x00", 7)
sha := split[0]
unixTimestamp := split[1]
authorName := split[2]
authorEmail := split[3]
extraInfo := strings.TrimSpace(split[4])
parentHashes := split[5]
message := split[6]
tags := []string{}
if extraInfo != "" {
re := regexp.MustCompile(`tag: ([^,\)]+)`)
tagMatch := re.FindStringSubmatch(extraInfo)
if len(tagMatch) > 1 {
tags = append(tags, tagMatch[1])
}
}
unitTimestampInt, _ := strconv.Atoi(unixTimestamp)
parents := []string{}
if len(parentHashes) > 0 {
parents = strings.Split(parentHashes, " ")
}
return &models.Commit{
Sha: sha,
Name: message,
Tags: tags,
ExtraInfo: extraInfo,
UnixTimestamp: int64(unitTimestampInt),
AuthorName: authorName,
AuthorEmail: authorEmail,
Parents: parents,
}
}
func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode) ([]*models.Commit, error) {
commits, err := self.getRebasingCommits(rebaseMode)
if err != nil {
return nil, err
}
if len(commits) == 0 {
return nil, nil
}
commitShas := slices.FilterMap(commits, func(commit *models.Commit) (string, bool) {
return commit.Sha, commit.Sha != ""
})
// note that we're not filtering these as we do non-rebasing commits just because
// I suspect that will cause some damage
cmdObj := self.cmd.New(
fmt.Sprintf(
"git -c log.showSignature=false show %s --no-patch --oneline %s --abbrev=%d",
strings.Join(commitShas, " "),
prettyFormat,
20,
),
).DontLog()
fullCommits := map[string]*models.Commit{}
err = cmdObj.RunAndProcessLines(func(line string) (bool, error) {
commit := self.extractCommitFromLine(line)
fullCommits[commit.Sha] = commit
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
}
// getRebasingCommits obtains the commits that we're in the process of rebasing
func (self *CommitLoader) getRebasingCommits(rebaseMode enums.RebaseMode) ([]*models.Commit, error) {
switch rebaseMode {
case enums.REBASE_MODE_MERGING:
return self.getNormalRebasingCommits()
case enums.REBASE_MODE_INTERACTIVE:
return self.getInteractiveRebasingCommits()
default:
return nil, nil
}
}
func (self *CommitLoader) getNormalRebasingCommits() ([]*models.Commit, error) {
rewrittenCount := 0
bytesContent, err := self.readFile(filepath.Join(self.dotGitDir, "rebase-apply/rewritten"))
if err == nil {
content := string(bytesContent)
rewrittenCount = len(strings.Split(content, "\n"))
}
// we know we're rebasing, so lets get all the files whose names have numbers
commits := []*models.Commit{}
err = self.walkFiles(filepath.Join(self.dotGitDir, "rebase-apply"), func(path string, f os.FileInfo, err error) error {
if rewrittenCount > 0 {
rewrittenCount--
return nil
}
if err != nil {
return err
}
re := regexp.MustCompile(`^\d+$`)
if !re.MatchString(f.Name()) {
return nil
}
bytesContent, err := self.readFile(path)
if err != nil {
return err
}
content := string(bytesContent)
commit := self.commitFromPatch(content)
commits = append([]*models.Commit{commit}, commits...)
return nil
})
if err != nil {
return nil, err
}
return commits, nil
}
// git-rebase-todo example:
// pick ac446ae94ee560bdb8d1d057278657b251aaef17 ac446ae
// pick afb893148791a2fbd8091aeb81deba4930c73031 afb8931
// git-rebase-todo.backup example:
// pick 49cbba374296938ea86bbd4bf4fee2f6ba5cccf6 third commit on master
// pick ac446ae94ee560bdb8d1d057278657b251aaef17 blah commit on master
// pick afb893148791a2fbd8091aeb81deba4930c73031 fourth commit on master
// getInteractiveRebasingCommits takes our git-rebase-todo and our git-rebase-todo.backup files
// and extracts out the sha and names of commits that we still have to go
// in the rebase:
func (self *CommitLoader) getInteractiveRebasingCommits() ([]*models.Commit, error) {
bytesContent, err := self.readFile(filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo"))
if err != nil {
self.Log.Error(fmt.Sprintf("error occurred reading git-rebase-todo: %s", err.Error()))
// we assume an error means the file doesn't exist so we just return
return nil, nil
}
commits := []*models.Commit{}
todos, err := todo.Parse(bytes.NewBuffer(bytesContent))
if err != nil {
self.Log.Error(fmt.Sprintf("error occurred while parsing git-rebase-todo file: %s", err.Error()))
return nil, nil
}
for _, t := range todos {
if t.Command == todo.UpdateRef {
t.Msg = strings.TrimPrefix(t.Ref, "refs/heads/")
} else 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,
})
}
return commits, nil
}
// assuming the file starts like this:
// From e93d4193e6dd45ca9cf3a5a273d7ba6cd8b8fb20 Mon Sep 17 00:00:00 2001
// From: Lazygit Tester <test@example.com>
// Date: Wed, 5 Dec 2018 21:03:23 +1100
// Subject: second commit on master
func (self *CommitLoader) commitFromPatch(content string) *models.Commit {
lines := strings.Split(content, "\n")
sha := strings.Split(lines[0], " ")[1]
name := strings.TrimPrefix(lines[3], "Subject: ")
return &models.Commit{
Sha: sha,
Name: name,
Status: models.StatusRebasing,
}
}
func (self *CommitLoader) setCommitMergedStatuses(refName string, commits []*models.Commit) ([]*models.Commit, error) {
ancestor, err := self.getMergeBase(refName)
if err != nil {
return nil, err
}
if ancestor == "" {
return commits, nil
}
passedAncestor := false
for i, commit := range commits {
if strings.HasPrefix(ancestor, commit.Sha) {
passedAncestor = true
}
if commit.Status != models.StatusPushed {
continue
}
if passedAncestor {
commits[i].Status = models.StatusMerged
}
}
return commits, nil
}
func (self *CommitLoader) getMergeBase(refName string) (string, error) {
info, err := self.getCurrentBranchInfo()
if err != nil {
return "", err
}
baseBranch := "master"
if strings.HasPrefix(info.RefName, "feature/") {
baseBranch = "develop"
}
// swallowing error because it's not a big deal; probably because there are no commits yet
output, _ := self.cmd.New(fmt.Sprintf("git merge-base %s %s", self.cmd.Quote(refName), self.cmd.Quote(baseBranch))).DontLog().RunWithOutput()
return ignoringWarnings(output), nil
}
func ignoringWarnings(commandOutput string) string {
trimmedOutput := strings.TrimSpace(commandOutput)
split := strings.Split(trimmedOutput, "\n")
// need to get last line in case the first line is a warning about how the error is ambiguous.
// At some point we should find a way to make it unambiguous
lastLine := split[len(split)-1]
return lastLine
}
// getFirstPushedCommit returns the first commit SHA which has been pushed to the ref's upstream.
// all commits above this are deemed unpushed and marked as such.
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/"))),
).
DontLog().
RunWithOutput()
if err != nil {
return "", err
}
return ignoringWarnings(output), nil
}
// getLog gets the git log.
func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj {
limitFlag := ""
if opts.Limit {
limitFlag = " -300"
}
followFlag := ""
filterFlag := ""
if opts.FilterPath != "" {
followFlag = " --follow"
filterFlag = fmt.Sprintf(" %s", self.cmd.Quote(opts.FilterPath))
}
config := self.UserConfig.Git.Log
orderFlag := ""
if config.Order != "default" {
orderFlag = " --" + config.Order
}
allFlag := ""
if opts.All {
allFlag = " --all"
}
return self.cmd.New(
fmt.Sprintf(
"git log %s%s%s --oneline %s%s --abbrev=%d%s --no-show-signature --%s",
self.cmd.Quote(opts.RefName),
orderFlag,
allFlag,
prettyFormat,
limitFlag,
40,
followFlag,
filterFlag,
),
).DontLog()
}
const prettyFormat = `--pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s"`
@@ -1,251 +0,0 @@
package git_commands
import (
"path/filepath"
"strings"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
)
var commitsOutput = strings.Replace(`0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|jessedduffield@gmail.com| (HEAD -> better-tests)|b21997d6b4cbdf84b149|better typing for rebase mode
b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164|1640824515|Jesse Duffield|jessedduffield@gmail.com| (origin/better-tests)|e94e8fc5b6fab4cb755f|fix logging
e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c|1640823749|Jesse Duffield|jessedduffield@gmail.com||d8084cd558925eb7c9c3|refactor
d8084cd558925eb7c9c38afeed5725c21653ab90|1640821426|Jesse Duffield|jessedduffield@gmail.com||65f910ebd85283b5cce9|WIP
65f910ebd85283b5cce9bf67d03d3f1a9ea3813a|1640821275|Jesse Duffield|jessedduffield@gmail.com||26c07b1ab33860a1a759|WIP
26c07b1ab33860a1a7591a0638f9925ccf497ffa|1640750752|Jesse Duffield|jessedduffield@gmail.com||3d4470a6c072208722e5|WIP
3d4470a6c072208722e5ae9a54bcb9634959a1c5|1640748818|Jesse Duffield|jessedduffield@gmail.com||053a66a7be3da43aacdc|WIP
053a66a7be3da43aacdc7aa78e1fe757b82c4dd2|1640739815|Jesse Duffield|jessedduffield@gmail.com||985fe482e806b172aea4|refactoring the config struct`, "|", "\x00", -1)
func TestGetCommits(t *testing.T) {
type scenario struct {
testName string
runner *oscommands.FakeCmdObjRunner
expectedCommits []*models.Commit
expectedError error
logOrder string
rebaseMode enums.RebaseMode
currentBranchName string
opts GetCommitsOptions
}
scenarios := []scenario{
{
testName: "should return no commits if there are none",
logOrder: "topo-order",
rebaseMode: enums.REBASE_MODE_NONE,
currentBranchName: "master",
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),
expectedCommits: []*models.Commit{},
expectedError: nil,
},
{
testName: "should return commits if they are present",
logOrder: "topo-order",
rebaseMode: enums.REBASE_MODE_NONE,
currentBranchName: "master",
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
runner: oscommands.NewFakeRunner(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).
// 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),
expectedCommits: []*models.Commit{
{
Sha: "0eea75e8c631fba6b58135697835d58ba4c18dbc",
Name: "better typing for rebase mode",
Status: models.StatusUnpushed,
Action: models.ActionNone,
Tags: []string{},
ExtraInfo: "(HEAD -> better-tests)",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640826609,
Parents: []string{
"b21997d6b4cbdf84b149",
},
},
{
Sha: "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164",
Name: "fix logging",
Status: models.StatusPushed,
Action: models.ActionNone,
Tags: []string{},
ExtraInfo: "(origin/better-tests)",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640824515,
Parents: []string{
"e94e8fc5b6fab4cb755f",
},
},
{
Sha: "e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c",
Name: "refactor",
Status: models.StatusPushed,
Action: models.ActionNone,
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640823749,
Parents: []string{
"d8084cd558925eb7c9c3",
},
},
{
Sha: "d8084cd558925eb7c9c38afeed5725c21653ab90",
Name: "WIP",
Status: models.StatusPushed,
Action: models.ActionNone,
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640821426,
Parents: []string{
"65f910ebd85283b5cce9",
},
},
{
Sha: "65f910ebd85283b5cce9bf67d03d3f1a9ea3813a",
Name: "WIP",
Status: models.StatusPushed,
Action: models.ActionNone,
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640821275,
Parents: []string{
"26c07b1ab33860a1a759",
},
},
{
Sha: "26c07b1ab33860a1a7591a0638f9925ccf497ffa",
Name: "WIP",
Status: models.StatusMerged,
Action: models.ActionNone,
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640750752,
Parents: []string{
"3d4470a6c072208722e5",
},
},
{
Sha: "3d4470a6c072208722e5ae9a54bcb9634959a1c5",
Name: "WIP",
Status: models.StatusMerged,
Action: models.ActionNone,
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640748818,
Parents: []string{
"053a66a7be3da43aacdc",
},
},
{
Sha: "053a66a7be3da43aacdc7aa78e1fe757b82c4dd2",
Name: "refactoring the config struct",
Status: models.StatusMerged,
Action: models.ActionNone,
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640739815,
Parents: []string{
"985fe482e806b172aea4",
},
},
},
expectedError: nil,
},
{
testName: "should not specify order if `log.order` is `default`",
logOrder: "default",
rebaseMode: enums.REBASE_MODE_NONE,
currentBranchName: "master",
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),
expectedCommits: []*models.Commit{},
expectedError: nil,
},
}
for _, scenario := range scenarios {
scenario := scenario
t.Run(scenario.testName, func(t *testing.T) {
common := utils.NewDummyCommon()
common.UserConfig.Git.Log.Order = scenario.logOrder
builder := &CommitLoader{
Common: common,
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
getCurrentBranchInfo: func() (BranchInfo, error) {
return BranchInfo{RefName: scenario.currentBranchName, DisplayName: scenario.currentBranchName, DetachedHead: false}, nil
},
getRebaseMode: func() (enums.RebaseMode, error) { return scenario.rebaseMode, nil },
dotGitDir: ".git",
readFile: func(filename string) ([]byte, error) {
return []byte(""), nil
},
walkFiles: func(root string, fn filepath.WalkFunc) error {
return nil
},
}
commits, err := builder.GetCommits(scenario.opts)
assert.Equal(t, scenario.expectedCommits, commits)
assert.Equal(t, scenario.expectedError, err)
scenario.runner.CheckForMissingCalls()
})
}
}
+23 -147
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) {
@@ -47,7 +27,7 @@ func TestCommitResetToCommit(t *testing.T) {
runner.CheckForMissingCalls()
}
func TestCommitCommitCmdObj(t *testing.T) {
func TestCommitCommitObj(t *testing.T) {
type scenario struct {
testName string
message string
@@ -109,62 +89,6 @@ func TestCommitCommitCmdObj(t *testing.T) {
}
}
func TestCommitCommitEditorCmdObj(t *testing.T) {
type scenario struct {
testName string
configSignoff bool
configVerbose string
expected string
}
scenarios := []scenario{
{
testName: "Commit using editor",
configSignoff: false,
configVerbose: "default",
expected: `git commit`,
},
{
testName: "Commit with --no-verbose flag",
configSignoff: false,
configVerbose: "never",
expected: `git commit --no-verbose`,
},
{
testName: "Commit with --verbose flag",
configSignoff: false,
configVerbose: "always",
expected: `git commit --verbose`,
},
{
testName: "Commit with --signoff",
configSignoff: true,
configVerbose: "default",
expected: `git commit --signoff`,
},
{
testName: "Commit with --signoff and --no-verbose",
configSignoff: true,
configVerbose: "never",
expected: `git commit --signoff --no-verbose`,
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
userConfig := config.GetDefaultConfig()
userConfig.Git.Commit.SignOff = s.configSignoff
userConfig.Git.Commit.Verbose = s.configVerbose
instance := buildCommitCommands(commonDeps{userConfig: userConfig})
cmdStr := instance.CommitEditorCmdObj().ToString()
assert.Equal(t, s.expected, cmdStr)
})
}
}
func TestCommitCreateFixupCommit(t *testing.T) {
type scenario struct {
testName string
@@ -197,41 +121,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 +156,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 +207,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)
})
}
}
-3
View File
@@ -9,7 +9,6 @@ import (
type GitCommon struct {
*common.Common
version *GitVersion
cmd oscommands.ICmdObjBuilder
os *oscommands.OSCommand
dotGitDir string
@@ -21,7 +20,6 @@ type GitCommon struct {
func NewGitCommon(
cmn *common.Common,
version *GitVersion,
cmd oscommands.ICmdObjBuilder,
osCommand *oscommands.OSCommand,
dotGitDir string,
@@ -31,7 +29,6 @@ func NewGitCommon(
) *GitCommon {
return &GitCommon{
Common: cmn,
version: version,
cmd: cmd,
os: osCommand,
dotGitDir: dotGitDir,
+3 -11
View File
@@ -1,11 +1,10 @@
package git_commands
import (
"os"
"github.com/go-errors/errors"
gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/config"
@@ -15,7 +14,6 @@ import (
type commonDeps struct {
runner *oscommands.FakeCmdObjRunner
userConfig *config.UserConfig
gitVersion *GitVersion
gitConfig *git_config.FakeGitConfig
getenv func(string) string
removeFile func(string) error
@@ -49,11 +47,6 @@ func buildGitCommon(deps commonDeps) *GitCommon {
gitCommon.Common.UserConfig = config.GetDefaultConfig()
}
gitCommon.version = deps.gitVersion
if gitCommon.version == nil {
gitCommon.version = &GitVersion{2, 0, 0, ""}
}
gitConfig := deps.gitConfig
if gitConfig == nil {
gitConfig = git_config.NewFakeGitConfig(nil)
@@ -77,7 +70,6 @@ func buildGitCommon(deps commonDeps) *GitCommon {
GetenvFn: getenv,
Cmd: cmd,
RemoveFileFn: removeFile,
TempDir: os.TempDir(),
})
gitCommon.dotGitDir = deps.dotGitDir
@@ -94,8 +86,8 @@ func buildRepo() *gogit.Repository {
return repo
}
func buildFileLoader(gitCommon *GitCommon) *FileLoader {
return NewFileLoader(gitCommon.Common, gitCommon.cmd, gitCommon.config)
func buildFileLoader(gitCommon *GitCommon) *loaders.FileLoader {
return loaders.NewFileLoader(gitCommon.Common, gitCommon.cmd, gitCommon.config)
}
func buildSubmoduleCommands(deps commonDeps) *SubmoduleCommands {
+3 -84
View File
@@ -1,12 +1,10 @@
package git_commands
import (
"os"
"io/ioutil"
"strconv"
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -22,14 +20,14 @@ func NewFileCommands(gitCommon *GitCommon) *FileCommands {
// Cat obtains the content of a file
func (self *FileCommands) Cat(fileName string) (string, error) {
buf, err := os.ReadFile(fileName)
buf, err := ioutil.ReadFile(fileName)
if err != nil {
return "", nil
}
return string(buf), nil
}
func (self *FileCommands) GetEditCmdStrLegacy(filename string, lineNumber int) (string, error) {
func (self *FileCommands) GetEditCmdStr(filename string, lineNumber int) (string, error) {
editor := self.UserConfig.OS.EditCommand
if editor == "" {
@@ -74,82 +72,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
}
-123
View File
@@ -1,123 +0,0 @@
package git_commands
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
)
type FileLoaderConfig interface {
GetShowUntrackedFiles() string
}
type FileLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
config FileLoaderConfig
getFileType func(string) string
}
func NewFileLoader(cmn *common.Common, cmd oscommands.ICmdObjBuilder, config FileLoaderConfig) *FileLoader {
return &FileLoader{
Common: cmn,
cmd: cmd,
getFileType: oscommands.FileType,
config: config,
}
}
type GetStatusFileOptions struct {
NoRenames bool
}
func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
// check if config wants us ignoring untracked files
untrackedFilesSetting := self.config.GetShowUntrackedFiles()
if untrackedFilesSetting == "" {
untrackedFilesSetting = "all"
}
untrackedFilesArg := fmt.Sprintf("--untracked-files=%s", untrackedFilesSetting)
statuses, err := self.GitStatus(GitStatusOptions{NoRenames: opts.NoRenames, UntrackedFilesArg: untrackedFilesArg})
if err != nil {
self.Log.Error(err)
}
files := []*models.File{}
for _, status := range statuses {
if strings.HasPrefix(status.StatusString, "warning") {
self.Log.Warningf("warning when calling git status: %s", status.StatusString)
continue
}
file := &models.File{
Name: status.Name,
PreviousName: status.PreviousName,
DisplayString: status.StatusString,
Type: self.getFileType(status.Name),
}
models.SetStatusFields(file, status.Change)
files = append(files, file)
}
return files
}
// GitStatus returns the file status of the repo
type GitStatusOptions struct {
NoRenames bool
UntrackedFilesArg string
}
type FileStatus struct {
StatusString string
Change string // ??, MM, AM, ...
Name string
PreviousName string
}
func (c *FileLoader) GitStatus(opts GitStatusOptions) ([]FileStatus, error) {
noRenamesFlag := ""
if opts.NoRenames {
noRenamesFlag = " --no-renames"
}
statusLines, _, err := c.cmd.New(fmt.Sprintf("git status %s --porcelain -z%s", opts.UntrackedFilesArg, noRenamesFlag)).DontLog().RunWithOutputs()
if err != nil {
return []FileStatus{}, err
}
splitLines := strings.Split(statusLines, "\x00")
response := []FileStatus{}
for i := 0; i < len(splitLines); i++ {
original := splitLines[i]
if len(original) < 3 {
continue
}
status := FileStatus{
StatusString: original,
Change: original[:2],
Name: original[3:],
PreviousName: "",
}
if strings.HasPrefix(status.Change, "R") {
// if a line starts with 'R' then the next line is the original file.
status.PreviousName = splitLines[i+1]
status.StatusString = fmt.Sprintf("%s %s -> %s", status.Change, status.PreviousName, status.Name)
i++
}
response = append(response, status)
}
return response, nil
}
@@ -1,210 +0,0 @@
package git_commands
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
)
func TestFileGetStatusFiles(t *testing.T) {
type scenario struct {
testName string
runner oscommands.ICmdObjRunner
expectedFiles []*models.File
}
scenarios := []scenario{
{
"No files found",
oscommands.NewFakeRunner(t).
Expect(`git status --untracked-files=yes --porcelain -z`, "", nil),
[]*models.File{},
},
{
"Several files found",
oscommands.NewFakeRunner(t).
Expect(
`git status --untracked-files=yes --porcelain -z`,
"MM file1.txt\x00A file3.txt\x00AM file2.txt\x00?? file4.txt\x00UU file5.txt",
nil,
),
[]*models.File{
{
Name: "file1.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "MM file1.txt",
Type: "file",
ShortStatus: "MM",
},
{
Name: "file3.txt",
HasStagedChanges: true,
HasUnstagedChanges: false,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "A file3.txt",
Type: "file",
ShortStatus: "A ",
},
{
Name: "file2.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "AM file2.txt",
Type: "file",
ShortStatus: "AM",
},
{
Name: "file4.txt",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "?? file4.txt",
Type: "file",
ShortStatus: "??",
},
{
Name: "file5.txt",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: true,
HasInlineMergeConflicts: true,
DisplayString: "UU file5.txt",
Type: "file",
ShortStatus: "UU",
},
},
},
{
"File with new line char",
oscommands.NewFakeRunner(t).
Expect(`git status --untracked-files=yes --porcelain -z`, "MM a\nb.txt", nil),
[]*models.File{
{
Name: "a\nb.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "MM a\nb.txt",
Type: "file",
ShortStatus: "MM",
},
},
},
{
"Renamed files",
oscommands.NewFakeRunner(t).
Expect(
`git status --untracked-files=yes --porcelain -z`,
"R after1.txt\x00before1.txt\x00RM after2.txt\x00before2.txt",
nil,
),
[]*models.File{
{
Name: "after1.txt",
PreviousName: "before1.txt",
HasStagedChanges: true,
HasUnstagedChanges: false,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "R before1.txt -> after1.txt",
Type: "file",
ShortStatus: "R ",
},
{
Name: "after2.txt",
PreviousName: "before2.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "RM before2.txt -> after2.txt",
Type: "file",
ShortStatus: "RM",
},
},
},
{
"File with arrow in name",
oscommands.NewFakeRunner(t).
Expect(
`git status --untracked-files=yes --porcelain -z`,
`?? a -> b.txt`,
nil,
),
[]*models.File{
{
Name: "a -> b.txt",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "?? a -> b.txt",
Type: "file",
ShortStatus: "??",
},
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
cmd := oscommands.NewDummyCmdObjBuilder(s.runner)
loader := &FileLoader{
Common: utils.NewDummyCommon(),
cmd: cmd,
config: &FakeFileLoaderConfig{showUntrackedFiles: "yes"},
getFileType: func(string) string { return "file" },
}
assert.EqualValues(t, s.expectedFiles, loader.GetStatusFiles(GetStatusFileOptions{}))
})
}
}
type FakeFileLoaderConfig struct {
showUntrackedFiles string
}
func (self *FakeFileLoaderConfig) GetShowUntrackedFiles() string {
return self.showUntrackedFiles
}
+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())
}
}
+146
View File
@@ -0,0 +1,146 @@
package git_commands
import (
"encoding/json"
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
)
type GhCommands struct {
*GitCommon
}
func NewGhCommand(gitCommon *GitCommon) *GhCommands {
return &GhCommands{
GitCommon: gitCommon,
}
}
// https://github.com/cli/cli/issues/2300
func (self *GhCommands) BaseRepo() error {
return self.cmd.New("git config --local --get-regexp .gh-resolved").Run()
}
// Ex: git config --local --add "remote.origin.gh-resolved" "jesseduffield/lazygit"
func (self *GhCommands) SetBaseRepo(repository string) (string, error) {
return self.cmd.New(
fmt.Sprintf("git config --local --add \"remote.origin.gh-resolved\" \"%s\"", repository),
).RunWithOutput()
}
func (self *GhCommands) prList() (string, error) {
return self.cmd.New(
"gh pr list --limit 500 --state all --json state,url,number,headRefName,headRepositoryOwner",
).RunWithOutput()
}
func (self *GhCommands) GithubMostRecentPRs() ([]*models.GithubPullRequest, error) {
commandOutput, err := self.prList()
if err != nil {
return nil, err
}
prs := []*models.GithubPullRequest{}
err = json.Unmarshal([]byte(commandOutput), &prs)
if err != nil {
return nil, err
}
return prs, nil
}
func GenerateGithubPullRequestMap(prs []*models.GithubPullRequest, branches []*models.Branch, remotes []*models.Remote) map[*models.Branch]*models.GithubPullRequest {
res := map[*models.Branch]*models.GithubPullRequest{}
if len(prs) == 0 {
return res
}
remotesToOwnersMap := getRemotesToOwnersMap(remotes)
if len(remotesToOwnersMap) == 0 {
return res
}
// A PR can be identified by two things: the owner e.g. 'jesseduffield' and the
// branch name e.g. 'feature/my-feature'. The owner might be different
// to the owner of the repo if the PR is from a fork of that repo.
type prKey struct {
owner string
branchName string
}
prByKey := map[prKey]models.GithubPullRequest{}
for _, pr := range prs {
prByKey[prKey{owner: pr.UserName(), branchName: pr.BranchName()}] = *pr
}
for _, branch := range branches {
if !branch.IsTrackingRemote() {
continue
}
// TODO: support branches whose UpstreamRemote contains a full git
// URL rather than just a remote name.
owner, foundRemoteOwner := remotesToOwnersMap[branch.UpstreamRemote]
if !foundRemoteOwner {
continue
}
pr, hasPr := prByKey[prKey{owner: owner, branchName: branch.UpstreamBranch}]
if !hasPr {
continue
}
res[branch] = &pr
}
return res
}
func getRemotesToOwnersMap(remotes []*models.Remote) map[string]string {
res := map[string]string{}
for _, remote := range remotes {
if len(remote.Urls) == 0 {
continue
}
res[remote.Name] = GetRepoInfoFromURL(remote.Urls[0]).Owner
}
return res
}
type RepoInformation struct {
Owner string
Repository string
}
// TODO: move this into hosting_service.go
func GetRepoInfoFromURL(url string) RepoInformation {
isHTTP := strings.HasPrefix(url, "http")
if isHTTP {
splits := strings.Split(url, "/")
owner := strings.Join(splits[3:len(splits)-1], "/")
repo := strings.TrimSuffix(splits[len(splits)-1], ".git")
return RepoInformation{
Owner: owner,
Repository: repo,
}
}
tmpSplit := strings.Split(url, ":")
splits := strings.Split(tmpSplit[1], "/")
owner := strings.Join(splits[0:len(splits)-1], "/")
repo := strings.TrimSuffix(splits[len(splits)-1], ".git")
return RepoInformation{
Owner: owner,
Repository: repo,
}
}
@@ -0,0 +1,34 @@
package git_commands
import "github.com/jesseduffield/lazygit/pkg/commands/hosting_service"
// a hosting service is something like github, gitlab, bitbucket etc
type HostingService struct {
*GitCommon
}
func NewHostingServiceCommand(gitCommon *GitCommon) *HostingService {
return &HostingService{
GitCommon: gitCommon,
}
}
func (self *HostingService) GetPullRequestURL(from string, to string) (string, error) {
return self.getHostingServiceMgr(self.config.GetRemoteURL()).GetPullRequestURL(from, to)
}
func (self *HostingService) GetCommitURL(commitSha string) (string, error) {
return self.getHostingServiceMgr(self.config.GetRemoteURL()).GetCommitURL(commitSha)
}
func (self *HostingService) GetRepoNameFromRemoteURL(remoteURL string) (string, error) {
return self.getHostingServiceMgr(remoteURL).GetRepoName()
}
// getting this on every request rather than storing it in state in case our remoteURL changes
// from one invocation to the next. Note however that we're currently caching config
// results so we might want to invalidate the cache here if it becomes a problem.
func (self *HostingService) getHostingServiceMgr(remoteURL string) *hosting_service.HostingServiceMgr {
configServices := self.UserConfig.Services
return hosting_service.NewHostingServiceMgr(self.Log, self.Tr, remoteURL, configServices)
}
+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 -155
View File
@@ -2,17 +2,15 @@ package git_commands
import (
"fmt"
"io/ioutil"
"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,95 +129,134 @@ func (self *RebaseCommands) PrepareInteractiveRebaseCommand(opts PrepareInteract
debug = "TRUE"
}
emptyArg := " --empty=keep"
if self.version.IsOlderThan(2, 26, 0) {
emptyArg = ""
}
rebaseMergesArg := " --rebase-merges"
if self.version.IsOlderThan(2, 22, 0) {
rebaseMergesArg = ""
}
cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty%s --no-autosquash%s %s",
emptyArg, 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 := ioutil.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 ioutil.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 := ioutil.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 ioutil.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 --autostash --autosquash %s^",
sha,
),
),
)
@@ -289,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 {
@@ -345,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()
}
@@ -388,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"
}
+15 -41
View File
@@ -2,7 +2,6 @@ package git_commands
import (
"regexp"
"strconv"
"testing"
"github.com/go-errors/errors"
@@ -16,60 +15,37 @@ import (
func TestRebaseRebaseBranch(t *testing.T) {
type scenario struct {
testName string
arg string
gitVersion *GitVersion
runner *oscommands.FakeCmdObjRunner
test func(error)
testName string
arg string
runner *oscommands.FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
testName: "successful rebase",
arg: "master",
gitVersion: &GitVersion{2, 26, 0, ""},
testName: "successful rebase",
arg: "master",
runner: oscommands.NewFakeRunner(t).
Expect(`git rebase --interactive --autostash --keep-empty --empty=keep --no-autosquash --rebase-merges master`, "", nil),
Expect(`git rebase --interactive --autostash --keep-empty master`, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
testName: "unsuccessful rebase",
arg: "master",
gitVersion: &GitVersion{2, 26, 0, ""},
testName: "unsuccessful rebase",
arg: "master",
runner: oscommands.NewFakeRunner(t).
Expect(`git rebase --interactive --autostash --keep-empty --empty=keep --no-autosquash --rebase-merges master`, "", errors.New("error")),
Expect(`git rebase --interactive --autostash --keep-empty master`, "", errors.New("error")),
test: func(err error) {
assert.Error(t, err)
},
},
{
testName: "successful rebase (< 2.26.0)",
arg: "master",
gitVersion: &GitVersion{2, 25, 5, ""},
runner: oscommands.NewFakeRunner(t).
Expect(`git rebase --interactive --autostash --keep-empty --no-autosquash --rebase-merges master`, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
testName: "successful rebase (< 2.22.0)",
arg: "master",
gitVersion: &GitVersion{2, 21, 9, ""},
runner: oscommands.NewFakeRunner(t).
Expect(`git rebase --interactive --autostash --keep-empty --no-autosquash master`, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildRebaseCommands(commonDeps{runner: s.runner, gitVersion: s.gitVersion})
instance := buildRebaseCommands(commonDeps{runner: s.runner})
s.test(instance.RebaseBranch(s.arg))
})
}
@@ -86,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 {
@@ -149,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 --rebase-merges 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).
@@ -166,9 +141,8 @@ func TestRebaseDiscardOldFileChanges(t *testing.T) {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildRebaseCommands(commonDeps{
runner: s.runner,
gitVersion: &GitVersion{2, 26, 0, ""},
gitConfig: git_config.NewFakeGitConfig(s.gitConfigMockResponses),
runner: s.runner,
gitConfig: git_config.NewFakeGitConfig(s.gitConfigMockResponses),
})
s.test(instance.DiscardOldFileChanges(s.commits, s.commitIndex, s.fileName))
@@ -1,77 +0,0 @@
package git_commands
import (
"fmt"
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
)
type ReflogCommitLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
}
func NewReflogCommitLoader(common *common.Common, cmd oscommands.ICmdObjBuilder) *ReflogCommitLoader {
return &ReflogCommitLoader{
Common: common,
cmd: cmd,
}
}
// GetReflogCommits only returns the new reflog commits since the given lastReflogCommit
// if none is passed (i.e. it's value is nil) then we get all the reflog commits
func (self *ReflogCommitLoader) GetReflogCommits(lastReflogCommit *models.Commit, filterPath string) ([]*models.Commit, bool, error) {
commits := make([]*models.Commit, 0)
filterPathArg := ""
if filterPath != "" {
filterPathArg = fmt.Sprintf(" --follow -- %s", self.cmd.Quote(filterPath))
}
cmdObj := self.cmd.New(fmt.Sprintf(`git -c log.showSignature=false log -g --abbrev=40 --format="%s"%s`, "%h%x00%ct%x00%gs%x00%p", filterPathArg)).DontLog()
onlyObtainedNewReflogCommits := false
err := cmdObj.RunAndProcessLines(func(line string) (bool, error) {
fields := strings.SplitN(line, "\x00", 4)
if len(fields) <= 3 {
return false, nil
}
unixTimestamp, _ := strconv.Atoi(fields[1])
parentHashes := fields[3]
parents := []string{}
if len(parentHashes) > 0 {
parents = strings.Split(parentHashes, " ")
}
commit := &models.Commit{
Sha: fields[0],
Name: fields[2],
UnixTimestamp: int64(unixTimestamp),
Status: models.StatusReflog,
Parents: parents,
}
// note that the unix timestamp here is the timestamp of the COMMIT, not the reflog entry itself,
// so two consecutive reflog entries may have both the same SHA and therefore same timestamp.
// We use the reflog message to disambiguate, and fingers crossed that we never see the same of those
// twice in a row. Reason being that it would mean we'd be erroneously exiting early.
if lastReflogCommit != nil && commit.Sha == lastReflogCommit.Sha && commit.UnixTimestamp == lastReflogCommit.UnixTimestamp && commit.Name == lastReflogCommit.Name {
onlyObtainedNewReflogCommits = true
// after this point we already have these reflogs loaded so we'll simply return the new ones
return true, nil
}
commits = append(commits, commit)
return false, nil
})
if err != nil {
return nil, false, err
}
return commits, onlyObtainedNewReflogCommits, nil
}
@@ -1,169 +0,0 @@
package git_commands
import (
"errors"
"strings"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sanity-io/litter"
"github.com/stretchr/testify/assert"
)
var reflogOutput = strings.Replace(`c3c4b66b64c97ffeecde|1643150483|checkout: moving from A to B|51baa8c1
c3c4b66b64c97ffeecde|1643150483|checkout: moving from B to A|51baa8c1
c3c4b66b64c97ffeecde|1643150483|checkout: moving from A to B|51baa8c1
c3c4b66b64c97ffeecde|1643150483|checkout: moving from master to A|51baa8c1
f4ddf2f0d4be4ccc7efa|1643149435|checkout: moving from A to master|51baa8c1
`, "|", "\x00", -1)
func TestGetReflogCommits(t *testing.T) {
type scenario struct {
testName string
runner *oscommands.FakeCmdObjRunner
lastReflogCommit *models.Commit
filterPath string
expectedCommits []*models.Commit
expectedOnlyObtainedNew bool
expectedError error
}
scenarios := []scenario{
{
testName: "no reflog entries",
runner: oscommands.NewFakeRunner(t).
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p"`, "", nil),
lastReflogCommit: nil,
expectedCommits: []*models.Commit{},
expectedOnlyObtainedNew: false,
expectedError: nil,
},
{
testName: "some reflog entries",
runner: oscommands.NewFakeRunner(t).
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p"`, reflogOutput, nil),
lastReflogCommit: nil,
expectedCommits: []*models.Commit{
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from A to B",
Status: models.StatusReflog,
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from B to A",
Status: models.StatusReflog,
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from A to B",
Status: models.StatusReflog,
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from master to A",
Status: models.StatusReflog,
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
{
Sha: "f4ddf2f0d4be4ccc7efa",
Name: "checkout: moving from A to master",
Status: models.StatusReflog,
UnixTimestamp: 1643149435,
Parents: []string{"51baa8c1"},
},
},
expectedOnlyObtainedNew: false,
expectedError: nil,
},
{
testName: "some reflog entries where last commit is given",
runner: oscommands.NewFakeRunner(t).
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p"`, reflogOutput, nil),
lastReflogCommit: &models.Commit{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from B to A",
Status: models.StatusReflog,
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
expectedCommits: []*models.Commit{
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from A to B",
Status: models.StatusReflog,
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
},
expectedOnlyObtainedNew: true,
expectedError: nil,
},
{
testName: "when passing filterPath",
runner: oscommands.NewFakeRunner(t).
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p" --follow -- "path"`, reflogOutput, nil),
lastReflogCommit: &models.Commit{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from B to A",
Status: models.StatusReflog,
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
filterPath: "path",
expectedCommits: []*models.Commit{
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from A to B",
Status: models.StatusReflog,
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
},
expectedOnlyObtainedNew: true,
expectedError: nil,
},
{
testName: "when command returns error",
runner: oscommands.NewFakeRunner(t).
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p"`, "", errors.New("haha")),
lastReflogCommit: nil,
filterPath: "",
expectedCommits: nil,
expectedOnlyObtainedNew: false,
expectedError: errors.New("haha"),
},
}
for _, scenario := range scenarios {
scenario := scenario
t.Run(scenario.testName, func(t *testing.T) {
builder := &ReflogCommitLoader{
Common: utils.NewDummyCommon(),
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
}
commits, onlyObtainednew, err := builder.GetReflogCommits(scenario.lastReflogCommit, scenario.filterPath)
assert.Equal(t, scenario.expectedOnlyObtainedNew, onlyObtainednew)
assert.Equal(t, scenario.expectedError, err)
t.Logf("actual commits: \n%s", litter.Sdump(commits))
assert.Equal(t, scenario.expectedCommits, commits)
scenario.runner.CheckForMissingCalls()
})
}
}
@@ -1,77 +0,0 @@
package git_commands
import (
"fmt"
"regexp"
"strings"
"github.com/jesseduffield/generics/slices"
gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
)
type RemoteLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
getGoGitRemotes func() ([]*gogit.Remote, error)
}
func NewRemoteLoader(
common *common.Common,
cmd oscommands.ICmdObjBuilder,
getGoGitRemotes func() ([]*gogit.Remote, error),
) *RemoteLoader {
return &RemoteLoader{
Common: common,
cmd: cmd,
getGoGitRemotes: getGoGitRemotes,
}
}
func (self *RemoteLoader) GetRemotes() ([]*models.Remote, error) {
remoteBranchesStr, err := self.cmd.New("git branch -r").DontLog().RunWithOutput()
if err != nil {
return nil, err
}
goGitRemotes, err := self.getGoGitRemotes()
if err != nil {
return nil, err
}
// first step is to get our remotes from go-git
remotes := slices.Map(goGitRemotes, func(goGitRemote *gogit.Remote) *models.Remote {
remoteName := goGitRemote.Config().Name
re := regexp.MustCompile(fmt.Sprintf(`(?m)^\s*%s\/([\S]+)`, regexp.QuoteMeta(remoteName)))
matches := re.FindAllStringSubmatch(remoteBranchesStr, -1)
branches := slices.Map(matches, func(match []string) *models.RemoteBranch {
return &models.RemoteBranch{
Name: match[1],
RemoteName: remoteName,
}
})
return &models.Remote{
Name: goGitRemote.Config().Name,
Urls: goGitRemote.Config().URLs,
Branches: branches,
}
})
// now lets sort our remotes by name alphabetically
slices.SortFunc(remotes, func(a, b *models.Remote) bool {
// we want origin at the top because we'll be most likely to want it
if a.Name == "origin" {
return true
}
if b.Name == "origin" {
return false
}
return strings.ToLower(a.Name) < strings.ToLower(b.Name)
})
return remotes, nil
}
+4 -39
View File
@@ -2,20 +2,20 @@ package git_commands
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
)
type StashCommands struct {
*GitCommon
fileLoader *FileLoader
fileLoader *loaders.FileLoader
workingTree *WorkingTreeCommands
}
func NewStashCommands(
gitCommon *GitCommon,
fileLoader *FileLoader,
fileLoader *loaders.FileLoader,
workingTree *WorkingTreeCommands,
) *StashCommands {
return &StashCommands{
@@ -46,19 +46,6 @@ func (self *StashCommands) Save(message string) error {
return self.cmd.New("git stash save " + self.cmd.Quote(message)).Run()
}
func (self *StashCommands) Store(sha string, message string) error {
trimmedMessage := strings.Trim(message, " \t")
if len(trimmedMessage) > 0 {
return self.cmd.New(fmt.Sprintf("git stash store %s -m %s", self.cmd.Quote(sha), self.cmd.Quote(trimmedMessage))).Run()
}
return self.cmd.New(fmt.Sprintf("git stash store %s", self.cmd.Quote(sha))).Run()
}
func (self *StashCommands) Sha(index int) (string, error) {
sha, _, err := self.cmd.New(fmt.Sprintf("git rev-parse refs/stash@{%d}", index)).DontLog().RunWithOutputs()
return strings.Trim(sha, "\r\n"), err
}
func (self *StashCommands) ShowStashEntryCmdObj(index int) oscommands.ICmdObj {
cmdStr := fmt.Sprintf("git stash show -p --stat --color=%s --unified=%d stash@{%d}", self.UserConfig.Git.Paging.ColorArg, self.UserConfig.Git.DiffContextSize, index)
@@ -110,7 +97,7 @@ func (self *StashCommands) SaveStagedChanges(message string) error {
// meaning it's deleted in your working tree but added in your index. Given that it's
// now safely stashed, we need to remove it.
files := self.fileLoader.
GetStatusFiles(GetStatusFileOptions{})
GetStatusFiles(loaders.GetStatusFileOptions{})
for _, file := range files {
if file.ShortStatus == "AD" {
@@ -122,25 +109,3 @@ func (self *StashCommands) SaveStagedChanges(message string) error {
return nil
}
func (self *StashCommands) StashIncludeUntrackedChanges(message string) error {
return self.cmd.New(fmt.Sprintf("git stash save %s --include-untracked", self.cmd.Quote(message))).Run()
}
func (self *StashCommands) Rename(index int, message string) error {
sha, err := self.Sha(index)
if err != nil {
return err
}
if err := self.Drop(index); err != nil {
return err
}
err = self.Store(sha, message)
if err != nil {
return err
}
return nil
}
-79
View File
@@ -1,79 +0,0 @@
package git_commands
import (
"regexp"
"strconv"
"strings"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type StashLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
}
func NewStashLoader(
common *common.Common,
cmd oscommands.ICmdObjBuilder,
) *StashLoader {
return &StashLoader{
Common: common,
cmd: cmd,
}
}
func (self *StashLoader) GetStashEntries(filterPath string) []*models.StashEntry {
if filterPath == "" {
return self.getUnfilteredStashEntries()
}
rawString, err := self.cmd.New("git stash list --name-only").DontLog().RunWithOutput()
if err != nil {
return self.getUnfilteredStashEntries()
}
stashEntries := []*models.StashEntry{}
var currentStashEntry *models.StashEntry
lines := utils.SplitLines(rawString)
isAStash := func(line string) bool { return strings.HasPrefix(line, "stash@{") }
re := regexp.MustCompile(`stash@\{(\d+)\}`)
outer:
for i := 0; i < len(lines); i++ {
if !isAStash(lines[i]) {
continue
}
match := re.FindStringSubmatch(lines[i])
idx, err := strconv.Atoi(match[1])
if err != nil {
return self.getUnfilteredStashEntries()
}
currentStashEntry = self.stashEntryFromLine(lines[i], idx)
for i+1 < len(lines) && !isAStash(lines[i+1]) {
i++
if lines[i] == filterPath {
stashEntries = append(stashEntries, currentStashEntry)
continue outer
}
}
}
return stashEntries
}
func (self *StashLoader) getUnfilteredStashEntries() []*models.StashEntry {
rawString, _ := self.cmd.New("git stash list -z --pretty='%gs'").DontLog().RunWithOutput()
return slices.MapWithIndex(utils.SplitNul(rawString), func(line string, index int) *models.StashEntry {
return self.stashEntryFromLine(line, index)
})
}
func (c *StashLoader) stashEntryFromLine(line string, index int) *models.StashEntry {
return &models.StashEntry{
Name: line,
Index: index,
}
}
@@ -1,60 +0,0 @@
package git_commands
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
)
func TestGetStashEntries(t *testing.T) {
type scenario struct {
testName string
filterPath string
runner oscommands.ICmdObjRunner
expectedStashEntries []*models.StashEntry
}
scenarios := []scenario{
{
"No stash entries found",
"",
oscommands.NewFakeRunner(t).
Expect(`git stash list -z --pretty='%gs'`, "", nil),
[]*models.StashEntry{},
},
{
"Several stash entries found",
"",
oscommands.NewFakeRunner(t).
Expect(
`git stash list -z --pretty='%gs'`,
"WIP on add-pkg-commands-test: 55c6af2 increase parallel build\x00WIP on master: bb86a3f update github template\x00",
nil,
),
[]*models.StashEntry{
{
Index: 0,
Name: "WIP on add-pkg-commands-test: 55c6af2 increase parallel build",
},
{
Index: 1,
Name: "WIP on master: bb86a3f update github template",
},
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
cmd := oscommands.NewDummyCmdObjBuilder(s.runner)
loader := NewStashLoader(utils.NewDummyCommon(), cmd)
assert.EqualValues(t, s.expectedStashEntries, loader.GetStashEntries(s.filterPath))
})
}
}
+1 -101
View File
@@ -10,7 +10,7 @@ import (
func TestStashDrop(t *testing.T) {
runner := oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"stash", "drop", "stash@{1}"}, "Dropped refs/stash@{1} (98e9cca532c37c766107093010c72e26f2c24c04)\n", nil)
ExpectGitArgs([]string{"stash", "drop", "stash@{1}"}, "", nil)
instance := buildStashCommands(commonDeps{runner: runner})
assert.NoError(t, instance.Drop(1))
@@ -44,59 +44,6 @@ func TestStashSave(t *testing.T) {
runner.CheckForMissingCalls()
}
func TestStashStore(t *testing.T) {
type scenario struct {
testName string
sha string
message string
expected []string
}
scenarios := []scenario{
{
testName: "Non-empty message",
sha: "0123456789abcdef",
message: "New stash name",
expected: []string{"stash", "store", "0123456789abcdef", "-m", "New stash name"},
},
{
testName: "Empty message",
sha: "0123456789abcdef",
message: "",
expected: []string{"stash", "store", "0123456789abcdef"},
},
{
testName: "Space message",
sha: "0123456789abcdef",
message: " ",
expected: []string{"stash", "store", "0123456789abcdef"},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
runner := oscommands.NewFakeRunner(t).
ExpectGitArgs(s.expected, "", nil)
instance := buildStashCommands(commonDeps{runner: runner})
assert.NoError(t, instance.Store(s.sha, s.message))
runner.CheckForMissingCalls()
})
}
}
func TestStashSha(t *testing.T) {
runner := oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"rev-parse", "refs/stash@{5}"}, "14d94495194651adfd5f070590df566c11d28243\n", nil)
instance := buildStashCommands(commonDeps{runner: runner})
sha, err := instance.Sha(5)
assert.NoError(t, err)
assert.Equal(t, "14d94495194651adfd5f070590df566c11d28243", sha)
runner.CheckForMissingCalls()
}
func TestStashStashEntryCmdObj(t *testing.T) {
type scenario struct {
testName string
@@ -132,50 +79,3 @@ func TestStashStashEntryCmdObj(t *testing.T) {
})
}
}
func TestStashRename(t *testing.T) {
type scenario struct {
testName string
index int
message string
expectedShaCmd []string
shaResult string
expectedDropCmd []string
expectedStoreCmd []string
}
scenarios := []scenario{
{
testName: "Default case",
index: 3,
message: "New message",
expectedShaCmd: []string{"rev-parse", "refs/stash@{3}"},
shaResult: "f0d0f20f2f61ffd6d6bfe0752deffa38845a3edd\n",
expectedDropCmd: []string{"stash", "drop", "stash@{3}"},
expectedStoreCmd: []string{"stash", "store", "f0d0f20f2f61ffd6d6bfe0752deffa38845a3edd", "-m", "New message"},
},
{
testName: "Empty message",
index: 4,
message: "",
expectedShaCmd: []string{"rev-parse", "refs/stash@{4}"},
shaResult: "f0d0f20f2f61ffd6d6bfe0752deffa38845a3edd\n",
expectedDropCmd: []string{"stash", "drop", "stash@{4}"},
expectedStoreCmd: []string{"stash", "store", "f0d0f20f2f61ffd6d6bfe0752deffa38845a3edd"},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
runner := oscommands.NewFakeRunner(t).
ExpectGitArgs(s.expectedShaCmd, s.shaResult, nil).
ExpectGitArgs(s.expectedDropCmd, "", nil).
ExpectGitArgs(s.expectedStoreCmd, "", nil)
instance := buildStashCommands(commonDeps{runner: runner})
err := instance.Rename(s.index, s.message)
assert.NoError(t, err)
})
}
}
+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()
}
-55
View File
@@ -1,55 +0,0 @@
package git_commands
import (
"regexp"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type TagLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
}
func NewTagLoader(
common *common.Common,
cmd oscommands.ICmdObjBuilder,
) *TagLoader {
return &TagLoader{
Common: common,
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()
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]
}
return &models.Tag{
Name: tagName,
Message: message,
}
})
return tags, nil
}
@@ -1,62 +0,0 @@
package git_commands
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
)
const tagsOutput = `tag1 this is my message
tag2
tag3 this is my other message
`
func TestGetTags(t *testing.T) {
type scenario struct {
testName string
runner *oscommands.FakeCmdObjRunner
expectedTags []*models.Tag
expectedError error
}
scenarios := []scenario{
{
testName: "should return no tags if there are none",
runner: oscommands.NewFakeRunner(t).
Expect(`git tag --list -n --sort=-creatordate`, "", nil),
expectedTags: []*models.Tag{},
expectedError: nil,
},
{
testName: "should return tags if present",
runner: oscommands.NewFakeRunner(t).
Expect(`git tag --list -n --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"},
},
expectedError: nil,
},
}
for _, scenario := range scenarios {
scenario := scenario
t.Run(scenario.testName, func(t *testing.T) {
loader := &TagLoader{
Common: utils.NewDummyCommon(),
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
}
tags, err := loader.GetTags()
assert.Equal(t, scenario.expectedTags, tags)
assert.Equal(t, scenario.expectedError, err)
scenario.runner.CheckForMissingCalls()
})
}
}
-71
View File
@@ -1,71 +0,0 @@
package git_commands
import (
"errors"
"regexp"
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
)
type GitVersion struct {
Major, Minor, Patch int
Additional string
}
func GetGitVersion(osCommand *oscommands.OSCommand) (*GitVersion, error) {
versionStr, _, err := osCommand.Cmd.New("git --version").RunWithOutputs()
if err != nil {
return nil, err
}
version, err := ParseGitVersion(versionStr)
if err != nil {
return nil, err
}
return version, nil
}
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+)?(.*)`)
matches := re.FindStringSubmatch(versionStr)
if len(matches) < 5 {
return nil, errors.New("unexpected git version format: " + versionStr)
}
v := &GitVersion{}
var err error
if v.Major, err = strconv.Atoi(matches[1]); err != nil {
return nil, err
}
if len(matches[2]) > 1 {
if v.Minor, err = strconv.Atoi(matches[2][1:]); err != nil {
return nil, err
}
}
if len(matches[3]) > 1 {
if v.Patch, err = strconv.Atoi(matches[3][1:]); err != nil {
return nil, err
}
}
v.Additional = strings.Trim(matches[4], " \r\n")
return v, nil
}
func (v *GitVersion) IsOlderThan(major, minor, patch int) bool {
actual := v.Major*1000*1000 + v.Minor*1000 + v.Patch
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)
}
-47
View File
@@ -1,47 +0,0 @@
package git_commands
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseGitVersion(t *testing.T) {
scenarios := []struct {
input string
expected GitVersion
}{
{
input: "git version 2.39.0",
expected: GitVersion{Major: 2, Minor: 39, Patch: 0, Additional: ""},
},
{
input: "git version 2.37.1 (Apple Git-137.1)",
expected: GitVersion{Major: 2, Minor: 37, Patch: 1, Additional: "(Apple Git-137.1)"},
},
{
input: "git version 2.37 (Apple Git-137.1)",
expected: GitVersion{Major: 2, Minor: 37, Patch: 0, Additional: "(Apple Git-137.1)"},
},
}
for _, s := range scenarios {
actual, err := ParseGitVersion(s.input)
assert.NoError(t, err)
assert.NotNil(t, actual)
assert.Equal(t, s.expected.Major, actual.Major)
assert.Equal(t, s.expected.Minor, actual.Minor)
assert.Equal(t, s.expected.Patch, actual.Patch)
assert.Equal(t, s.expected.Additional, actual.Additional)
}
}
func TestGitVersionIsOlderThan(t *testing.T) {
assert.False(t, (&GitVersion{2, 0, 0, ""}).IsOlderThan(1, 99, 99))
assert.False(t, (&GitVersion{2, 0, 0, ""}).IsOlderThan(2, 0, 0))
assert.False(t, (&GitVersion{2, 1, 0, ""}).IsOlderThan(2, 0, 9))
assert.True(t, (&GitVersion{2, 0, 1, ""}).IsOlderThan(2, 1, 0))
assert.True(t, (&GitVersion{2, 0, 1, ""}).IsOlderThan(3, 0, 0))
}
+10 -18
View File
@@ -9,6 +9,7 @@ import (
"github.com/go-errors/errors"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
@@ -17,13 +18,13 @@ import (
type WorkingTreeCommands struct {
*GitCommon
submodule *SubmoduleCommands
fileLoader *FileLoader
fileLoader *loaders.FileLoader
}
func NewWorkingTreeCommands(
gitCommon *GitCommon,
submodule *SubmoduleCommands,
fileLoader *FileLoader,
fileLoader *loaders.FileLoader,
) *WorkingTreeCommands {
return &WorkingTreeCommands{
GitCommon: gitCommon,
@@ -89,7 +90,7 @@ func (self *WorkingTreeCommands) BeforeAndAfterFileForRename(file *models.File)
// all files, passing the --no-renames flag and then recursively call the function
// again for the before file and after file.
filesWithoutRenames := self.fileLoader.GetStatusFiles(GetStatusFileOptions{NoRenames: true})
filesWithoutRenames := self.fileLoader.GetStatusFiles(loaders.GetStatusFileOptions{NoRenames: true})
var beforeFile *models.File
var afterFile *models.File
@@ -240,7 +241,7 @@ func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain
if cached {
cachedArg = " --cached"
}
if !node.GetIsTracked() && !node.GetHasStagedChanges() && !cached && node.GetIsFile() {
if !node.GetIsTracked() && !node.GetHasStagedChanges() && !cached {
trackedArg = "--no-index -- /dev/null"
}
if plain {
@@ -287,15 +288,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 +304,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()
}
+22 -36
View File
@@ -2,7 +2,7 @@ package git_commands
import (
"fmt"
"os"
"io/ioutil"
"regexp"
"testing"
@@ -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()
@@ -446,7 +432,7 @@ func TestWorkingTreeApplyPatch(t *testing.T) {
filename := matches[1]
content, err := os.ReadFile(filename)
content, err := ioutil.ReadFile(filename)
assert.NoError(t, err)
assert.Equal(t, "test", string(content))
+2 -2
View File
@@ -3,7 +3,7 @@ package git_config
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os/exec"
"strings"
"syscall"
@@ -38,7 +38,7 @@ import (
func runGitConfigCmd(cmd *exec.Cmd) (string, error) {
var stdout bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = io.Discard
cmd.Stderr = ioutil.Discard
err := cmd.Run()
if exitError, ok := err.(*exec.ExitError); ok {
-2
View File
@@ -8,7 +8,6 @@ import (
"github.com/go-errors/errors"
gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
@@ -219,7 +218,6 @@ func TestNewGitCommand(t *testing.T) {
s.setup()
s.test(
NewGitCommand(utils.NewDummyCommon(),
&git_commands.GitVersion{},
oscommands.NewDummyOSCommand(),
git_config.NewFakeGitConfig(nil),
&deadlock.Mutex{},
+14 -18
View File
@@ -4,9 +4,13 @@ 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}}"
var (
defaultRepoURLTemplate = "https://{{.webDomain}}/{{.owner}}/{{.repo}}"
defaultRepoNameTemplate = "{{.owner}}/{{.repo}}"
)
// we've got less type safety using go templates but this lends itself better to
// users adding custom service definitions in their config
@@ -17,6 +21,7 @@ var githubServiceDef = ServiceDefinition{
commitURL: "/commit/{{.CommitSha}}",
regexStrings: defaultUrlRegexStrings,
repoURLTemplate: defaultRepoURLTemplate,
repoNameTemplate: defaultRepoNameTemplate,
}
var bitbucketServiceDef = ServiceDefinition{
@@ -28,7 +33,8 @@ var bitbucketServiceDef = ServiceDefinition{
`^(?:https?|ssh)://.*/(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
`^.*@.*:(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
},
repoURLTemplate: defaultRepoURLTemplate,
repoURLTemplate: defaultRepoURLTemplate,
repoNameTemplate: defaultRepoNameTemplate,
}
var gitLabServiceDef = ServiceDefinition{
@@ -38,6 +44,7 @@ var gitLabServiceDef = ServiceDefinition{
commitURL: "/commit/{{.CommitSha}}",
regexStrings: defaultUrlRegexStrings,
repoURLTemplate: defaultRepoURLTemplate,
repoNameTemplate: defaultRepoNameTemplate,
}
var azdoServiceDef = ServiceDefinition{
@@ -50,6 +57,8 @@ var azdoServiceDef = ServiceDefinition{
`^https://.*@dev.azure.com/(?P<org>.*?)/(?P<project>.*?)/_git/(?P<repo>.*?)(?:\.git)?$`,
},
repoURLTemplate: "https://{{.webDomain}}/{{.org}}/{{.project}}/_git/{{.repo}}",
// TODO: verify this is actually correct
repoNameTemplate: "{{.org}}/{{.project}}/{{.repo}}",
}
var bitbucketServerServiceDef = ServiceDefinition{
@@ -62,15 +71,8 @@ var bitbucketServerServiceDef = ServiceDefinition{
`^https://.*/scm/(?P<project>.*)/(?P<repo>.*?)(?:\.git)?$`,
},
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,
// TODO: verify this is actually correct
repoNameTemplate: "{{.project}}/{{.repo}}",
}
var serviceDefinitions = []ServiceDefinition{
@@ -79,7 +81,6 @@ var serviceDefinitions = []ServiceDefinition{
gitLabServiceDef,
azdoServiceDef,
bitbucketServerServiceDef,
giteaServiceDef,
}
var defaultServiceDomains = []ServiceDomain{
@@ -103,9 +104,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.
@@ -61,6 +61,18 @@ func (self *HostingServiceMgr) GetCommitURL(commitSha string) (string, error) {
return pullRequestURL, nil
}
// e.g. 'jesseduffield/lazygit'
func (self *HostingServiceMgr) GetRepoName() (string, error) {
gitService, err := self.getService()
if err != nil {
return "", err
}
repoName := gitService.repoName
return repoName, nil
}
func (self *HostingServiceMgr) getService() (*Service, error) {
serviceDomain, err := self.getServiceDomain(self.remoteURL)
if err != nil {
@@ -72,8 +84,14 @@ func (self *HostingServiceMgr) getService() (*Service, error) {
return nil, err
}
repoName, err := serviceDomain.serviceDefinition.getRepoNameFromRemoteURL(self.remoteURL)
if err != nil {
return nil, err
}
return &Service{
repoURL: repoURL,
repoName: repoName,
ServiceDefinition: serviceDomain.serviceDefinition,
}, nil
}
@@ -147,24 +165,45 @@ type ServiceDefinition struct {
regexStrings []string
// can expect 'webdomain' to be passed in. Otherwise, you get to pick what we match in the regex
repoURLTemplate string
repoURLTemplate string
repoNameTemplate string
}
func (self ServiceDefinition) getRepoURLFromRemoteURL(url string, webDomain string) (string, error) {
matches, err := self.parseRemoteUrl(url)
if err != nil {
return "", err
}
matches["webDomain"] = webDomain
return utils.ResolvePlaceholderString(self.repoURLTemplate, matches), nil
}
func (self ServiceDefinition) getRepoNameFromRemoteURL(url string) (string, error) {
matches, err := self.parseRemoteUrl(url)
if err != nil {
return "", err
}
return utils.ResolvePlaceholderString(self.repoNameTemplate, matches), nil
}
func (self ServiceDefinition) parseRemoteUrl(url string) (map[string]string, error) {
for _, regexStr := range self.regexStrings {
re := regexp.MustCompile(regexStr)
input := utils.FindNamedMatches(re, url)
if input != nil {
input["webDomain"] = webDomain
return utils.ResolvePlaceholderString(self.repoURLTemplate, input), nil
matches := utils.FindNamedMatches(re, url)
if matches != nil {
return matches, nil
}
}
return "", errors.New("Failed to parse repo information from url")
return nil, errors.New("Failed to parse repo information from url")
}
type Service struct {
repoURL string
// e.g. 'jesseduffield/lazygit'
repoName string
ServiceDefinition
}
@@ -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)
+204
View File
@@ -0,0 +1,204 @@
package loaders
import (
"regexp"
"strings"
"github.com/jesseduffield/generics/set"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/go-git/v5/config"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// context:
// we want to only show 'safe' branches (ones that haven't e.g. been deleted)
// which `git branch -a` gives us, but we also want the recency data that
// git reflog gives us.
// So we get the HEAD, then append get the reflog branches that intersect with
// our safe branches, then add the remaining safe branches, ensuring uniqueness
// along the way
// if we find out we need to use one of these functions in the git.go file, we
// can just pull them out of here and put them there and then call them from in here
type BranchLoaderConfigCommands interface {
Branches() (map[string]*config.Branch, error)
}
// BranchLoader returns a list of Branch objects for the current repo
type BranchLoader struct {
*common.Common
getRawBranches func() (string, error)
getCurrentBranchName func() (string, string, error)
config BranchLoaderConfigCommands
}
func NewBranchLoader(
cmn *common.Common,
getRawBranches func() (string, error),
getCurrentBranchName func() (string, string, error),
config BranchLoaderConfigCommands,
) *BranchLoader {
return &BranchLoader{
Common: cmn,
getRawBranches: getRawBranches,
getCurrentBranchName: getCurrentBranchName,
config: config,
}
}
// Load the list of branches for the current repo
func (self *BranchLoader) Load(reflogCommits []*models.Commit) ([]*models.Branch, error) {
branches := self.obtainBranches()
reflogBranches := self.obtainReflogBranches(reflogCommits)
// loop through reflog branches. If there is a match, merge them, then remove it from the branches and keep it in the reflog branches
branchesWithRecency := make([]*models.Branch, 0)
outer:
for _, reflogBranch := range reflogBranches {
for j, branch := range branches {
if branch.Head {
continue
}
if strings.EqualFold(reflogBranch.Name, branch.Name) {
branch.Recency = reflogBranch.Recency
branchesWithRecency = append(branchesWithRecency, branch)
branches = slices.Remove(branches, j)
continue outer
}
}
}
branches = slices.Prepend(branches, branchesWithRecency...)
foundHead := false
for i, branch := range branches {
if branch.Head {
foundHead = true
branch.Recency = " *"
branches = slices.Move(branches, i, 0)
break
}
}
if !foundHead {
currentBranchName, currentBranchDisplayName, err := self.getCurrentBranchName()
if err != nil {
return nil, err
}
branches = slices.Prepend(branches, &models.Branch{Name: currentBranchName, DisplayName: currentBranchDisplayName, Head: true, Recency: " *"})
}
configBranches, err := self.config.Branches()
if err != nil {
return nil, err
}
for _, branch := range branches {
match := configBranches[branch.Name]
if match != nil {
branch.UpstreamRemote = match.Remote
branch.UpstreamBranch = match.Merge.Short()
}
}
return branches, nil
}
func (self *BranchLoader) obtainBranches() []*models.Branch {
output, err := self.getRawBranches()
if err != nil {
panic(err)
}
trimmedOutput := strings.TrimSpace(output)
outputLines := strings.Split(trimmedOutput, "\n")
return slices.FilterMap(outputLines, func(line string) (*models.Branch, bool) {
if line == "" {
return nil, false
}
split := strings.Split(line, "\x00")
if len(split) != 4 {
// Ignore line if it isn't separated into 4 parts
// This is probably a warning message, for more info see:
// https://github.com/jesseduffield/lazygit/issues/1385#issuecomment-885580439
return nil, false
}
return obtainBranch(split), true
})
}
// Obtain branch information from parsed line output of getRawBranches()
// split contains the '|' separated tokens in the line of output
func obtainBranch(split []string) *models.Branch {
name := strings.TrimPrefix(split[1], "heads/")
branch := &models.Branch{
Name: name,
Pullables: "?",
Pushables: "?",
Head: split[0] == "*",
}
upstreamName := split[2]
if upstreamName == "" {
// if we're here then it means we do not have a local version of the remote.
// The branch might still be tracking a remote though, we just don't know
// how many commits ahead/behind it is
return branch
}
track := split[3]
if track == "[gone]" {
branch.UpstreamGone = true
} else {
re := regexp.MustCompile(`ahead (\d+)`)
match := re.FindStringSubmatch(track)
if len(match) > 1 {
branch.Pushables = match[1]
} else {
branch.Pushables = "0"
}
re = regexp.MustCompile(`behind (\d+)`)
match = re.FindStringSubmatch(track)
if len(match) > 1 {
branch.Pullables = match[1]
} else {
branch.Pullables = "0"
}
}
return branch
}
// TODO: only look at the new reflog commits, and otherwise store the recencies in
// int form against the branch to recalculate the time ago
func (self *BranchLoader) obtainReflogBranches(reflogCommits []*models.Commit) []*models.Branch {
foundBranches := set.New[string]()
re := regexp.MustCompile(`checkout: moving from ([\S]+) to ([\S]+)`)
reflogBranches := make([]*models.Branch, 0, len(reflogCommits))
for _, commit := range reflogCommits {
match := re.FindStringSubmatch(commit.Name)
if len(match) != 3 {
continue
}
recency := utils.UnixToTimeAgo(commit.UnixTimestamp)
for _, branchName := range match[1:] {
if !foundBranches.Includes(branchName) {
foundBranches.Add(branchName)
reflogBranches = append(reflogBranches, &models.Branch{
Recency: recency,
Name: branchName,
})
}
}
}
return reflogBranches
}
+52
View File
@@ -0,0 +1,52 @@
package loaders
// "*|feat/detect-purge|origin/feat/detect-purge|[ahead 1]"
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/stretchr/testify/assert"
)
func TestObtainBanch(t *testing.T) {
type scenario struct {
testName string
input []string
expectedBranch *models.Branch
}
scenarios := []scenario{
{
testName: "TrimHeads",
input: []string{"", "heads/a_branch", "", ""},
expectedBranch: &models.Branch{Name: "a_branch", Pushables: "?", Pullables: "?", Head: false},
},
{
testName: "NoUpstream",
input: []string{"", "a_branch", "", ""},
expectedBranch: &models.Branch{Name: "a_branch", Pushables: "?", Pullables: "?", Head: false},
},
{
testName: "IsHead",
input: []string{"*", "a_branch", "", ""},
expectedBranch: &models.Branch{Name: "a_branch", Pushables: "?", Pullables: "?", Head: true},
},
{
testName: "IsBehindAndAhead",
input: []string{"", "a_branch", "a_remote/a_branch", "[behind 2, ahead 3]"},
expectedBranch: &models.Branch{Name: "a_branch", Pushables: "3", Pullables: "2", Head: false},
},
{
testName: "RemoteBranchIsGone",
input: []string{"", "a_branch", "a_remote/a_branch", "[gone]"},
expectedBranch: &models.Branch{Name: "a_branch", UpstreamGone: true, Pushables: "?", Pullables: "?", Head: false},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
branch := obtainBranch(s.input)
assert.EqualValues(t, s.expectedBranch, branch)
})
}
}
+56
View File
@@ -0,0 +1,56 @@
package loaders
import (
"fmt"
"strings"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/samber/lo"
)
type CommitFileLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
}
func NewCommitFileLoader(common *common.Common, cmd oscommands.ICmdObjBuilder) *CommitFileLoader {
return &CommitFileLoader{
Common: common,
cmd: cmd,
}
}
// GetFilesInDiff get the specified commit files
func (self *CommitFileLoader) GetFilesInDiff(from string, to string, reverse bool) ([]*models.CommitFile, error) {
reverseFlag := ""
if reverse {
reverseFlag = " -R "
}
filenames, err := self.cmd.New(fmt.Sprintf("git diff --submodule --no-ext-diff --name-status -z --no-renames %s %s %s", reverseFlag, from, to)).DontLog().RunWithOutput()
if err != nil {
return nil, err
}
return getCommitFilesFromFilenames(filenames), nil
}
// filenames string is something like "MM\x00file1\x00MU\x00file2\x00AA\x00file3\x00"
// so we need to split it by the null character and then map each status-name pair to a commit file
func getCommitFilesFromFilenames(filenames string) []*models.CommitFile {
lines := strings.Split(strings.TrimRight(filenames, "\x00"), "\x00")
if len(lines) == 1 {
return []*models.CommitFile{}
}
// typical result looks like 'A my_file' meaning my_file was added
return slices.Map(lo.Chunk(lines, 2), func(chunk []string) *models.CommitFile {
return &models.CommitFile{
ChangeStatus: chunk[0],
Name: chunk[1],
}
})
}
+71
View File
@@ -0,0 +1,71 @@
package loaders
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/stretchr/testify/assert"
)
func TestGetCommitFilesFromFilenames(t *testing.T) {
tests := []struct {
testName string
input string
output []*models.CommitFile
}{
{
testName: "no files",
input: "",
output: []*models.CommitFile{},
},
{
testName: "one file",
input: "MM\x00Myfile\x00",
output: []*models.CommitFile{
{
Name: "Myfile",
ChangeStatus: "MM",
},
},
},
{
testName: "two files",
input: "MM\x00Myfile\x00M \x00MyOtherFile\x00",
output: []*models.CommitFile{
{
Name: "Myfile",
ChangeStatus: "MM",
},
{
Name: "MyOtherFile",
ChangeStatus: "M ",
},
},
},
{
testName: "three files",
input: "MM\x00Myfile\x00M \x00MyOtherFile\x00 M\x00YetAnother\x00",
output: []*models.CommitFile{
{
Name: "Myfile",
ChangeStatus: "MM",
},
{
Name: "MyOtherFile",
ChangeStatus: "M ",
},
{
Name: "YetAnother",
ChangeStatus: " M",
},
},
},
}
for _, test := range tests {
t.Run(test.testName, func(t *testing.T) {
result := getCommitFilesFromFilenames(test.input)
assert.Equal(t, test.output, result)
})
}
}
+460
View File
@@ -0,0 +1,460 @@
package loaders
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strconv"
"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/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/gui/style"
)
// context:
// here we get the commits from git log but format them to show whether they're
// unpushed/pushed/merged into the base branch or not, or if they're yet to
// be processed as part of a rebase (these won't appear in git log but we
// grab them from the rebase-related files in the .git directory to show them
// CommitLoader returns a list of Commit objects for the current repo
type CommitLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
getCurrentBranchName func() (string, string, error)
getRebaseMode func() (enums.RebaseMode, error)
readFile func(filename string) ([]byte, error)
walkFiles func(root string, fn filepath.WalkFunc) error
dotGitDir string
}
// making our dependencies explicit for the sake of easier testing
func NewCommitLoader(
cmn *common.Common,
cmd oscommands.ICmdObjBuilder,
dotGitDir string,
getCurrentBranchName func() (string, string, error),
getRebaseMode func() (enums.RebaseMode, error),
) *CommitLoader {
return &CommitLoader{
Common: cmn,
cmd: cmd,
getCurrentBranchName: getCurrentBranchName,
getRebaseMode: getRebaseMode,
readFile: ioutil.ReadFile,
walkFiles: filepath.Walk,
dotGitDir: dotGitDir,
}
}
type GetCommitsOptions struct {
Limit bool
FilterPath string
IncludeRebaseCommits bool
RefName string // e.g. "HEAD" or "my_branch"
// determines if we show the whole git graph i.e. pass the '--all' flag
All bool
}
// GetCommits obtains the commits of the current branch
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
rebasingCommits, err = self.MergeRebasingCommits(commits)
if err != nil {
return nil, err
}
commits = append(commits, rebasingCommits...)
}
passedFirstPushedCommit := false
firstPushedCommit, err := self.getFirstPushedCommit(opts.RefName)
if err != nil {
// must have no upstream branch so we'll consider everything as pushed
passedFirstPushedCommit = true
}
err = self.getLogCmd(opts).RunAndProcessLines(func(line string) (bool, error) {
commit := self.extractCommitFromLine(line)
if commit.Sha == firstPushedCommit {
passedFirstPushedCommit = true
}
commit.Status = map[bool]string{true: "unpushed", false: "pushed"}[!passedFirstPushedCommit]
commits = append(commits, commit)
return false, nil
})
if err != nil {
return nil, err
}
if len(commits) == 0 {
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
}
return commits, nil
}
func (self *CommitLoader) MergeRebasingCommits(commits []*models.Commit) ([]*models.Commit, error) {
// 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.Status != "rebasing" { // removing the existing rebase commits so we can add the refreshed ones
result = append(result, commits[i:]...)
break
}
}
rebaseMode, err := self.getRebaseMode()
if err != nil {
return nil, err
}
if rebaseMode == enums.REBASE_MODE_NONE {
// not in rebase mode so return original commits
return result, nil
}
rebasingCommits, err := self.getHydratedRebasingCommits(rebaseMode)
if err != nil {
return nil, err
}
if len(rebasingCommits) > 0 {
result = append(rebasingCommits, result...)
}
return result, nil
}
// extractCommitFromLine takes a line from a git log and extracts the sha, message, date, and tag if present
// then puts them into a commit object
// example input:
// 8ad01fe32fcc20f07bc6693f87aa4977c327f1e1|10 hours ago|Jesse Duffield| (HEAD -> master, tag: v0.15.2)|refresh commits when adding a tag
func (self *CommitLoader) extractCommitFromLine(line string) *models.Commit {
split := strings.SplitN(line, "\x00", 7)
sha := split[0]
unixTimestamp := split[1]
authorName := split[2]
authorEmail := split[3]
extraInfo := strings.TrimSpace(split[4])
parentHashes := split[5]
message := split[6]
tags := []string{}
if extraInfo != "" {
re := regexp.MustCompile(`tag: ([^,\)]+)`)
tagMatch := re.FindStringSubmatch(extraInfo)
if len(tagMatch) > 1 {
tags = append(tags, tagMatch[1])
}
}
unitTimestampInt, _ := strconv.Atoi(unixTimestamp)
parents := []string{}
if len(parentHashes) > 0 {
parents = strings.Split(parentHashes, " ")
}
return &models.Commit{
Sha: sha,
Name: message,
Tags: tags,
ExtraInfo: extraInfo,
UnixTimestamp: int64(unitTimestampInt),
AuthorName: authorName,
AuthorEmail: authorEmail,
Parents: parents,
}
}
func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode) ([]*models.Commit, error) {
commits, err := self.getRebasingCommits(rebaseMode)
if err != nil {
return nil, err
}
if len(commits) == 0 {
return nil, nil
}
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
// I suspect that will cause some damage
cmdObj := self.cmd.New(
fmt.Sprintf(
"git -c log.showSignature=false show %s --no-patch --oneline %s --abbrev=%d",
strings.Join(commitShas, " "),
prettyFormat,
20,
),
).DontLog()
hydratedCommits := make([]*models.Commit, 0, len(commits))
i := 0
err = cmdObj.RunAndProcessLines(func(line string) (bool, error) {
commit := self.extractCommitFromLine(line)
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
}
return hydratedCommits, nil
}
// getRebasingCommits obtains the commits that we're in the process of rebasing
func (self *CommitLoader) getRebasingCommits(rebaseMode enums.RebaseMode) ([]*models.Commit, error) {
switch rebaseMode {
case enums.REBASE_MODE_MERGING:
return self.getNormalRebasingCommits()
case enums.REBASE_MODE_INTERACTIVE:
return self.getInteractiveRebasingCommits()
default:
return nil, nil
}
}
func (self *CommitLoader) getNormalRebasingCommits() ([]*models.Commit, error) {
rewrittenCount := 0
bytesContent, err := self.readFile(filepath.Join(self.dotGitDir, "rebase-apply/rewritten"))
if err == nil {
content := string(bytesContent)
rewrittenCount = len(strings.Split(content, "\n"))
}
// we know we're rebasing, so lets get all the files whose names have numbers
commits := []*models.Commit{}
err = self.walkFiles(filepath.Join(self.dotGitDir, "rebase-apply"), func(path string, f os.FileInfo, err error) error {
if rewrittenCount > 0 {
rewrittenCount--
return nil
}
if err != nil {
return err
}
re := regexp.MustCompile(`^\d+$`)
if !re.MatchString(f.Name()) {
return nil
}
bytesContent, err := self.readFile(path)
if err != nil {
return err
}
content := string(bytesContent)
commit := self.commitFromPatch(content)
commits = append([]*models.Commit{commit}, commits...)
return nil
})
if err != nil {
return nil, err
}
return commits, nil
}
// git-rebase-todo example:
// pick ac446ae94ee560bdb8d1d057278657b251aaef17 ac446ae
// pick afb893148791a2fbd8091aeb81deba4930c73031 afb8931
// git-rebase-todo.backup example:
// pick 49cbba374296938ea86bbd4bf4fee2f6ba5cccf6 third commit on master
// pick ac446ae94ee560bdb8d1d057278657b251aaef17 blah commit on master
// pick afb893148791a2fbd8091aeb81deba4930c73031 fourth commit on master
// getInteractiveRebasingCommits takes our git-rebase-todo and our git-rebase-todo.backup files
// and extracts out the sha and names of commits that we still have to go
// in the rebase:
func (self *CommitLoader) getInteractiveRebasingCommits() ([]*models.Commit, error) {
bytesContent, err := self.readFile(filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo"))
if err != nil {
self.Log.Error(fmt.Sprintf("error occurred reading git-rebase-todo: %s", err.Error()))
// we assume an error means the file doesn't exist so we just return
return nil, nil
}
commits := []*models.Commit{}
todos, err := todo.Parse(bytes.NewBuffer(bytesContent))
if err != nil {
self.Log.Error(fmt.Sprintf("error occurred while parsing git-rebase-todo file: %s", err.Error()))
return nil, nil
}
for _, t := range todos {
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: "rebasing",
Action: t.Command.String(),
})
}
return commits, nil
}
// assuming the file starts like this:
// From e93d4193e6dd45ca9cf3a5a273d7ba6cd8b8fb20 Mon Sep 17 00:00:00 2001
// From: Lazygit Tester <test@example.com>
// Date: Wed, 5 Dec 2018 21:03:23 +1100
// Subject: second commit on master
func (self *CommitLoader) commitFromPatch(content string) *models.Commit {
lines := strings.Split(content, "\n")
sha := strings.Split(lines[0], " ")[1]
name := strings.TrimPrefix(lines[3], "Subject: ")
return &models.Commit{
Sha: sha,
Name: name,
Status: "rebasing",
}
}
func (self *CommitLoader) setCommitMergedStatuses(refName string, commits []*models.Commit) ([]*models.Commit, error) {
ancestor, err := self.getMergeBase(refName)
if err != nil {
return nil, err
}
if ancestor == "" {
return commits, nil
}
passedAncestor := false
for i, commit := range commits {
if strings.HasPrefix(ancestor, commit.Sha) {
passedAncestor = true
}
if commit.Status != "pushed" {
continue
}
if passedAncestor {
commits[i].Status = "merged"
}
}
return commits, nil
}
func (self *CommitLoader) getMergeBase(refName string) (string, error) {
currentBranch, _, err := self.getCurrentBranchName()
if err != nil {
return "", err
}
baseBranch := "master"
if strings.HasPrefix(currentBranch, "feature/") {
baseBranch = "develop"
}
// swallowing error because it's not a big deal; probably because there are no commits yet
output, _ := self.cmd.New(fmt.Sprintf("git merge-base %s %s", self.cmd.Quote(refName), self.cmd.Quote(baseBranch))).DontLog().RunWithOutput()
return ignoringWarnings(output), nil
}
func ignoringWarnings(commandOutput string) string {
trimmedOutput := strings.TrimSpace(commandOutput)
split := strings.Split(trimmedOutput, "\n")
// need to get last line in case the first line is a warning about how the error is ambiguous.
// At some point we should find a way to make it unambiguous
lastLine := split[len(split)-1]
return lastLine
}
// getFirstPushedCommit returns the first commit SHA which has been pushed to the ref's upstream.
// all commits above this are deemed unpushed and marked as such.
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(refName)),
).
DontLog().
RunWithOutput()
if err != nil {
return "", err
}
return ignoringWarnings(output), nil
}
// getLog gets the git log.
func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj {
limitFlag := ""
if opts.Limit {
limitFlag = " -300"
}
filterFlag := ""
if opts.FilterPath != "" {
filterFlag = fmt.Sprintf(" --follow -- %s", self.cmd.Quote(opts.FilterPath))
}
config := self.UserConfig.Git.Log
orderFlag := "--" + config.Order
allFlag := ""
if opts.All {
allFlag = " --all"
}
return self.cmd.New(
fmt.Sprintf(
"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,
filterFlag,
),
).DontLog()
}
var prettyFormat = fmt.Sprintf(
"--pretty=format:\"%%H%s%%at%s%%aN%s%%ae%s%%d%s%%p%s%%s\"",
NULL_CODE,
NULL_CODE,
NULL_CODE,
NULL_CODE,
NULL_CODE,
NULL_CODE,
)
const NULL_CODE = "%x00"
+206
View File
@@ -0,0 +1,206 @@
package loaders
import (
"path/filepath"
"strings"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
)
var commitsOutput = strings.Replace(`0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|jessedduffield@gmail.com| (HEAD -> better-tests)|b21997d6b4cbdf84b149|better typing for rebase mode
b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164|1640824515|Jesse Duffield|jessedduffield@gmail.com| (origin/better-tests)|e94e8fc5b6fab4cb755f|fix logging
e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c|1640823749|Jesse Duffield|jessedduffield@gmail.com||d8084cd558925eb7c9c3|refactor
d8084cd558925eb7c9c38afeed5725c21653ab90|1640821426|Jesse Duffield|jessedduffield@gmail.com||65f910ebd85283b5cce9|WIP
65f910ebd85283b5cce9bf67d03d3f1a9ea3813a|1640821275|Jesse Duffield|jessedduffield@gmail.com||26c07b1ab33860a1a759|WIP
26c07b1ab33860a1a7591a0638f9925ccf497ffa|1640750752|Jesse Duffield|jessedduffield@gmail.com||3d4470a6c072208722e5|WIP
3d4470a6c072208722e5ae9a54bcb9634959a1c5|1640748818|Jesse Duffield|jessedduffield@gmail.com||053a66a7be3da43aacdc|WIP
053a66a7be3da43aacdc7aa78e1fe757b82c4dd2|1640739815|Jesse Duffield|jessedduffield@gmail.com||985fe482e806b172aea4|refactoring the config struct`, "|", "\x00", -1)
func TestGetCommits(t *testing.T) {
type scenario struct {
testName string
runner *oscommands.FakeCmdObjRunner
expectedCommits []*models.Commit
expectedError error
rebaseMode enums.RebaseMode
currentBranchName string
opts GetCommitsOptions
}
scenarios := []scenario{
{
testName: "should return no commits if there are none",
rebaseMode: enums.REBASE_MODE_NONE,
currentBranchName: "master",
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
runner: oscommands.NewFakeRunner(t).
Expect(`git merge-base "HEAD" "HEAD"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", 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,
},
{
testName: "should return commits if they are present",
rebaseMode: enums.REBASE_MODE_NONE,
currentBranchName: "master",
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
runner: oscommands.NewFakeRunner(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 -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),
expectedCommits: []*models.Commit{
{
Sha: "0eea75e8c631fba6b58135697835d58ba4c18dbc",
Name: "better typing for rebase mode",
Status: "unpushed",
Action: "",
Tags: []string{},
ExtraInfo: "(HEAD -> better-tests)",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640826609,
Parents: []string{
"b21997d6b4cbdf84b149",
},
},
{
Sha: "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164",
Name: "fix logging",
Status: "pushed",
Action: "",
Tags: []string{},
ExtraInfo: "(origin/better-tests)",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640824515,
Parents: []string{
"e94e8fc5b6fab4cb755f",
},
},
{
Sha: "e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c",
Name: "refactor",
Status: "pushed",
Action: "",
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640823749,
Parents: []string{
"d8084cd558925eb7c9c3",
},
},
{
Sha: "d8084cd558925eb7c9c38afeed5725c21653ab90",
Name: "WIP",
Status: "pushed",
Action: "",
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640821426,
Parents: []string{
"65f910ebd85283b5cce9",
},
},
{
Sha: "65f910ebd85283b5cce9bf67d03d3f1a9ea3813a",
Name: "WIP",
Status: "pushed",
Action: "",
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640821275,
Parents: []string{
"26c07b1ab33860a1a759",
},
},
{
Sha: "26c07b1ab33860a1a7591a0638f9925ccf497ffa",
Name: "WIP",
Status: "merged",
Action: "",
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640750752,
Parents: []string{
"3d4470a6c072208722e5",
},
},
{
Sha: "3d4470a6c072208722e5ae9a54bcb9634959a1c5",
Name: "WIP",
Status: "merged",
Action: "",
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640748818,
Parents: []string{
"053a66a7be3da43aacdc",
},
},
{
Sha: "053a66a7be3da43aacdc7aa78e1fe757b82c4dd2",
Name: "refactoring the config struct",
Status: "merged",
Action: "",
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640739815,
Parents: []string{
"985fe482e806b172aea4",
},
},
},
expectedError: nil,
},
}
for _, scenario := range scenarios {
scenario := scenario
t.Run(scenario.testName, func(t *testing.T) {
builder := &CommitLoader{
Common: utils.NewDummyCommon(),
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
getCurrentBranchName: func() (string, string, error) {
return scenario.currentBranchName, scenario.currentBranchName, nil
},
getRebaseMode: func() (enums.RebaseMode, error) { return scenario.rebaseMode, nil },
dotGitDir: ".git",
readFile: func(filename string) ([]byte, error) {
return []byte(""), nil
},
walkFiles: func(root string, fn filepath.WalkFunc) error {
return nil
},
}
commits, err := builder.GetCommits(scenario.opts)
assert.Equal(t, scenario.expectedCommits, commits)
assert.Equal(t, scenario.expectedError, err)
scenario.runner.CheckForMissingCalls()
})
}
}
+123
View File
@@ -0,0 +1,123 @@
package loaders
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
)
type FileLoaderConfig interface {
GetShowUntrackedFiles() string
}
type FileLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
config FileLoaderConfig
getFileType func(string) string
}
func NewFileLoader(cmn *common.Common, cmd oscommands.ICmdObjBuilder, config FileLoaderConfig) *FileLoader {
return &FileLoader{
Common: cmn,
cmd: cmd,
getFileType: oscommands.FileType,
config: config,
}
}
type GetStatusFileOptions struct {
NoRenames bool
}
func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
// check if config wants us ignoring untracked files
untrackedFilesSetting := self.config.GetShowUntrackedFiles()
if untrackedFilesSetting == "" {
untrackedFilesSetting = "all"
}
untrackedFilesArg := fmt.Sprintf("--untracked-files=%s", untrackedFilesSetting)
statuses, err := self.GitStatus(GitStatusOptions{NoRenames: opts.NoRenames, UntrackedFilesArg: untrackedFilesArg})
if err != nil {
self.Log.Error(err)
}
files := []*models.File{}
for _, status := range statuses {
if strings.HasPrefix(status.StatusString, "warning") {
self.Log.Warningf("warning when calling git status: %s", status.StatusString)
continue
}
file := &models.File{
Name: status.Name,
PreviousName: status.PreviousName,
DisplayString: status.StatusString,
Type: self.getFileType(status.Name),
}
models.SetStatusFields(file, status.Change)
files = append(files, file)
}
return files
}
// GitStatus returns the file status of the repo
type GitStatusOptions struct {
NoRenames bool
UntrackedFilesArg string
}
type FileStatus struct {
StatusString string
Change string // ??, MM, AM, ...
Name string
PreviousName string
}
func (c *FileLoader) GitStatus(opts GitStatusOptions) ([]FileStatus, error) {
noRenamesFlag := ""
if opts.NoRenames {
noRenamesFlag = " --no-renames"
}
statusLines, err := c.cmd.New(fmt.Sprintf("git status %s --porcelain -z%s", opts.UntrackedFilesArg, noRenamesFlag)).DontLog().RunWithOutput()
if err != nil {
return []FileStatus{}, err
}
splitLines := strings.Split(statusLines, "\x00")
response := []FileStatus{}
for i := 0; i < len(splitLines); i++ {
original := splitLines[i]
if len(original) < 3 {
continue
}
status := FileStatus{
StatusString: original,
Change: original[:2],
Name: original[3:],
PreviousName: "",
}
if strings.HasPrefix(status.Change, "R") {
// if a line starts with 'R' then the next line is the original file.
status.PreviousName = splitLines[i+1]
status.StatusString = fmt.Sprintf("%s %s -> %s", status.Change, status.PreviousName, status.Name)
i++
}
response = append(response, status)
}
return response, nil
}
+210
View File
@@ -0,0 +1,210 @@
package loaders
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
)
func TestFileGetStatusFiles(t *testing.T) {
type scenario struct {
testName string
runner oscommands.ICmdObjRunner
expectedFiles []*models.File
}
scenarios := []scenario{
{
"No files found",
oscommands.NewFakeRunner(t).
Expect(`git status --untracked-files=yes --porcelain -z`, "", nil),
[]*models.File{},
},
{
"Several files found",
oscommands.NewFakeRunner(t).
Expect(
`git status --untracked-files=yes --porcelain -z`,
"MM file1.txt\x00A file3.txt\x00AM file2.txt\x00?? file4.txt\x00UU file5.txt",
nil,
),
[]*models.File{
{
Name: "file1.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "MM file1.txt",
Type: "file",
ShortStatus: "MM",
},
{
Name: "file3.txt",
HasStagedChanges: true,
HasUnstagedChanges: false,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "A file3.txt",
Type: "file",
ShortStatus: "A ",
},
{
Name: "file2.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "AM file2.txt",
Type: "file",
ShortStatus: "AM",
},
{
Name: "file4.txt",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "?? file4.txt",
Type: "file",
ShortStatus: "??",
},
{
Name: "file5.txt",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: true,
HasInlineMergeConflicts: true,
DisplayString: "UU file5.txt",
Type: "file",
ShortStatus: "UU",
},
},
},
{
"File with new line char",
oscommands.NewFakeRunner(t).
Expect(`git status --untracked-files=yes --porcelain -z`, "MM a\nb.txt", nil),
[]*models.File{
{
Name: "a\nb.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "MM a\nb.txt",
Type: "file",
ShortStatus: "MM",
},
},
},
{
"Renamed files",
oscommands.NewFakeRunner(t).
Expect(
`git status --untracked-files=yes --porcelain -z`,
"R after1.txt\x00before1.txt\x00RM after2.txt\x00before2.txt",
nil,
),
[]*models.File{
{
Name: "after1.txt",
PreviousName: "before1.txt",
HasStagedChanges: true,
HasUnstagedChanges: false,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "R before1.txt -> after1.txt",
Type: "file",
ShortStatus: "R ",
},
{
Name: "after2.txt",
PreviousName: "before2.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "RM before2.txt -> after2.txt",
Type: "file",
ShortStatus: "RM",
},
},
},
{
"File with arrow in name",
oscommands.NewFakeRunner(t).
Expect(
`git status --untracked-files=yes --porcelain -z`,
`?? a -> b.txt`,
nil,
),
[]*models.File{
{
Name: "a -> b.txt",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "?? a -> b.txt",
Type: "file",
ShortStatus: "??",
},
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
cmd := oscommands.NewDummyCmdObjBuilder(s.runner)
loader := &FileLoader{
Common: utils.NewDummyCommon(),
cmd: cmd,
config: &FakeFileLoaderConfig{showUntrackedFiles: "yes"},
getFileType: func(string) string { return "file" },
}
assert.EqualValues(t, s.expectedFiles, loader.GetStatusFiles(GetStatusFileOptions{}))
})
}
}
type FakeFileLoaderConfig struct {
showUntrackedFiles string
}
func (self *FakeFileLoaderConfig) GetShowUntrackedFiles() string {
return self.showUntrackedFiles
}
+77
View File
@@ -0,0 +1,77 @@
package loaders
import (
"fmt"
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
)
type ReflogCommitLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
}
func NewReflogCommitLoader(common *common.Common, cmd oscommands.ICmdObjBuilder) *ReflogCommitLoader {
return &ReflogCommitLoader{
Common: common,
cmd: cmd,
}
}
// GetReflogCommits only returns the new reflog commits since the given lastReflogCommit
// if none is passed (i.e. it's value is nil) then we get all the reflog commits
func (self *ReflogCommitLoader) GetReflogCommits(lastReflogCommit *models.Commit, filterPath string) ([]*models.Commit, bool, error) {
commits := make([]*models.Commit, 0)
filterPathArg := ""
if filterPath != "" {
filterPathArg = fmt.Sprintf(" --follow -- %s", self.cmd.Quote(filterPath))
}
cmdObj := self.cmd.New(fmt.Sprintf(`git -c log.showSignature=false log -g --abbrev=40 --format="%s"%s`, "%h%x00%ct%x00%gs%x00%p", filterPathArg)).DontLog()
onlyObtainedNewReflogCommits := false
err := cmdObj.RunAndProcessLines(func(line string) (bool, error) {
fields := strings.SplitN(line, "\x00", 4)
if len(fields) <= 3 {
return false, nil
}
unixTimestamp, _ := strconv.Atoi(fields[1])
parentHashes := fields[3]
parents := []string{}
if len(parentHashes) > 0 {
parents = strings.Split(parentHashes, " ")
}
commit := &models.Commit{
Sha: fields[0],
Name: fields[2],
UnixTimestamp: int64(unixTimestamp),
Status: "reflog",
Parents: parents,
}
// note that the unix timestamp here is the timestamp of the COMMIT, not the reflog entry itself,
// so two consecutive reflog entries may have both the same SHA and therefore same timestamp.
// We use the reflog message to disambiguate, and fingers crossed that we never see the same of those
// twice in a row. Reason being that it would mean we'd be erroneously exiting early.
if lastReflogCommit != nil && commit.Sha == lastReflogCommit.Sha && commit.UnixTimestamp == lastReflogCommit.UnixTimestamp && commit.Name == lastReflogCommit.Name {
onlyObtainedNewReflogCommits = true
// after this point we already have these reflogs loaded so we'll simply return the new ones
return true, nil
}
commits = append(commits, commit)
return false, nil
})
if err != nil {
return nil, false, err
}
return commits, onlyObtainedNewReflogCommits, nil
}
+169
View File
@@ -0,0 +1,169 @@
package loaders
import (
"errors"
"strings"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sanity-io/litter"
"github.com/stretchr/testify/assert"
)
var reflogOutput = strings.Replace(`c3c4b66b64c97ffeecde|1643150483|checkout: moving from A to B|51baa8c1
c3c4b66b64c97ffeecde|1643150483|checkout: moving from B to A|51baa8c1
c3c4b66b64c97ffeecde|1643150483|checkout: moving from A to B|51baa8c1
c3c4b66b64c97ffeecde|1643150483|checkout: moving from master to A|51baa8c1
f4ddf2f0d4be4ccc7efa|1643149435|checkout: moving from A to master|51baa8c1
`, "|", "\x00", -1)
func TestGetReflogCommits(t *testing.T) {
type scenario struct {
testName string
runner *oscommands.FakeCmdObjRunner
lastReflogCommit *models.Commit
filterPath string
expectedCommits []*models.Commit
expectedOnlyObtainedNew bool
expectedError error
}
scenarios := []scenario{
{
testName: "no reflog entries",
runner: oscommands.NewFakeRunner(t).
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p"`, "", nil),
lastReflogCommit: nil,
expectedCommits: []*models.Commit{},
expectedOnlyObtainedNew: false,
expectedError: nil,
},
{
testName: "some reflog entries",
runner: oscommands.NewFakeRunner(t).
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p"`, reflogOutput, nil),
lastReflogCommit: nil,
expectedCommits: []*models.Commit{
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from A to B",
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from B to A",
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from A to B",
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from master to A",
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
{
Sha: "f4ddf2f0d4be4ccc7efa",
Name: "checkout: moving from A to master",
Status: "reflog",
UnixTimestamp: 1643149435,
Parents: []string{"51baa8c1"},
},
},
expectedOnlyObtainedNew: false,
expectedError: nil,
},
{
testName: "some reflog entries where last commit is given",
runner: oscommands.NewFakeRunner(t).
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p"`, reflogOutput, nil),
lastReflogCommit: &models.Commit{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from B to A",
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
expectedCommits: []*models.Commit{
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from A to B",
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
},
expectedOnlyObtainedNew: true,
expectedError: nil,
},
{
testName: "when passing filterPath",
runner: oscommands.NewFakeRunner(t).
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p" --follow -- "path"`, reflogOutput, nil),
lastReflogCommit: &models.Commit{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from B to A",
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
filterPath: "path",
expectedCommits: []*models.Commit{
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from A to B",
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
},
expectedOnlyObtainedNew: true,
expectedError: nil,
},
{
testName: "when command returns error",
runner: oscommands.NewFakeRunner(t).
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p"`, "", errors.New("haha")),
lastReflogCommit: nil,
filterPath: "",
expectedCommits: nil,
expectedOnlyObtainedNew: false,
expectedError: errors.New("haha"),
},
}
for _, scenario := range scenarios {
scenario := scenario
t.Run(scenario.testName, func(t *testing.T) {
builder := &ReflogCommitLoader{
Common: utils.NewDummyCommon(),
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
}
commits, onlyObtainednew, err := builder.GetReflogCommits(scenario.lastReflogCommit, scenario.filterPath)
assert.Equal(t, scenario.expectedOnlyObtainedNew, onlyObtainednew)
assert.Equal(t, scenario.expectedError, err)
t.Logf("actual commits: \n%s", litter.Sdump(commits))
assert.Equal(t, scenario.expectedCommits, commits)
scenario.runner.CheckForMissingCalls()
})
}
}
+77
View File
@@ -0,0 +1,77 @@
package loaders
import (
"fmt"
"regexp"
"strings"
"github.com/jesseduffield/generics/slices"
gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
)
type RemoteLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
getGoGitRemotes func() ([]*gogit.Remote, error)
}
func NewRemoteLoader(
common *common.Common,
cmd oscommands.ICmdObjBuilder,
getGoGitRemotes func() ([]*gogit.Remote, error),
) *RemoteLoader {
return &RemoteLoader{
Common: common,
cmd: cmd,
getGoGitRemotes: getGoGitRemotes,
}
}
func (self *RemoteLoader) GetRemotes() ([]*models.Remote, error) {
remoteBranchesStr, err := self.cmd.New("git branch -r").DontLog().RunWithOutput()
if err != nil {
return nil, err
}
goGitRemotes, err := self.getGoGitRemotes()
if err != nil {
return nil, err
}
// first step is to get our remotes from go-git
remotes := slices.Map(goGitRemotes, func(goGitRemote *gogit.Remote) *models.Remote {
remoteName := goGitRemote.Config().Name
re := regexp.MustCompile(fmt.Sprintf(`(?m)^\s*%s\/([\S]+)`, remoteName))
matches := re.FindAllStringSubmatch(remoteBranchesStr, -1)
branches := slices.Map(matches, func(match []string) *models.RemoteBranch {
return &models.RemoteBranch{
Name: match[1],
RemoteName: remoteName,
}
})
return &models.Remote{
Name: goGitRemote.Config().Name,
Urls: goGitRemote.Config().URLs,
Branches: branches,
}
})
// now lets sort our remotes by name alphabetically
slices.SortFunc(remotes, func(a, b *models.Remote) bool {
// we want origin at the top because we'll be most likely to want it
if a.Name == "origin" {
return true
}
if b.Name == "origin" {
return false
}
return strings.ToLower(a.Name) < strings.ToLower(b.Name)
})
return remotes, nil
}
+79
View File
@@ -0,0 +1,79 @@
package loaders
import (
"regexp"
"strconv"
"strings"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type StashLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
}
func NewStashLoader(
common *common.Common,
cmd oscommands.ICmdObjBuilder,
) *StashLoader {
return &StashLoader{
Common: common,
cmd: cmd,
}
}
func (self *StashLoader) GetStashEntries(filterPath string) []*models.StashEntry {
if filterPath == "" {
return self.getUnfilteredStashEntries()
}
rawString, err := self.cmd.New("git stash list --name-only").DontLog().RunWithOutput()
if err != nil {
return self.getUnfilteredStashEntries()
}
stashEntries := []*models.StashEntry{}
var currentStashEntry *models.StashEntry
lines := utils.SplitLines(rawString)
isAStash := func(line string) bool { return strings.HasPrefix(line, "stash@{") }
re := regexp.MustCompile(`stash@\{(\d+)\}`)
outer:
for i := 0; i < len(lines); i++ {
if !isAStash(lines[i]) {
continue
}
match := re.FindStringSubmatch(lines[i])
idx, err := strconv.Atoi(match[1])
if err != nil {
return self.getUnfilteredStashEntries()
}
currentStashEntry = self.stashEntryFromLine(lines[i], idx)
for i+1 < len(lines) && !isAStash(lines[i+1]) {
i++
if lines[i] == filterPath {
stashEntries = append(stashEntries, currentStashEntry)
continue outer
}
}
}
return stashEntries
}
func (self *StashLoader) getUnfilteredStashEntries() []*models.StashEntry {
rawString, _ := self.cmd.New("git stash list --pretty='%gs'").DontLog().RunWithOutput()
return slices.MapWithIndex(utils.SplitLines(rawString), func(line string, index int) *models.StashEntry {
return self.stashEntryFromLine(line, index)
})
}
func (c *StashLoader) stashEntryFromLine(line string, index int) *models.StashEntry {
return &models.StashEntry{
Name: line,
Index: index,
}
}
+60
View File
@@ -0,0 +1,60 @@
package loaders
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
)
func TestGetStashEntries(t *testing.T) {
type scenario struct {
testName string
filterPath string
runner oscommands.ICmdObjRunner
expectedStashEntries []*models.StashEntry
}
scenarios := []scenario{
{
"No stash entries found",
"",
oscommands.NewFakeRunner(t).
Expect(`git stash list --pretty='%gs'`, "", nil),
[]*models.StashEntry{},
},
{
"Several stash entries found",
"",
oscommands.NewFakeRunner(t).
Expect(
`git stash list --pretty='%gs'`,
"WIP on add-pkg-commands-test: 55c6af2 increase parallel build\nWIP on master: bb86a3f update github template",
nil,
),
[]*models.StashEntry{
{
Index: 0,
Name: "WIP on add-pkg-commands-test: 55c6af2 increase parallel build",
},
{
Index: 1,
Name: "WIP on master: bb86a3f update github template",
},
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
cmd := oscommands.NewDummyCmdObjBuilder(s.runner)
loader := NewStashLoader(utils.NewDummyCommon(), cmd)
assert.EqualValues(t, s.expectedStashEntries, loader.GetStashEntries(s.filterPath))
})
}
}
+43
View File
@@ -0,0 +1,43 @@
package loaders
import (
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type TagLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
}
func NewTagLoader(
common *common.Common,
cmd oscommands.ICmdObjBuilder,
) *TagLoader {
return &TagLoader{
Common: common,
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 --sort=-creatordate`).DontLog().RunWithOutput()
if err != nil {
return nil, err
}
split := utils.SplitLines(tagsOutput)
tags := slices.Map(split, func(tagName string) *models.Tag {
return &models.Tag{
Name: tagName,
}
})
return tags, nil
}
+68
View File
@@ -0,0 +1,68 @@
package loaders
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
)
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
runner *oscommands.FakeCmdObjRunner
expectedTags []*models.Tag
expectedError error
}
scenarios := []scenario{
{
testName: "should return no tags if there are none",
runner: oscommands.NewFakeRunner(t).
Expect(`git tag --list --sort=-creatordate`, "", nil),
expectedTags: []*models.Tag{},
expectedError: nil,
},
{
testName: "should return tags if present",
runner: oscommands.NewFakeRunner(t).
Expect(`git tag --list --sort=-creatordate`, 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,
},
}
for _, scenario := range scenarios {
scenario := scenario
t.Run(scenario.testName, func(t *testing.T) {
loader := &TagLoader{
Common: utils.NewDummyCommon(),
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
}
tags, err := loader.GetTags()
assert.Equal(t, scenario.expectedTags, tags)
assert.Equal(t, scenario.expectedError, err)
scenario.runner.CheckForMissingCalls()
})
}
}
-4
View File
@@ -11,7 +11,6 @@ type Branch struct {
Pullables string
UpstreamGone bool
Head bool
DetachedHead bool
// if we have a named remote locally this will be the name of that remote e.g.
// 'origin' or 'tiwood'. If we don't have the remote locally it'll look like
// 'git@github.com:tiwood/lazygit.git'
@@ -20,9 +19,6 @@ type Branch struct {
}
func (b *Branch) FullRefName() string {
if b.DetachedHead {
return b.Name
}
return "refs/heads/" + b.Name
}
+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 != ""
}
-5
View File
@@ -29,7 +29,6 @@ type IFile interface {
GetIsTracked() bool
GetPath() string
GetPreviousPath() string
GetIsFile() bool
}
func (f *File) IsRename() bool {
@@ -93,10 +92,6 @@ func (f *File) GetPreviousPath() string {
return f.PreviousName
}
func (f *File) GetIsFile() bool {
return true
}
type StatusFields struct {
HasStagedChanges bool
HasUnstagedChanges bool
+25
View File
@@ -0,0 +1,25 @@
package models
type GithubPullRequest struct {
HeadRefName string `json:"headRefName"`
Number int `json:"number"`
State string `json:"state"` // "MERGED", "OPEN", "CLOSED"
Url string `json:"url"`
HeadRepositoryOwner GithubRepositoryOwner `json:"headRepositoryOwner"`
}
func (pr *GithubPullRequest) UserName() string {
// e.g. 'jesseduffield'
return pr.HeadRepositoryOwner.Login
}
func (pr *GithubPullRequest) BranchName() string {
// e.g. 'feature/my-feature'
return pr.HeadRefName
}
type GithubRepositoryOwner struct {
ID string `json:"id"`
Name string `json:"name"`
Login string `json:"login"`
}
+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
}
-6
View File
@@ -22,8 +22,6 @@ type ICmdObj interface {
Run() error
// runs the command and returns the output as a string, and an error if any
RunWithOutput() (string, error)
// runs the command and returns stdout and stderr as a string, and an error if any
RunWithOutputs() (string, string, error)
// runs the command and runs a callback function on each line of the output. If the callback returns true for the boolean value, we kill the process and return.
RunAndProcessLines(onLine func(line string) (bool, error)) error
@@ -164,10 +162,6 @@ func (self *CmdObj) RunWithOutput() (string, error) {
return self.runner.RunWithOutput(self)
}
func (self *CmdObj) RunWithOutputs() (string, string, error) {
return self.runner.RunWithOutputs(self)
}
func (self *CmdObj) RunAndProcessLines(onLine func(line string) (bool, error)) error {
return self.runner.RunAndProcessLines(self, onLine)
}
+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(
+10 -73
View File
@@ -15,7 +15,6 @@ import (
type ICmdObjRunner interface {
Run(cmdObj ICmdObj) error
RunWithOutput(cmdObj ICmdObj) (string, error)
RunWithOutputs(cmdObj ICmdObj) (string, string, error)
RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error
}
@@ -25,7 +24,6 @@ const (
Password CredentialType = iota
Username
Passphrase
PIN
)
type cmdObjRunner struct {
@@ -78,31 +76,6 @@ func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
return self.RunWithOutputAux(cmdObj)
}
func (self *cmdObjRunner) RunWithOutputs(cmdObj ICmdObj) (string, string, error) {
if cmdObj.Mutex() != nil {
cmdObj.Mutex().Lock()
defer cmdObj.Mutex().Unlock()
}
if cmdObj.GetCredentialStrategy() != NONE {
err := self.runWithCredentialHandling(cmdObj)
// for now we're not capturing output, just because it would take a little more
// effort and there's currently no use case for it. Some commands call RunWithOutputs
// but ignore the output, hence why we've got this check here.
return "", "", err
}
if cmdObj.ShouldStreamOutput() {
err := self.runAndStream(cmdObj)
// for now we're not capturing output, just because it would take a little more
// effort and there's currently no use case for it. Some commands call RunWithOutputs
// but ignore the output, hence why we've got this check here.
return "", "", err
}
return self.RunWithOutputsAux(cmdObj)
}
func (self *cmdObjRunner) RunWithOutputAux(cmdObj ICmdObj) (string, error) {
self.log.WithField("command", cmdObj.ToString()).Debug("RunCommand")
@@ -117,28 +90,6 @@ func (self *cmdObjRunner) RunWithOutputAux(cmdObj ICmdObj) (string, error) {
return output, err
}
func (self *cmdObjRunner) RunWithOutputsAux(cmdObj ICmdObj) (string, string, error) {
self.log.WithField("command", cmdObj.ToString()).Debug("RunCommand")
if cmdObj.ShouldLog() {
self.logCmdObj(cmdObj)
}
var outBuffer, errBuffer bytes.Buffer
cmd := cmdObj.GetCmd()
cmd.Stdout = &outBuffer
cmd.Stderr = &errBuffer
err := cmd.Run()
stdout := outBuffer.String()
stderr, err := sanitisedCommandOutput(errBuffer.Bytes(), err)
if err != nil {
self.log.WithField("command", cmdObj.ToString()).Error(stderr)
}
return stdout, stderr, err
}
func (self *cmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error {
if cmdObj.Mutex() != nil {
cmdObj.Mutex().Lock()
@@ -323,23 +274,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 +281,21 @@ 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,
}
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())
}
})
}
}
-2
View File
@@ -19,7 +19,6 @@ type OSCommandDeps struct {
GetenvFn func(string) string
RemoveFileFn func(string) error
Cmd *CmdObjBuilder
TempDir string
}
func NewDummyOSCommandWithDeps(deps OSCommandDeps) *OSCommand {
@@ -39,7 +38,6 @@ func NewDummyOSCommandWithDeps(deps OSCommandDeps) *OSCommand {
getenvFn: deps.GetenvFn,
removeFileFn: deps.RemoveFileFn,
guiIO: NewNullGuiIO(utils.NewDummyLog()),
tempDir: deps.TempDir,
}
}
@@ -44,11 +44,6 @@ func (self *FakeCmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
return output, err
}
func (self *FakeCmdObjRunner) RunWithOutputs(cmdObj ICmdObj) (string, string, error) {
output, err := self.RunWithOutput(cmdObj)
return output, "", err
}
func (self *FakeCmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error {
output, err := self.RunWithOutput(cmdObj)
if err != nil {
+2 -1
View File
@@ -2,6 +2,7 @@ package oscommands
import (
"io"
"io/ioutil"
"github.com/sirupsen/logrus"
)
@@ -44,7 +45,7 @@ func NewNullGuiIO(log *logrus.Entry) *guiIO {
return &guiIO{
log: log,
logCommandFn: func(string, bool) {},
newCmdWriterFn: func() io.Writer { return io.Discard },
newCmdWriterFn: func() io.Writer { return ioutil.Discard },
promptForCredentialFn: failPromptFn,
}
}

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